Skip to content

Latest commit

 

History

History
449 lines (360 loc) · 19.8 KB

DashboardGWT.asciidoc

File metadata and controls

449 lines (360 loc) · 19.8 KB

Building A Statistics Dashboard Using GWT And Errai

What will you learn now?

You’ve just an 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 what follows next, 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.

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,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 rel="stylesheet" href="resources/bootstrap/css/bootstrap.css" />
    <link type="text/css" rel="stylesheet" href="resources/css/screen.css" />

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

<body>
    <div id="container">
        <div id="menu">
            <div class="navbar">
                <div class="navbar-inner">
                    <div class="container">
                        <a class="brand">JBoss Ticket Monster Booking Monitor</a>
                    </div>
                </div>
            </div>
        </div>
        <h3 class="booking-status-header">Booking status</h3>
        <div id="content" class="container-fluid"></div>
    </div>

    <footer>
        <div style="text-align: center;">
            <img src="resources/img/logo.png" alt="Errai" />
        </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/mai/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 of using 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.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 */
@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 createAndShowUI method is invoked (@AfterInitialization takes care of that). 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 occured. 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}.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 */
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}.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 */
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).