Chariot Training Classes

Training Courses

I run Chariot's training and mentoring services. We provide training in AngularJS, HTML5, Spring, Hibernate, Maven, Scala, and more.

Chariot Education Services

Technology

Chariot Emerging Tech

Learn about upcoming technologies and trends from my colleagues at Chariot Solutions.

Resources

Chariot Conferences

Podcasts

Entries in spring-roo (25)

Saturday
Feb252012

Quizzo Saturday - testing web flows

GitHub commit URLs: 82add3e1e23eee8b84058c12fcd6ff211f70096e and more importantly, 4cfed580c3404e61d501ac4ee0182c294b1b0b89

Ok, this is kind of a tangent. But I wanted to really debug the flow logic for these interactions, and better understand the flow logic itself before I bother to write up nice looking views.

Test that our flow starts with the correct state

I started with this first step, which is pretty easy to test:



<var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" name="teamSetupForm"/>

<view-state id="register-team" view="playQuizzo/register-team"
            model="flowScope.teamSetupForm">
    <transition on="continue" 
                to="register-team-members" 
                bind="true" validate="true"/>
</view-state>

This state fills in a portion of the teamSetupForm flow-scoped variable. How do we test this? Somewhat easily. First, we start by building a test that is a subclass of AbstractXmlFlowExecutionTests:

public class QuizzoFlowTest extends AbstractXmlFlowExecutionTests {

  @Override
  protected FlowDefinitionResource getResource(
               FlowDefinitionResourceFactory resourceFactory) {

    return resourceFactory.
        createFileResource("src/main/webapp/WEB-INF/views/playQuizzo/flow.xml");
  }
}

Next, we have to build a method to expose a stub version of our Web Flow Spring Bean, the QuizzoFlowManager. It's referred to in the rest of the flow as quizzoFlowManager. This stub will replace our Spring Bean, and will provide access to any methods the flow calls. The flow execution engine may throw errors if it looks for this bean while parsing the form. So, we add this method to the test:


@Override
protected void configureFlowBuilderContext(
               MockFlowBuilderContext builderContext) {

  StubQuizzoFlowManager stubQuizzoFlowManager = 
                        new StubQuizzoFlowManager();

  builderContext.registerBean("quizzoFlowManager", 
                        stubQuizzoFlowManager);
}

This method is called once we execute any flow start or resume methods in our mock flow engine.

Writing a flow test

Consider the test code below:

public void testStartFlow() {
  MutableAttributeMap map = new LocalAttributeMap();
  MockExternalContext context = new MockExternalContext();
  startFlow(map, context);
  assertCurrentStateEquals("register-team");
  TeamSetupForm form = (TeamSetupForm) getFlowScope().get("teamSetupForm");
  assertNotNull(form);
}

The test does the following things:

  • Sets up a attribute map, required for starting a flow. This is just a property map.
  • Builds a fake Flow context, also required when launching a flow.
  • Calls the base test class's startFlow method, which launches the flow.
  • Checks to make sure that the flow is waiting on the register-team view state.
  • Checks to make sure the Web Flow engine creates the variable, teamSetupForm in the var webflow tag.
  • Checks to make sure the form is not null.

But what about checking events? For that, we'll push a "continue" event, and set the values that the form would have submitted in our Form bean:

@Test
public void testSubmitTeamName() {
  setCurrentState("register-team");
  MockExternalContext context = new MockExternalContext();

  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets", "When you're a Jet you're a Jet"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("register-team-members");
}

In this test, we start with the flow sitting on the register-team state. Since Web Flow is not running any states before our unit test, we have to create our teamSetupForm ourselves, and set the value of it using a helper method to include the team name and team message.

We then tell the context to send the continue event, and resume our flow. We check once this completes to make sure we are now sitting on the register-team-members state.

A more complex example

Now we are going to test the next state and its transition to the following state using continue:

<view-state id="register-team-members" view="playQuizzo/register-team-members"
          model="flowScope.teamSetupForm">

  <transition on="continue" to="ready-to-play" bind="true" validate="true">
      <evaluate expression="quizzoFlowManager.saveTeamData(flowRequestContext)"/>
  </transition>
  <transition on="add-team-member"/>
  <transition on="remove-team-member"/>
  <transition on="back" to="register-team"/>
</view-state>

<action-state id="ready-to-play">
  <evaluate expression="quizzoFlowManager.pollReady(flowRequestContext)"/>
  <transition on="yes" to="play-round"/>
  <transition on="poll" to="poll"/>
</action-state>

You may think this test executes just fine:

@Test
public void testSubmitTeamMembers() {
  setCurrentState("register-team-members");

  MockExternalContext context = new MockExternalContext();
  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets",
              "When you're a Jet you're a Jet",
      "Ice", "Action", "Baby John", "Tiger", "Joyboy"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("ready-to-play");
}

But it fails on the state assertion. It states that the state is actually play-round. How can this be?

Action States

This is because we're using an action state from our web flow. Action states do not wait on anything. They immediately execute their evaluations, and take action from their responses. I knew this when building the flow, but scanned by it when writing the test. Ugh.

How did I figure this out? You'd think by closely reading the ready-to-play state definition, but no... Instead, I amped up the logging for the webflow framework by setting this entry in log4j.properties:

log4j.logger.org.springframework.webflow=trace

This rewarded me with a great amount of useful detail:

2012-02-25 12:19:47,032 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImplFactory - Creating new execution of 'flow'
2012-02-25 12:19:47,047 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Resuming in org.springframework.webflow.test.MockExternalContext@1494cb8b
2012-02-25 12:19:47,054 [main] DEBUG org.springframework.webflow.engine.Flow - Restoring [FlowVariable@3209fa8f name = 'teamSetupForm', valueFactory = [BeanFactoryVariableValueFactory@2d20dbf3 type = TeamSetupForm]]
2012-02-25 12:19:47,072 [main] DEBUG org.springframework.webflow.engine.ViewState - Event 'continue' returned from view [MockViewFactoryCreator.MockView@3e0d1329 viewId = 'playQuizzo/register-team-members']
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
...
2012-02-25 12:19:47,084 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@3a4c5b4 on = continue, to = ready-to-play]
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'register-team-members'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.engine.ActionState - Entering state 'ready-to-play' of flow 'flow'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]; result = yes
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@47db9852 on = yes, to = play-round]
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'ready-to-play'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.ViewState - Entering state 'play-round' of flow 'flow'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,089 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Assigned key 1
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.ViewState - Rendering + [MockViewFactoryCreator.MockView@643cb075 viewId = 'playQuizzo/play-round']
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Flash scope = map[[empty]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Messages = [DefaultMessageContext@4c6504bc sourceMessages = map[[null] -> list[[empty]]]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'
2012-02-25 12:19:47,093 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'

If you spend time reading this, you'll see two flow state executions - one for the view state, and one for the action state. Our stub class returns "yes" from our transition in the poller state, and so we move to the next view state, play-round

Wrap-up

Testing in WebFlow is rather difficult, because of the number of mock objects, the complexity of flow logic, and the fact that it's in XML, which is not compiled and therefore really requires tests. But is is worth it, as long as you're not using this as an excuse to test your beans themselves. You should test those either in unit or Spring integration tests. My assertions are mostly around making sure we submitted the right events to trigger the right view states.

Next I'll be wiring up some pages to these flow view states, and starting to really get the processing going. I'm also going to start writing an admin page to control the game itself and provide an audience view.

Friday
Feb242012

TGIF - Ok, last Friday Post - Spring Data JPA Makes it Sweeter

Now that we have our service and repository layers, let's not give people too much rope to hang themselves. Let's get rid of the Active Record model.

To do this, change the annotation from @RooJpaActiveRecord to @RooJpaEntity:

@RooJavaBean
@RooToString
@RooJpaEntity
@RooSerializable
public class Team {
...
}

Now, watch Roo re-factor the configuration once we re-run the shell:

(create-repositories-and-services)⚡ [130] % roo                                                                                                  ~/git-repositories/quizzo
    ____  ____  ____  
   / __ \/ __ \/ __ \ 
  / /_/ / / / / / / / 
 / _, _/ /_/ / /_/ /  
/_/ |_|\____/\____/    1.2.1.RELEASE [rev 6eae723]


Welcome to Spring Roo. For assistance press TAB or type "hint" then hit ENTER.
Deleted SRC_MAIN_JAVA/com/chariot/games/quizzo/model/Team_Roo_Configurable.aj - 
  not required for governor com.chariot.games.quizzo.model.Team
Deleted SRC_MAIN_JAVA/com/chariot/games/quizzo/model/Team_Roo_Jpa_ActiveRecord.aj - 
  not required for governor com.chariot.games.quizzo.model.Team
roo>

Pretty nifty, right? Just make sure to fix-up any classes that needed Active Record. I had two of them:

  • One place where I persisted the team and team members in the webflow service.
  • Another place in the state machine where I use Team's static entityManager() method to access the Jpa Entity Manager.

Two easy fixes.

First, removing the ActiveRecord code from the webflow method:

  // inject the service:
  @Autowired
  private TeamService teamService;
  
  ...
  
  // change from
  team.persist();
  
  // to
  teamService.saveTeam(team);

Now we can actually test our webflow and mock the service layer. YAY!

Single Responsibility Principal...

This lump of code is like an extension cord between the browser and the database... Let's refactor it and provide it in a TeamService method, and inject that service call into our state machine.

The original code:


@Override
public Map<String, BigDecimal> getScores() {
  Map<String, BigDecimal> scores = new HashMap<String, BigDecimal>();

  Query query = Team.entityManager().createQuery("select t from team " +
      "t where t.quizRun.id = :id", Team.class).setParameter("id", quizRun.getId());
  @SuppressWarnings("unchecked")
  List<Team> teams = query.getResultList();
  Iterator<Team> itTeams = teams.iterator();
  while (itTeams.hasNext()) {
    Team team = itTeams.next();
    scores.put(team.getName(), team.calculateTotalScore());
  }
  return scores;
}

First, create a Spring Data JPA Repository interface method to handle what was our entity manager query. Add the highlighted lines to the Repository interface.

@RooJpaRepository(domainType = Team.class)
public interface TeamRepository {

  @Query("select t from Team t where t.quizRun.id = ?1")
  public List<Team> teamsByQuizRun(Long quizRunId);
}

Yep, Spring Data JPA is super cool. Check with our own Gordon Dickens - he has two blog posts you'd like to read if you want to know more about it.

Now, on to writing the new service method. We'll make it transactional and also read only:

// TeamService method:
public Map<String, BigDecimal> calcScoresByQuizRun(Long quizRunId);

// TeamServiceBean implementation:
@Transactional(readOnly = true)
public Map<String, BigDecimal> calcScoresByQuizRun(Long quizRunId) {

  Map<String, BigDecimal> scores = new HashMap<String, BigDecimal>();
  List<Team> teams = teamRepository.teamsByQuizRun(quizRunId);
  Iterator<Team> itTeams = teams.iterator();
  while (itTeams.hasNext()) {
    Team team = itTeams.next();
    scores.put(team.getName(), team.calculateTotalScore());
  }
  return scores;
}

AND... Our call from the QuizRunStateMachineInMemory bean, which keeps track of state and lets us access the scores:

@Override
public Map<String, BigDecimal> getScores() {
  return teamService.calcScoresByQuizRun(quizRun.getId());
}
  • Note: even better- you can do this with one bit of logic - create a GROUP BY query in the Spring Data JPA method, and eliminate the middle tier. Adding to my refactoring list (broken windows!).

Now we've focused on responsibilities dealt with across this refactor:

  • QuizRunStateMachineInMemory - delivers information such as scores to web flows
  • TeamService - focuses on providing a calculation of scores, regardless of what datasource they use
  • TeamRepository - fetches teams from a JPA-backed data source

There. Now we've separated concerns, modularized our code a bit, and made it all much better to deal with.

Why not all in one heap?

Well, every framework looks great at first. You can just "get things done, quickly." The problem is, as you add developers, logic, fixes, complexity and the rest, it becomes a massive heap of code, with no visible organizing principles.

Final word (for this blog post)

Hey, let's flatten a Roo ActiveRecord finder into three lines!

// from roo's generated finder:

privileged aspect Answer_Roo_Finder {
    
    public static TypedQuery<Answer> Answer.findAnswersByTeamAndQuestion(Team team, Question question) {
        if (team == null) throw new IllegalArgumentException("The team argument is required");
        if (question == null) throw new IllegalArgumentException("The question argument is required");
        EntityManager em = Answer.entityManager();
        TypedQuery<Answer> q = em.createQuery("SELECT o FROM Answer AS o WHERE o.team = :team AND o.question = :question", Answer.class);
        q.setParameter("team", team);
        q.setParameter("question", question);
        return q;
    }
    
}

// to this:

@RooJpaRepository(domainType = Answer.class)
public interface AnswerRepository {
  @Query("SELECT a FROM Answer AS a WHERE a.team.id = ?1 AND a.question.id = ?2")
  public List<Answer> getAnswersByTeamIdAndQuestionId(Long teamId, Long questionId);
}

Ain't that sweet? Next post (unless I get derailed) on to testing the web flow methods.

Friday
Feb242012

Quizzo Friday - Goin' all layered on the darn thing

I'm currently working on a somewhat complex, ajax-based web flow. See commit 505fbe7989f62003a4188a4baab5b85cc31077fb for the additional methods and constructs. It's not working yet, so I figured I'd be a good little boy and write tests.

Now, I could be Mr. Smarty-pants and work with the models directly, and I've been doing so. However, here's where that breaks down - when I want to do some more involved application development, and want to try some web-flow tests using the WebFlow mock system, and need to mock the entity layer. That's just not so easy.

The challenge

Ok, take a look at what I'm up against here (click the image to see the fragment of the webflow, courtesy of IntelliJ's great WebFlow editor).
I have a section of the web flow that registers a new user. Two steps are currently involved, one with the team name, the second with a list of members (and this one is not yet working so don't bother reviewing it...)

I'll want to test the method being guarded by the exit, which is defined this way in the web flow:

<transition on="continue" to="ready-to-play">
  <evaluate expression="quizzoFlowManager.saveTeamData(flowRequestContext)"/>
</transition>

The method in ~.web.flow.QuizzoFlowManagerBean looks like this currently:

  @Override
  public Event saveTeamData(RequestContext flowRequestContext) throws FlowException {
    TeamSetupForm teamSetupForm = 
       (TeamSetupForm)flowRequestContext.getViewScope()
       .get("teamSetupForm");

    Team team = new Team();
    team.setName(teamSetupForm.getName());
    team.setMission(teamSetupForm.getMessage());

    Iterator<String> memberNamesIterator = 
      teamSetupForm.getTeamMemberNames().iterator();

    while (memberNamesIterator.hasNext()) {
      String teamMemberName = memberNamesIterator.next();
      TeamMember member = new TeamMember();
      member.setName(teamMemberName);
      team.getTeamMembers().add(member);
    }

    team.setQuizRun(stateMachine.getQuizRun());
    team.persist();
    flowRequestContext.getFlowScope().put("team", team);
    return success();
  }

This is web-flow cruft. It's kind of like controller code, and it's defined in a Spring Bean that lives as a web-layer module. There are several problems with this:

  • It's doing persistence in a web-tier object
  • I don't want to make a web-tier object @Transactional
  • If I'm writing tests, how do I mock the entity layer without a lot of headache?

In thinking of how to express a test against this logic, I'm struck with how complex the test needs to be. It's tough enough to set up the Web Flow layer with the appropriate testing. But to then have to do some magic (see this post on @MockStaticEntityMethods - it hurts us, Baggins!).

I consider a good test to be documentation of the use-case or exercising of the code in question. That's what it should be. I'm sensing coupling here to both the web layer AND the data layer. Not so good, in my feeling. Although it's nice to have model objects and throw around Active Record calls everywhere for prototyping, I think this is where it falls down, in testing without being able to mock a service or repository layer cleanly.

Going all Repository on it

Ok, so how do I set up a repository for each bean? Easy-peasy.

repository jpa --entity ~.model.Answer        --interface ~.db.AnswerRepository
repository jpa --entity ~.model.Choice        --interface ~.db.ChoiceRepository
repository jpa --entity ~.model.Question      --interface ~.db.QuestionRepository
repository jpa --entity ~.model.Quiz          --interface ~.db.QuizRepository
repository jpa --entity ~.model.QuizRun       --interface ~.db.QuizRunRepository
repository jpa --entity ~.model.Team          --interface ~.db.TeamRepository
repository jpa --entity ~.model.TeamMember    --interface ~.db.TeamMemberRepository

Forgive me if my format looks a little odd - I used TextMate (my favorite Mac editor in the whole wide world) to manipulate the text using column pasting.

Now, I have a repository layer. I can do the same thing to add a service layer, but it needs a little more gusto (line breaks are for readability, it doesn't fit on one line in the blog):

service --entity ~.model.Answer --interface ~.service.AnswerService 
         --class ~.service.AnswerServiceBean
service --entity ~.model.Choice        --interface ~.service.ChoiceService
        --class ~.service.ChoiceServiceBean
service --entity ~.model.Question      --interface ~.service.QuestionService
      --class ~.service.QuestionServiceBean
service --entity ~.model.Quiz          --interface ~.service.QuizService
          --class ~.service.QuizServiceBean
service --entity ~.model.QuizRun       --interface ~.service.QuizRunService
       --class ~.service.QuizRunServiceBean
service --entity ~.model.Team          --interface ~.service.TeamService
          --class ~.service.TeamServiceBean
service --entity ~.model.TeamMember    --interface ~.service.TeamMemberService
    --class ~.service.TeamMemberServiceBean

Now, I have a true repository-and-service layer for my system. My controllers and integration tests automatically convert to using the services and repositories once they exist:

@RequestMapping(method = RequestMethod.POST, produces = "text/html")
public String TeamController.create(...) {
  if (bindingResult.hasErrors()) {
    populateEditForm(uiModel, team);
    return "admin/teams/create";
  }
  
  uiModel.asMap().clear();
  teamService.saveTeam(team);
  return "redirect:/admin/teams/" ...
}

It is special, isn't it. Next up, we'll show you how to unit test the logic in our web flow method.

Thursday
Feb232012

Wednesday / Thursday Quizzo - flowin' it

Ok, most of my time in the last two days was non-quizzo related. Well, I should say non-business logic quizzo related. See, I was chasing my tail around the integration tests for the state of the quiz, which I've set up with a facade Spring Bean. Here's my interface:

public interface QuizRunStateMachine {
  void startQuiz(Long quizId, String text);
  boolean nextQuestion();
  Map<String, BigDecimal> getScores();
  Long getCurrentQuestionId();
  boolean submitAnswer(Team team, Answer answer);
  void endQuiz();
  QuizRun getQuizRun();
}

When I test the implementation of this state machine, which flips various bits in my database and keeps track of the current state, it works fine within IntelliJ (my IDE weapon of choice). However, when I use the maven test process, all tests fail due to the assertions coded within the bean. Grr.

I have no answer for that...

See above.

Anyhoo... Web flow, anyone?

Roo makes setting up the web flow engine easy. You just issue this simple command:

roo> web flow setup --flowName playQuizzo

The command sets up Web Flow, putting the flow itself in the WEB-INF/views/playQuizzo directory.

This blog entry lays out the beginning of using this strategy to implement a quiz player interface using web flow. It's incoherent a bit, as I'm sort of live-blogging this, so at some point I may go back and clean it up. But for now, hopefully it will serve as a helpful guide.

The flow

Here is a simple web flow (so far) that I've put together to handle the quiz flow:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/webflow     
   http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

  <var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" 
        name="teamSetupForm"/>

  <view-state id="register-team" 
            view="playQuizzo/register-team"
           model="flowScope.teamSetupForm">
    <transition on="continue" to="register-team-members" 
       bind="true" validate="true">
      <evaluate expression="quizzoFlowManager.debug(flowRequestContext)"/>
    </transition>
  </view-state>

  <view-state id="register-team-members" 
            view="playQuizzo/register-team-members"
           model="flowScope.teamSetupForm">
    <transition on="continue" to="ready-to-play"/>
    <transition on="add-team-member"/>
    <transition on="remove-team-member"/>
    <transition on="back" to="register-team"/>
  </view-state>

  <view-state id="ready-to-play" view="playQuizzo/ready-to-play">   
    <transition on="go" to="play-round">
      
    </transition>
  </view-state>

  <view-state id="play-round" view="playQuizzo/play-round">    
    <transition on="submit-answer" to="play-round"/>
    <transition on="finish" to="game-over"/>
  </view-state>

  <end-state id="game-over" view="playQuizzo/game-over"/>

</flow>

This web flow is relatively simple- it requires the users to register their teams and team members, then keeps presenting the current question from the quiz run state machine, and as the submissions progress, the users keep submitting answers for each question, etc...

I haven't worked all of this out yet, and as I'm writing this blog post I'm about to commit at least a lame beginning to the scripts, but you get the idea.

Building our form views

Ok, so I have a love-hate relationship with the web tier of Roo. For simple cases, if you don't go off the rails (couldn't resist), it works fine. However, if you want to free-style it, maybe such as set up non-RESTful views such as web flows, it breaks down because of the implicit conventions (specifically the form has to be RESTful in some way, includes a primary key, optional version, and most importantly, those dang Submit buttons.

I'm using Web Flow for the quiz takers. It makes sense to me (so far) that the quiz manager will control the flow using a specific set of protected pages only he can get to, which interact with the manager methods of my state machine (starting a quiz, advancing to the next question, etc).

However, the users will be walking through a directed web flow. The flow will have various actions, and they will be whatever I want them to be - bottom line, I have to break with Roo conventions. So I'm setting up my own Roo tags for a generic form, button grouping, and buttons.

The Form

I will update this as I go along, but here is my current generic "form" tag, which I place alongside form:create and others as form:form:

<jsp:root xmlns:c="http://java.sun.com/jsp/jstl/core" 
          xmlns:fn="http://java.sun.com/jsp/jstl/functions"
          xmlns:util="urn:jsptagdir:/WEB-INF/tags/util" 
          xmlns:form="http://www.springframework.org/tags/form"
          xmlns:jsp="http://java.sun.com/JSP/Page" 
          xmlns:spring="http://www.springframework.org/tags" version="2.0">

    <jsp:output omit-xml-declaration="yes"/>

    

    <c:set var="enctype" value="application/x-www-form-urlencoded"/>

    <c:if test="${multipart}">
        <c:set var="enctype" value="multipart/form-data"/>
    </c:if>

    <c:if test="${empty action}">
        <spring:url value="${path}" var="action"/>
    </c:if>

    <c:if test="${empty path}">
        <spring:url value="/playQuizzo" var="${path}"/>
    </c:if>

    <c:if test="${empty label}">
        <spring:message var="label" code="${labelCode}"/>
    </c:if>

    <util:panel id="${id}" title="${label}" openPane="${openPane}">
        <form:form method="POST" action="${path}"
                   modelAttribute="${modelAttribute}"
                   enctype="${enctype}">
            <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
            <jsp:doBody/>
        </form:form>

    </util:panel>
</jsp:root>

I have the when and otherwise in there to handle the case where we're submitting back to ourselves (web flow) and otherwise, if a path was given, submit to that path.

Anyhoo... the button group

Ok, so I also made a buttons tag to group buttons together, because on the Roo forms the singluar button was wrapped in a styled div:

<jsp:root ...>
  <jsp:output omit-xml-declaration="yes"/>
  <div class="submit" id="form_submit">
      <jsp:doBody />
  </div>
</jsp:root>

Really just a wrapper div, you know...

The button tag

Then, I put together a button tag which configures buttons for the form. You can add as many as you want. This is in tags/form/field/button.tagx:

<jsp:root 
   xmlns:c="http://java.sun.com/jsp/jstl/core" 
   xmlns:fn="http://java.sun.com/jsp/jstl/functions"
   xmlns:spring="http://www.springframework.org/tags" 
   xmlns:form="http://www.springframework.org/tags/form"
   xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0">
    <jsp:output omit-xml-declaration="yes"/>

   

    <c:if test="${empty text}">
        <spring:message code="label_${fn:toLowerCase(fn:substringAfter(id,'_'))}" var="label" htmlEscape="false"/>
    </c:if>

    <c:set var="sec_field">
        <spring:escapeBody javaScriptEscape="true">${field}</spring:escapeBody>
    </c:set>

    <c:if test="${empty validate or validate}">
        
    </c:if>

    <input id="_${sec_id}_id" type="submit" value="${text}" name="${value}"/>
</jsp:root>

The gist of this tag is to create the same validator declaration on the buttons (as long as they need validation) as before. Note that I don't turn my buttons into Dojo buttons. Neither does the Roo team. I guess they felt that the standard submit buttons were OK. My goal here is to make the library conform, so any stylesheet manipulation I do will work equally well for scaffolded forms as well as my new ones.

Using the forms library, or where I got cranky

Ok, you see now my goal was to make life easy for web flow and non-scaffolded forms developers. Also I wanted to extend what we already had - not dip into the Spring MVC form tag libraries for fields, but use the Roo ones.

And that's where the fun stopped for a bit.

Label conventions

Let me lay this out, as I've tried to explain in Roo in Action but have more space here. The conventions that I am aware of for forms, fields, and other things are not well understood even by me. For exmample, if I plop a <fields:input%gt; tag down, and want to set the label, I have to remember to do this:

  • Set the label up in messages.properties
  • Prefix the message id itself in the properties file with label_
  • Use the label without the label_ prefix in the JSPX page
  • Use the labelCode attribute rather than label
  • Most cryptically: use _field_id as the id of the form fields, as opposed to the field id, as the tag will write an HTML tag with the un-underscored field name and id, and a Dojo dijit for the field with the underscored name. Otherwise, your field will not be submitted

I logged a few JIRA issues as suggestions to implement this tag set and also to make overall non-scaffolded ui more kind to the developer. We'll see how this evolves over time.

Using the tag library:

Ok, so that means I have something like this in my JSPX file:

  <form:form
          id="f_quizzo_flow_register_team"
          labelCode="webflow_playQuizzo_register_team_title"
          modelAttribute="teamSetupForm">
      <fields:input id="_name" field="name"
              labelCode="webflow_playquizzo_register_team_name_field_label"
              required="true" min="1" max="80" />
      <fields:buttons>
          <fields:button id="continue" text="Continue" 
                      value="_eventId_continue"/>
      </fields:buttons>
  </form:form>

and this in my properties file:

label_webflow_playquizzo_register_team_name_field_label=Team Name

That took me a bit of time to unwind as I was writing my form, but in the end, it made it more concise. And I forced myself to locallize. Well, almost completely.

Next up, I complete the web flow and get the state machine going.

More later...

Tuesday
Feb212012

Monday/Tuesday Quizzo - It's a mess w/a silver lining?

What did I do over the last two days, beyond deal with proposals, our training website, and a number of other issues? Oh, yeah, right... I DID say I'd be working on the Quizzo too...

I did a few interesting things over the last few days. First off, let's talk about adding Maven reports.

The Maven Site Plugin

Ok, you're looking at Roo, and you realize it uses Maven. Ugh, you say (assuming you've not ever worked with Maven in the enterprise). No, not ugh. Awesome. Actually, as unexciting as the language of Maven is (XML), it can do a ton of work for you automatically.

We're going to configure a plugin called the maven site plugin. This plugin gives you reports on your project, such as code coverage, JavaDoc, and much more. To set it up, we'll first have to edit our pom.xml file, and add the following plugin definition BEFORE the end of the section:


<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-site-plugin</artifactId>
    <version>3.0</version>
    <configuration>
        <reportPlugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
                <version>2.2</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>findbugs-maven-plugin</artifactId>
                <version>2.4.0</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>cobertura-maven-plugin</artifactId>
                <version>2.5.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jxr-plugin</artifactId>
                <version>2.3</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-report-plugin</artifactId>
                <version>2.11</version>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>failsafe-report-only</report>
                            <report>report-only</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
        </reportPlugins>
    </configuration>
    <executions>
        <execution>
            <id>attach-descriptor</id>
            <goals>
                <goal>attach-descriptor</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The reports being run via this setup include:

  • The Maven JavaDoc report
  • The standard project info reports (referenced in the report descriptor below)
  • The Cobertura code coverage engine. Shows teams how much code they are testing.
  • The Java Cross Reference report (anywhere source is listed, it will write hyperlinks and allow you to browse into the code.
  • Findbugs, a very interesting code analysis engine
  • Maven surefire report - which shows an overview of all JUnit tests executed by the run

Once we've set this up, we're all set to add a site descriptor to the project and deploy the site using the mvn site command. First, we create a src/site/site.xml descriptor, and load it with this information:


<?xml version="1.0" encoding="ISO-8859-1"?>

<project name="quizzo" xmlns="http://maven.apache.org/DECORATION/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0    
       http://maven.apache.org/xsd/decoration-1.0.0.xsd">
  <publishDate position="right" />
  <version position="right" />  
  <body>
    <menu ref="reports" inherit="top" />
  </body>
</project>

To generate our report site, we issue the mvn site command. This should download 1/2 of the internet the first time (lots of reporting engine APIs are being downloaded). Once finished, the site report will live in target/site and can be deployed via various mechanisms.

Browsing the Site Report

I've put a copy of my site report in a zip file for review. It's just a snapshot, but it gives you an idea of what you can expect.

Coming up tomorrow AM I'll highlight a few more changes we've made in the master trunk of our github repository. The Roo in Action final edits beckon me...

'Nite all.