TicketMonster Tutorial

Building The Statistics Dashboard Using GWT And Errai

What Will You Learn Here?

You’ve just built the administration view, and would like to collect real-time information about ticket sales and attendance. Now it would be good to implement a dashboard that can collect data and receive real-time updates. After reading this tutorial, you will understand our dashboard design and the choices that we made in its implementation. Topics covered include:

  • Adding GWT to your application

  • Setting up CDI server-client eventing using Errai

  • Testing GWT applications

The tutorial will show you how to perform all these steps in JBoss Developer Studio, including screenshots that guide you through. For those of you who prefer to watch and learn, the included video shows you how we performed all the steps.

In this tutorial, we will create a booking monitor using Errai and GWT, and add it to the TicketMonster application. It will show live updates on the booking status of all performances and shows. These live updates are powered by CDI events crossing the client-server boundary, a feature provided by the Errai Framework.

Before we start

Let us quickly review the starting point of this chapter. If you are re-creating TicketMonster as part of reading this tutorial, this is a good time to check that all the prerequisites are in place. If you are not re-creating TicketMonster on your own, then you can skip this section.

Before everything, make sure that you have read and created the code described in chapter [BuildingBusinessServices].

Afterwards, make sure that Errai is properly configured in the application.

First, we check if pom.xml contains a reference to the Bill Of Materials (BOM) that describes the correct version for the Errai artifacts. Make sure that you have the following in the dependencyManagement section:

pom.xml
<project ...>
  ...
  <dependencyManagement>
    <dependencies>
      ...
      <dependency>
         <groupId>org.jboss.bom</groupId>
         <artifactId>jboss-javaee-6.0-with-errai</artifactId>
         <version>${jboss.bom.version}</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

Next, we check if the GWT and Errai artifacts are included in the project.

pom.xml
<project ...>
  ...
  <dependencies>

    <!-- The next set of dependencies are for Errai, which we use for
           the TicketMonster booking monitor -->
   <dependency>
       <groupId>org.jboss.errai</groupId>
       <artifactId>errai-bus</artifactId>
       <exclusions>
           <exclusion>
               <groupId>javax.inject</groupId>
               <artifactId>javax.inject</artifactId>
           </exclusion>
           <exclusion>
               <groupId>javax.annotation</groupId>
               <artifactId>jsr250-api</artifactId>
           </exclusion>
       </exclusions>
   </dependency>
   <dependency>
       <groupId>org.jboss.errai</groupId>
       <artifactId>errai-ioc</artifactId>
       <exclusions>
           <exclusion>
               <groupId>javax.inject</groupId>
               <artifactId>javax.inject</artifactId>
           </exclusion>
           <exclusion>
               <groupId>javax.annotation</groupId>
               <artifactId>jsr250-api</artifactId>
           </exclusion>
       </exclusions>
   </dependency>
   <dependency>
       <groupId>org.jboss.errai</groupId>
       <artifactId>errai-tools</artifactId>
   </dependency>
   <dependency>
       <groupId>org.mvel</groupId>
       <artifactId>mvel2</artifactId>
   </dependency>

   <!-- CDI/ Errai Integration Modules -->
   <dependency>
       <groupId>org.jboss.errai</groupId>
       <artifactId>errai-cdi-client</artifactId>
   </dependency>

   <dependency>
       <groupId>org.jboss.errai</groupId>
       <artifactId>errai-javax-enterprise</artifactId>
       <scope>provided</scope>
   </dependency>

   <dependency>
    <groupId>org.jboss.errai</groupId>
    <artifactId>errai-weld-integration</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.jboss.weld.servlet</groupId>
        <artifactId>weld-servlet</artifactId>
      </exclusion>
    </exclusions>
  </dependency>

  <dependency>
    <groupId>com.google.gwt</groupId>
    <artifactId>gwt-user</artifactId>
    <scope>provided</scope>
  </dependency>

    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-dev</artifactId>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  ...
</project>

Make sure that the appropriate Maven plugins are configured too, and your build configuration contains the following:

pom.xml
<build>
    <!-- Maven will append the version to the finalName (which is the
   name given to the generated war, and hence the context root) -->
    <finalName>${project.artifactId}</finalName>
    <pluginManagement>

        <plugins>
            <!-- Compiler plugin enforces Java 1.6 compatibility and activates
          annotation processors -->
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.1.1</version>
                <configuration>
                    <!-- We must exclude GWT client local classes from the
               deployment, or classpath scanners such as Hibernate and Weld get confused
               when the webapp is bootstrapping. -->
                    <packagingExcludes>**/javax/**/*.*,**/client/local/**/*.class</packagingExcludes>
                    <archive>
                        <manifestEntries>
                            <Dependencies>org.jboss.as.naming,org.jboss.as.server,org.jboss.msc</Dependencies>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

            <!-- The JBoss AS plugin deploys your war to a local JBoss AS container -->
            <!-- To use run: mvn package jboss-as:deploy -->
            <plugin>
                <groupId>org.jboss.as.plugins</groupId>
                <artifactId>jboss-as-maven-plugin</artifactId>
                <version>7.1.1.Final</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>2.4.1</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>.errai</directory>
                            <includes>
                                <include>**</include>
                            </includes>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
            <!-- m2e (Maven integration for Eclipse) requires the following
                configuration -->
            <plugin>
                <groupId>org.eclipse.m2e</groupId>
                <artifactId>lifecycle-mapping</artifactId>
                <version>1.0.0</version>
                <configuration>
                    <lifecycleMappingMetadata>
                        <pluginExecutions>
                            <pluginExecution>
                                <pluginExecutionFilter>
                                    <groupId>org.codehaus.mojo</groupId>
                                    <artifactId>gwt-maven-plugin</artifactId>
                                    <versionRange>[2.3.0,)</versionRange>
                                    <goals>
                                        <goal>resources</goal>
                                    </goals>
                                </pluginExecutionFilter>
                                <action>
                                    <execute/>
                                </action>
                            </pluginExecution>
                        </pluginExecutions>
                    </lifecycleMappingMetadata>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
    <plugins>
        <!-- GWT plugin to compile client-side java code to javascript
        and to run GWT development mode -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>gwt-maven-plugin</artifactId>
            <version>2.4.0</version>
            <configuration>
                <inplace>true</inplace>
                <logLevel>INFO</logLevel>
                <extraJvmArgs>-Xmx512m</extraJvmArgs>
                <draftCompile>true</draftCompile>
                <!-- Configure GWT's development mode (formerly known
                as hosted mode) to not start the default server (embedded jetty), but to
                download the HTML host page from the configured runTarget. -->
                <noServer>true</noServer>
                <runTarget>http://localhost:8080/ticket-monster/booking-monitor.html</runTarget>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>resources</goal>
                        <goal>compile</goal>
                    </goals>
                </execution>
                <execution>
                    <id>gwt-clean</id>
                    <phase>clean</phase>
                    <goals>
                        <goal>clean</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

If one or more of the above is not true, please make the appropriate changes.

Module definition

The first step is to add a GWT module descriptor (a .gwt.xml file) which defines the GWT module, its dependencies and configures the client source paths. Only classes in these source paths will be compiled to JavaScript by the GWT compiler. Here’s the BookingMonitor.gwt.xml file:

src/main/resources/org/jboss/jdf/example/ticketmonster/BookingMonitor.gwt.xml
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6//EN"
        "http://google-web-toolkit.googlecode.com/svn/releases/1.6/distro-source/core/src/gwt-module.dtd">

<!--
   This file declares the Errai/GWT module for the TicketMonster booking monitor,
   which shares the model classes with the user-facing part of the app, but defines
   its own user interface for TicketMonster administrators.
-->

<module rename-to="BookingMonitor">
    <inherits name="org.jboss.errai.common.ErraiCommon"/>
    <inherits name="org.jboss.errai.bus.ErraiBus"/>
    <inherits name="org.jboss.errai.ioc.Container"/>
    <inherits name="org.jboss.errai.enterprise.CDI"/>

    <!-- Model classes that are shared with the rest of the application -->
    <source path="model"/>

    <!-- Classes that are specific to 'booking monitor' features; not shared with rest of app -->
    <source path="monitor"/>

    <!-- Limit the supported browsers for the sake of this demo -->
    <set-property name="user.agent" value="ie8,ie9,safari,gecko1_8"/>
</module>

The rename-to attribute specifies the output directory and file name of the resulting JavaScript file. In this case we specified that the BookingMonitor module will be compiled into BookingMonitor/BookingMonitor.nocache.js in the project’s output directory. The module further inherits the required Errai modules, and specifies the already existing model package as source path, as well as a new package named monitor, which will contain all the client source code specific to the booking monitor.

Host page

In the next step we add a host HTML page which includes the generated JavaScript and all required CSS files for the booking monitor. It further specifies a <div> element with id content which will be used as a container for the booking monitor’s user interface.

src/main/webapp/booking-monitor.html
<!DOCTYPE html>
<html>
<head>
    <title>Ticket Monster Administration</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <link type="text/css" rel="stylesheet" href="resources/css/screen.css"/>
    <link rel="stylesheet" href="resources/css/bootstrap.css" type="text/css" media="all"/>
    <link rel="stylesheet" href="resources/css/custom.css" type="text/css" media="all">

    <link href='http://fonts.googleapis.com/css?family=Rokkitt' rel='stylesheet' type='text/css'>

    <script type="text/javascript" src="BookingMonitor/BookingMonitor.nocache.js"></script>
</head>

<body>
    <div id="logo"><div class="wrap"><h1>Ticket Monster</h1></div></div>

    <div id="container">
        <div id="menu">
            <div class="navbar">
                <div class="navbar-inner">
                    <div class="container">
                        <ul class="nav">
                            <li><a href="index.html#about">About</a></li>
                            <li><a href="index.html#events">Events</a></li>
                            <li><a href="index.html#venues">Venues</a></li>
                            <li><a href="index.html#bookings">Bookings</a></li>
                            <li><a href="#">Monitor</a></li>
                            <li><a href="admin">Administration</a></li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>


        <div class="container-fluid">

            <div class="row">
                <div class="span7">

                <h3 class="page-header light-font special-title">Booking status</h3>
                <div id="content">

                </div>
              </div>
            <div class="span5">
                <h3 class="page-header light-font special-title">Bot</h3>
                <div id="bot-content"></div>
             </div>

            </div>
        </div>
    </div>

    <footer style="">
        <div style="text-align: center;"><img src="resources/img/dualbrand_as7eap.png" alt="HTML5"/></div>
    </footer>
</body>
</html>

Enabling Errai

For enabling Errai in our application we will add an ErraiApp.properties marker file. When it is detected inside a JAR or at the top of any classpath, the subdirectories are scanned for deployable components. As such, all Errai application modules in a project must contain an ErraiApp.properties at the root of all classpaths that you wish to be scanned, in this case src/main/resources.

We will also add an ErraiService.properties file, which contains basic configuration for the bus itself. Unlike ErraiApp.properties, there should be at most one ErraiService.properties file on the classpath of a deployed application.

src/main/resources/ErraiService.properties
#
# Request dispatcher implementation (default is SimpleDispatcher)
#
errai.dispatcher_implementation=org.jboss.errai.bus.server.SimpleDispatcher

Preparing the wire objects

One of the strengths of Errai is the ability to use domain objects for communication across the wire. In order for that to be possible, all model classes that are transferred using Errai RPC or Errai CDI need to be annotated with the Errai-specific annotation @Portable. We will begin by annotating the Booking class which used as an the event payload.

src/main/java/org/jboss/jdf/example/ticketmonster/model/Booking.java
...
import org.jboss.errai.common.client.api.annotations.Portable;
...
@Portable
public class Booking implements Serializable {
...
}

You should do the same for the other model classes.

The EntryPoint

We are set up now and ready to start coding. The first class we need is the EntryPoint into the GWT application. Using Errai, all it takes is to create a POJO and annotate it with @EntryPoint.

src/main/java/org/jboss/jdf/example/ticketmonster/monitor/client/local/BookingMonitor.java
package org.jboss.jdf.example.ticketmonster.monitor.client.local;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.enterprise.event.Observes;
import javax.inject.Inject;

import org.jboss.errai.bus.client.api.RemoteCallback;
import org.jboss.errai.ioc.client.api.AfterInitialization;
import org.jboss.errai.ioc.client.api.Caller;
import org.jboss.errai.ioc.client.api.EntryPoint;
import org.jboss.jdf.example.ticketmonster.monitor.client.shared.BookingMonitorService;
import org.jboss.jdf.example.ticketmonster.monitor.client.shared.qualifier.Cancelled;
import org.jboss.jdf.example.ticketmonster.monitor.client.shared.qualifier.Created;
import org.jboss.jdf.example.ticketmonster.model.Booking;
import org.jboss.jdf.example.ticketmonster.model.Performance;
import org.jboss.jdf.example.ticketmonster.model.Show;

import com.google.gwt.user.client.ui.RootPanel;

/**
 * The entry point into the TicketMonster booking monitor.
 *
 * The {@code @EntryPoint} annotation indicates to the Errai framework that
 * this class should be instantiated inside the web browser when the web page
 * is first loaded.
 */
@EntryPoint
public class BookingMonitor {
    /**
     * This map caches the number of sold tickets for each {@link Performance} using
     * the performance id as key.
     */
    private static Map<Long, Long> occupiedCounts;

    /**
     * This is the client-side proxy to the {@link BookingMonitorService}.
     * The proxy is generated at build time, and injected into this field when the page loads.
     */
    @Inject
    private Caller<BookingMonitorService> monitorService;

    /**
     * We store references to {@link ShowStatusWidget}s in this map, so we can update
     * these widgets when {@link Booking}s are received for the corresponding {@link Show}.
     */
    private Map<Show, ShowStatusWidget> shows = new HashMap<Show, ShowStatusWidget>();

    /**
     * This method constructs the UI.
     *
     * Methods annotated with Errai's {@link AfterInitialization} are only called once
     * everything is up and running, including the communication channel to the server.
     */
    @AfterInitialization
    public void createAndShowUI() {
        // Retrieve the number of sold tickets for each performance.
        monitorService.call(new RemoteCallback<Map<Long, Long>>() {
            @Override
            public void callback(Map<Long, Long> occupiedCounts) {
                BookingMonitor.occupiedCounts = occupiedCounts;
                listShows();
            }
        }).retrieveOccupiedCounts();
    }

    private void listShows() {
        // Retrieve all shows
        monitorService.call(new RemoteCallback<List<Show>>() {
            @Override
            public void callback(List<Show> shows) {
                // Sort based on event name
                Collections.sort(shows, new Comparator<Show>() {
                    @Override
                    public int compare(Show s0, Show s1) {
                        return s0.getEvent().getName().compareTo(s1.getEvent().getName());
                    }
                });

                // Create a show status widget for each show
                for (Show show : shows) {
                    ShowStatusWidget sw = new ShowStatusWidget(show);
                    BookingMonitor.this.shows.put(show, sw);
                    RootPanel.get("content").add(sw);
                }
            }
        }).retrieveShows();
    }

}

As soon as Errai has completed its initialization process, the Booking Monitor#createAndShowUI() method is invoked (@AfterInitialization tells Errai to call it). In this case the method will fetch initial data from the server using Errai RPC and construct the user interface. To carry out the remote procedure call, we use an injected Caller for the remote interface BookingMonitorService which is part of the org.jboss.jdf.example.ticketmonster.monitor.client.shared package and whose implementation BookingMonitorServiceImpl has been explained in the previous chapter.

In order for the booking status to be updated in real-time, the class must be notified when a change has occurred. If you have built the service layer already, you may remember that the JAX-RS BookingService class will fire CDI events whenever a booking has been created or cancelled. Now we need to listen to those events.

src/main/java/org/jboss/jdf/example/ticketmonster/monitor/client/local/BookingMonitor.java
public class BookingMonitor {

        /**
     * Responds to the CDI event that's fired on the server when a {@link Booking} is created.
     *
     * @param booking  the create booking
     */
    public void onNewBooking(@Observes @Created Booking booking) {
        updateBooking(booking, false);
    }

    /**
     * Responds to the CDI event that's fired on the server when a {@link Booking} is cancelled.
     *
     * @param booking  the cancelled booking
     */
    public void onCancelledBooking(@Observes @Cancelled Booking booking) {
        updateBooking(booking, true);
    }

    // update the UI widget to reflect the new or cancelled booking
    private void updateBooking(Booking booking, boolean cancellation) {
        ShowStatusWidget sw = shows.get(booking.getPerformance().getShow());
        if (sw != null) {
            long count = getOccupiedCountForPerformance(booking.getPerformance());
            count += (cancellation) ? -booking.getTickets().size() : booking.getTickets().size();

            occupiedCounts.put(booking.getPerformance().getId(), count);
            sw.updatePerformance(booking.getPerformance());
        }
    }

    /**
     * Retrieve the sold ticket count for the given {@link Performance}.
     *
     * @param p  the performance
     * @return number of sold tickets.
     */
    public static long getOccupiedCountForPerformance(Performance p) {
        Long count = occupiedCounts.get(p.getId());
        return (count == null) ? 0 : count.intValue();
    }

}

The newly created methods onNewBooking and onCancelledBooking are event listeners. They are identified as such by the @Observes annotation applied to their parameters. By using the @Created and @Cancelled qualifiers that we have defined in our application, we narrow down the range of events that they listen.

The widgets

Next, we will define the widget classes that are responsible for rendering the user interface. First, we will create the widget class for an individual performance.

src/main/java/org/jboss/jdf/example/ticketmonster/monitor/client/local/PerformanceStatusWidget.java
package org.jboss.jdf.example.ticketmonster.monitor.client.local;

import org.jboss.jdf.example.ticketmonster.model.Performance;

import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;

/**
 * A UI component to display the status of a {@link Performance}.
 */
public class PerformanceStatusWidget extends Composite {

    private Label bookingStatusLabel = new Label();

    private HorizontalPanel progressBar = new HorizontalPanel();
    private Label soldPercentLabel;
    private Label availablePercentLabel;

    private Performance performance;
    private long soldTickets;
    private int capacity;

    public PerformanceStatusWidget(Performance performance) {
        this.performance = performance;

        soldTickets = BookingMonitor.getOccupiedCountForPerformance(performance);
        capacity = performance.getShow().getVenue().getCapacity();

        setBookingStatus();
        setProgress();

        HorizontalPanel performancePanel = new HorizontalPanel();
        String date = DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_SHORT).format(performance.getDate());
        performancePanel.add(new Label(date));
        performancePanel.add(progressBar);
        performancePanel.add(bookingStatusLabel);
        performancePanel.setStyleName("performance-status");
        initWidget(performancePanel);
    }

    /**
     * Updates the booking status (progress bar and corresponding text) of the {@link Performance}
     * associated with this widget based on the number of sold tickets cached in {@link BookingMonitor}.
     */
    public void updateBookingStatus() {
        this.soldTickets = BookingMonitor.getOccupiedCountForPerformance(performance);
        setBookingStatus();
        setProgress();
    }

    private void setBookingStatus() {
        bookingStatusLabel.setText(soldTickets + " of " + capacity + " tickets booked");
    }

    private void setProgress() {
        int soldPercent = Math.round((soldTickets / (float) capacity) * 100);

        if (soldPercentLabel != null) {
            progressBar.remove(soldPercentLabel);
        }

        if (availablePercentLabel != null) {
            progressBar.remove(availablePercentLabel);
        }

        soldPercentLabel = new Label();
        soldPercentLabel.setStyleName("performance-status-progress-sold");
        soldPercentLabel.setWidth(soldPercent + "px");

        availablePercentLabel = new Label();
        availablePercentLabel.setStyleName("performance-status-progress-available");
        availablePercentLabel.setWidth((100 - soldPercent) + "px");

        progressBar.add(soldPercentLabel);
        progressBar.add(availablePercentLabel);
    }
}

A show has multiple performances, so we will create a ShowStatusWidget to contains a PerformanceStatusWidget for each performance.

src/main/java/org/jboss/jdf/example/ticketmonster/monitor/client/local/ShowStatusWidget.java
package org.jboss.jdf.example.ticketmonster.monitor.client.local;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.jboss.jdf.example.ticketmonster.model.Performance;
import org.jboss.jdf.example.ticketmonster.model.Show;

import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * A UI component to display the status of a {@link Show}.
 */
public class ShowStatusWidget extends Composite {

    private Map<Long, PerformanceStatusWidget> performances = new HashMap<Long, PerformanceStatusWidget>();

    public ShowStatusWidget(Show show) {
        VerticalPanel widgetPanel = new VerticalPanel();
        widgetPanel.setStyleName("show-status");

        Label showStatusHeader = new Label(show.getEvent().getName() + " @ " + show.getVenue());
        showStatusHeader.setStyleName("show-status-header");
        widgetPanel.add(showStatusHeader);

        // Add a performance status widget for each performance of the show
        for (Performance performance : show.getPerformances()) {
            if (performance.getDate().getTime() > new Date().getTime()) {
                PerformanceStatusWidget psw = new PerformanceStatusWidget(performance);
                performances.put(performance.getId(), psw);
                widgetPanel.add(psw);
            }
        }

        initWidget(widgetPanel);
    }

    /**
     * Triggers an update of the {@link PerformanceStatusWidget} associated with
     * the provided {@link Performance}.
     *
     * @param performance
     */
    public void updatePerformance(Performance performance) {
        PerformanceStatusWidget pw = performances.get(performance.getId());
        if (pw != null) {
            pw.updateBookingStatus();
        }
    }
}

This class is has two responsibilities. First, it will to display together all the performances that belong to a given show. Also, it will update its PerformanceStatusWidget children whenever a booking event is received on the client (through the observer method defined above).

Share the Knowledge

Find this guide useful?

Feedback

Find a bug in the guide? Something missing? You can fix it by [forking the repository](http://github.com/jboss-jdf/ticket-monster), making the correction and [sending a pull request](http://help.github.com/send-pull-requests). If you're just plain stuck, feel free to ask a question in the [user discussion forum](http://site-jdf.rhcloud.com/forums/jdf-users).

Recent Changelog

  • Jul 13, 2013: Forge-345 improved the styling for code listings Vineet Reynolds
  • Jun 27, 2013: Oops. changes to tax headers removed the authors metadata. correcting it Rafael Benevides
  • Jun 24, 2013: Switching from setex to tax headers completely Rafael Benevides
  • Jun 07, 2013: Fixed spelling and formatting issues Vineet Reynolds
  • May 20, 2013: Jdf-320 upgrade jquery to 1.9.1 and jquery mobile to 1.3.1 Vineet Reynolds
  • Oct 24, 2012: Fix documentation build issues Marius Bogoevici
  • Jul 31, 2012: Prerequisites for gwt Marius Bogoevici
  • May 30, 2012: Rename files without ordering, finish off single book output Pete Muir
  • May 29, 2012: Numbered files for proper ordering Marius Bogoevici
  • May 28, 2012: Oops Pete Muir
  • May 28, 2012: Fix author info Pete Muir
  • May 28, 2012: Add author info Pete Muir
  • May 24, 2012: Removed @author tags from code Marius Bogoevici
  • May 24, 2012: Added author tags for the document Marius Bogoevici
  • May 24, 2012: Gwt fixes Marius Bogoevici

See full history »