站内搜索: 请输入搜索关键词

当前页面: 开发资料首页J2ME 专题End-to-End J2ME Application Development by Example - Introducing Smart Ticket

End-to-End J2ME Application Development by Example - Introducing Smart Ticket

摘要: End-to-End J2ME Application Development by Example - Introducing Smart Ticket

by Norman Richards and Michael Yuan
June 2003

Overview

The first Java technology blueprint, Java Pet Store, was released in 2001 as a showcase for Sun's J2EE technologies. The blueprint not only provides sample code for a multilayered, database-driven e-commerce application, it also furnishes design guidelines and demonstrates commonly used patterns. Since that first release, the Java blueprints have become one of the most important resources for developers wanting to learn the latest J2EE technologies and best practices.

The Smart Ticket blueprint adds a new dimension: mobility. It demonstrates how to build a complete end-to-end mobile commerce system for ordering movie tickets, using J2ME MIDP for a wireless front end and a J2EE application server and a relational database at the back end. Studying how this application is designed and built will greatly enhance your understanding of the problems of mobile enterprise applications – and their solutions.

This article covers version 2.0 Early Access of the Smart Ticket code, released in April 2003. The screen shots and code samples in the early-access version may change slightly in the final release, but the lessons you learn from the design should still hold. Smart Ticket 1.2 is still available. It has the same model and back-end implementation as the version discussed here, so many of the details are applicable to both past and future releases. Unless otherwise noted, all source code in this article is copyrighted by Sun Microsystems.

Download and Installation

The Smart Ticket application is available from Sun's Blueprints web site. The .zip archive contains source code, Ant build scripts, and pre-built, deployable applications.

Smart Ticket contains a J2ME component and a J2EE component. Running it requires a J2EE application server (such as the Sun J2EE reference implementation, version 1.3 or higher), and either a MIDP 2.0-compatible device with Internet connectivity or a suitable emulator, such as the one in Sun's J2ME Wireless Toolkit 2.0. The Smart Ticket distribution contains specific instructions for building and deploying the application. To get started:

  1. Make sure you have these resources installed:

    • JDK v1.4.1 or higher
    • J2EE v1.3.1 or higher
    • J2ME Wireless Toolkit 2.0 or higher

  2. Set the following environment variables:

    • JAVA_HOME: JDK installation directory
    • J2EE_HOME: J2EE RI installation directory
    • J2MEWTK_HOME: J2ME Wireless Toolkit installation directory

  3. Start the J2EE server:

    <table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
    J2EE_HOME/bin/cloudscape -start
    
    J2EE_HOME/bin/j2ee -verbose
    
    
    </td></tr></table>

  4. Deploy the J2EE application. Use the setup script as follows to invoke the deploy Ant task in setup.xml:

    <table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
    setup deploy
    
    
    </td></tr></table>
  5. Point your browser to http://localhost:8000/smartticket and click on the Populate Database link to import mock theater and movie data into the database. This is a very slow process on older computers, so be patient! The mock data includes theaters in two Zip codes: 95054 and 95130.

  6. Start J2ME Wireless Toolkit 2.0 and run the MIDlet described in smart_ticket-client.jad.

Smart Ticket in Action

When you have the MIDlet running, take a brief tour to get the user's perspective. You'll find you can perform four kinds of tasks.

Smart Ticket Benefits

Older mobile-commerce platforms such as the WAP/WML-based micro-browsers put all the intelligence on the server side. A key benefit of J2ME is that it supports smart clients that run on devices. Smart Ticket capitalizes fully on the advantages of the smart-client application paradigm:

How are those features implemented?

Important Architectural Patterns

The Overall MVC Pattern

The overall architecture of the Smart Ticket application follows the Model-View-Controller pattern. The application is separated into several logical layers, so developers can change one part without affecting others. Smart Ticket adopts the MVC model as follows:

The Client-Side Facade

For most application actions, the controller's entry point into the model layer is the ModelFacade class. In keeping with the MVC pattern, ModelFacade contains one method for each action in the model layer. Depending on the nature of the action, the façade delegates it to one or more of the following model classes:

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
package com.sun.j2me.blueprints.smartticket.client.midp.model;

public class ModelFacade {

  private SynchronizationAgent syncAgent;

  private RemoteModelProxy remoteModel;

  private LocalModel localModel;

  // Action methods ...

 

  public Reservation reserveSeats(String theaterKey,

                         String movieKey,

                         int[] showTime, Seat[] seats)

                         throws ApplicationException {

    try {

      return remoteModel.reserveSeats(theaterKey,

          movieKey, showTime, seats);

    } catch (ModelException me) {

      // ...

    }

  }

  public void purchaseTickets(Reservation reservation)

                          throws ApplicationException {

    try {

      remoteModel.purchaseTickets(reservation.getId());

      localModel.addMovieRating(

        new MovieRating(remoteModel.getMovie(reservation.getMovieId()),

                        reservation.getShowTime()));

    } catch (ModelException me) {

      // ...

    }

    return;

  }

 

  public void synchronizeMovieRatings(int

     conflictResolutionStrategyId)

         throws ApplicationException {

    try {

      syncAgent.synchronizeMovieRatings(conflictResolutionStrategyId);

      return;

    } catch (ModelException me) {

      // ...

    }

  }

  // ...

}

</td></tr></table>

The Server-Side Facade

The server side of the application uses a number of Enterprise JavaBeans components (EJBs) to encapsulate the business logic and manage interaction with a relational database. When the RemoteModelProxy on the client side makes an RPC call to the server side, the HTTP servlet SmartTicketServlet invokes the appropriate action method in a session EJB, SmartTicketFacadeBean, through a business delegate object SmartTicketBD. Depending on the nature of the request, it will be further delegated to one of two other session beans, TicketingBean or SynchronizingBean. An array of entity beans uses EJB 2.0 container-managed persistence to update the database as needed.

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
package com.sun.j2me.blueprints.smartticket.server.web.midp;

public class SmartTicketServlet extends HttpServlet {

  public static final String SESSION_ATTRIBUTE_SMART_TICKET_BD =

    "com.sun.j2me.blueprints.smartticket.server.web.midp.SmartTicketBD";

 

  protected void doPost(HttpServletRequest request,

                        HttpServletResponse response)

                        throws ServletException, IOException {

    HttpSession session = request.getSession(true);

    SmartTicketBD smartTicketBD = (SmartTicketBD)

      session.getAttribute(SESSION_ATTRIBUTE_SMART_TICKET_BD);

    // Calls handleCall() method and encodes the URL for

    // session tracking

  }

 

  public int handleCall(SmartTicketBD smartTicketBD,

     InputStream in, OutputStream out)

         throws IOException, ApplicationException {

    // Identifies the requested action method

   

    // Executes the method, as selected in a switch statement

    switch (method) {

   

    // cases ...

   

    case MessageConstants.OPERATION_GET_MOVIE: {

      getMovie(smartTicketBD, call, successfulResult);

      break;

    }

    // more cases ...

   

    }

  }

}

package com.sun.j2me.blueprints.smartticket.server.web.midp;

public class SmartTicketBD implements RemoteModel {

  public static final String EJB_REF_FACADE =

      "ejb/SmartTicketFacade";

  private SmartTicketFacadeLocal facade;

  private ServletContext servletContext = null;

  public SmartTicketBD(ServletContext servletContext)

                       throws ApplicationException {

    this.servletContext = servletContext;

    try {

      Context context = (Context)

        new InitialContext().lookup("java:comp/env");

      facade = ((SmartTicketFacadeLocalHome)

        context.lookup(EJB_REF_FACADE)).create();

      return;

    } catch (Exception e) {

      throw new ApplicationException(e);

    }

  }

 

  public Movie getMovie(String movieKey)

      throws ModelException, ApplicationException {

    try {

      MovieLocal movieLocal = facade.getMovie(movieKey);

      Movie movie = new Movie(movieLocal.getId(),

                              movieLocal.getTitle(),

                              movieLocal.getSummary(),

                              movieLocal.getRating());

      return movie;

    } catch (SmartTicketFacadeException stfe) {

      throw new

          ModelException(ModelException.CAUSE_MOVIE_NOT_FOUND);

    } catch (Exception e) {

      throw new ApplicationException(e);

    }

  }

 

  // Other action methods in RemoteModel interface ...

 

}

package com.sun.j2me.blueprints.smartticket.server.ejb;

public class SmartTicketFacadeBean implements SessionBean {

  // ...

 

  public MovieLocal getMovie(String movieId)

                    throws SmartTicketFacadeException {

    try {

      return movieHome.findByPrimaryKey(movieId);

    } catch (FinderException fe) {

      throw new

          SmartTicketFacadeException("No matching movie.");

    }

  }

 

  // ...

 

}

</td></tr></table>

This diagram illustrates the overall MVC-plus-façade architecture:

<table cellSpacing=0 cellPadding=10 width=image_width align=center border=0> <tr> <td align=middle> </td></tr></table>

Implementation Patterns

The MVC and façade patterns define the overall architecture of the application. In addition, Smart Ticket also showcases some important behavior patterns that could help developers improve productivity.

Chain of Handlers

The RemoteModelProxy class delegates each requested action to a chain of handler classes that transparently work out the dirty plumbing of the RMS serialization and HTTP connection. The chained-handler architecture is based on the RequestHandler interface and on the RemoteModelRequestHandler abstract class that implements it:

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
public interface RequestHandler {

    RequestHandler getNextHandler();

    void init() throws ApplicationException;

    void destroy() throws ApplicationException;

}

abstract public class RemoteModelRequestHandler

  implements RequestHandler, RemoteModel {

  private RemoteModelRequestHandler nextHandler;

  private Preferences preferences;

  protected static ProgressObserver progressObserver;

  public RemoteModelRequestHandler(

      RemoteModelRequestHandler nextHandler) {

    this.nextHandler = nextHandler;

  }

  public RequestHandler getNextHandler() {

    return nextHandler;

  }

  public void init() throws ApplicationException {

    if (nextHandler != null) {

      nextHandler.init();

    }

    return;

  }

  public void destroy() throws ApplicationException {

    if (nextHandler != null) {

      nextHandler.destroy();

    }

    return;

  }

 

  public void login(String userName, String password)

      throws ModelException, ApplicationException {

    getRemoteModelRequestHandler().login(userName, password);

    return;

  }

  public void createAccount(AccountInfo accountInfo)

                            throws ModelException,

                            ApplicationException {

    getRemoteModelRequestHandler().createAccount(accountInfo);

    return;

  }

 

  // Other action methods declared in RemoteModel

  // ...

}

</td></tr></table>

Concrete handler classes extend the RemoteModelRequestHandler class. Nested constructors establish a chain of handlers. Smart Ticket makes two handler classes available: RMSCacheHandler and HTTPCommunicationHandler. The chain is assembled thus:

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
public class RemoteModelProxy extends ModelObjectLoader

                              implements RemoteModel {

  private RemoteModelRequestHandler requestHandlerChain;

  private Preferences preferences = null;

  private Hashtable movies = new Hashtable();

  public RemoteModelProxy(String serviceURL)

                          throws ApplicationException {

    requestHandlerChain =

      new RMSCacheHandler(

        new HTTPCommunicationHandler(null, serviceURL));

    return;

  }

 

  // ...

 

  public Movie getMovie(String movieKey)

                        throws ModelException,

                        ApplicationException {

    Movie movie = (Movie) movies.get(movieKey);

    if (movie == null) {

      movie = requestHandlerChain.getMovie(movieKey);

      movies.put(movieKey, movie);

    }

    return movie;

  }

 

  // Other methods ...

 

}

</td></tr></table>

A handler can implement any action methods in the RemoteModel interface selectively, in either of two ways:

  • If a RemoteModelProxy class calls an action method not implemented by the first handler class, the base RemoteModelRequestHandler class ensures that the call is passed to the next handler in the chain.
  • If a handler in a chain decides it has finished processing an action, it returns directly. Otherwise, it invokes the same action method in the superclass, to pass it to the next handler in the chain
<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
public class RMSCacheHandler extends RemoteModelRequestHandler {

  // ...

  public Movie getMovie(String movieKey)

                        throws ModelException,

                        ApplicationException {

    IndexEntry indexEntry =

        rmsAdapter.getIndexEntry(movieKey,

            IndexEntry.TYPE_MOVIE, IndexEntry.MODE_ANY);

    if (indexEntry != null) {

      return rmsAdapter.loadMovie(indexEntry.getRecordId());

    }

    return super.getMovie(movieKey);

  }

 

  // ...

 

}

</td></tr></table>

Binary Remote Procedure Call over HTTP

In the model layer, the HTTPCommunicationHandler class in the RemoteModelProxy class invokes remote procedures on the server side through a binary RPC protocol over an HTTP connection. The protocol is defined as follows:

All RPC requests from the client to the server follow the same basic pattern. The first byte in the stream specifies the action method that the façade session bean on the server side must execute, and the remaining bytes encode a sequence of UTF strings that represent the parameters to be passed to the remote method. The response HTTP stream contains the RPC return value. The formats of the requests and responses are unique to each method, and you have to look at the source code for each to figure out the exact format.

The RPC codes that go into the first byte of the request stream are defined in the MessageConstants class:

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
package com.sun.j2me.blueprints.smartticket.shared.midp;

public final class MessageConstants {

  public static final byte OPERATION_LOGIN_USER = 0;

  public static final byte OPERATION_CREATE_ACCOUNT = 1;

  public static final byte OPERATION_UPDATE_ACCOUNT = 2;

  public static final byte OPERATION_GET_THEATERS = 3;

  public static final byte OPERATION_GET_THEATER_SCHEDULE = 4;

  public static final byte OPERATION_GET_MOVIE = 5;

  public static final byte OPERATION_GET_MOVIE_POSTER = 6;

  public static final byte OPERATION_GET_MOVIE_SHOWTIMES = 7;

  public static final byte OPERATION_GET_SEATING_PLAN = 8;

  public static final byte OPERATION_RESERVE_SEATS = 9;

  public static final byte OPERATION_PURCHASE_TICKETS = 10;

  public static final byte OPERATION_CANCEL_SEAT_RESERVATION = 11;

  public static final byte OPERATION_GET_LOCALES = 12;

  public static final byte OPERATION_GET_RESOURCE_BUNDLE = 13;

  public static final byte OPERATION_INITIATE_SYNCHRONIZATION = 14;

  public static final byte OPERATION_SYNCHRONIZE_MOVIE_RATINGS = 15;

  public static final byte OPERATION_COMMIT_MOVIE_RATINGS = 16;

  public static final byte ERROR_NONE = 0;

  public static final byte ERROR_UNKNOWN_OPERATION = 1;

  public static final byte ERROR_SERVER_ERROR = 2;

  public static final byte ERROR_MODEL_EXCEPTION = 3;

  public static final byte ERROR_REQUEST_FORMAT = 4;

 

  private MessageConstants() {}

}

</td></tr></table>

The two classes that follow illustrate an RPC round trip; an action method of the HTTPCommunicationHandler requests information about a specified movie, and invokes a method of the Movie class to extract the return values from the response stream.

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
package com.sun.j2me.blueprints.smartticket.client.midp.model;

public class HTTPCommunicationHandler

  extends RemoteModelRequestHandler {

  // ...

 

  public Movie getMovie(String movieKey)

                        throws ModelException,

                        ApplicationException {

    HttpConnection connection = null;

    DataOutputStream outputStream = null;

    DataInputStream inputStream = null;

    try {

      connection = openConnection();

      updateProgress();

      outputStream = openConnectionOutputStream(connection);

      // The RPC request

      outputStream.writeByte(MessageConstants.OPERATION_GET_MOVIE);

      outputStream.writeUTF(movieKey);

      outputStream.close();

      updateProgress();

      // unmarshal the return values

      inputStream = openConnectionInputStream(connection);

      Movie movie = Movie.deserialize(inputStream);

      updateProgress();

      return movie;

    } catch (IOException ioe) {

      throw new

        ApplicationException(ErrorMessageCodes.ERROR_CANNOT_CONNECT);

    } finally {

      closeConnection(connection, outputStream, inputStream);

    }

  }

 

  // Other action methods ...

 

}

package com.sun.j2me.blueprints.smartticket.shared.midp.model;

public class Movie {

  private String primaryKey;

  private String title;

  private String summary;

  private String rating;

  private boolean alreadySeen = false;

  transient private byte[] poster = null;

   

  public static Movie deserialize(DataInputStream

      dataStream) throws ApplicationException {

    try {

      Movie movie = new Movie();

      // Read the RPC response stream

      movie.primaryKey = dataStream.readUTF();

      movie.title = dataStream.readUTF();

      movie.summary = dataStream.readUTF();

      movie.rating = dataStream.readUTF();

      try {

        movie.alreadySeen = dataStream.readBoolean();

      } catch (IOException ioe) {

        movie.alreadySeen = false;

      }

      try {

        return

            ModelObjectLoader.getInstance().getMovie(movie);

      } catch (ModelException me) {

        throw new ApplicationException();

      }

    } catch (IOException ioe) {

      throw new ApplicationException(ioe);

    }

  }

 

  // Other methods ...

 

}

</td></tr></table>

On the server side, the SmartTicketServlet first determines the action desired from the code in the first byte in the request stream. It then dispatches the request to the appropriate action method through the façade, passing all the RPC parameters remaining in the stream.

In Smart Ticket, the client and server are tightly coupled. This approach can improve network efficiency because each RPC exchange can be specially designed and optimized. The trade-off, however, is development speed and robustness. Even small changes to the server are likely to force changes in the protocol and the parsing code on the client side too, and potentially in multiple places. Developers need to keep track of all code that might be affected, and update it when necessary. They also need to recompile and redistribute clients oftener than they'd like, which could also lead to errors.

The Client-Side Thread Model

The Smart Ticket application uses a sophisticated threading model on the client side, with two important aspects:

  • The MIDP specification requires the CommandListener.commandAction() method to "return immediately" to avoid blocking the UI, so any lengthy operation must be put into another thread.
  • One of the running threads can display a moving gauge indicating the progress of a long action, particularly any that involves remote network operations. The gauge screen can provide impatient users with a button to cancel actions that take too long.
    <table cellSpacing=0 cellPadding=10 width=image_width align=center border=0> <tr> <td align=middle>
    (Click image to enlarge.) </td></tr></table>

You probably noticed earlier that action methods in the UIController class are simply wrappers of the runWithProgress() method, which sets the display to ProgressObserverUI and starts the EventDispatcher thread. The ProgressObserverUI screen displays a gauge and a Stop button which is monitored by the main MIDlet system UI thread. As we described, the EventDispatcher thread eventually delegates the requested action to methods in the model layer. Each of these methods calls the ProgressObserverUI.updateProgress() at certain stages in its execution to tell the user it's making progress.

<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>
public class UIController {

 

  // Action methods ...

 

  public void chooseMovieRequested() {

    runWithProgress(

      new EventDispatcher(

        EventIds.EVENT_ID_CHOOSEMOVIEREQUESTED,

            mainMenuUI),

                getString(UIConstants.PROCESSING), false);

  }

 

  // Action methods ...

 

  public void runWithProgress(Thread thread, String title,

                              boolean stoppable) {

    progressObserverUI.init(title, stoppable);

    getDisplay().setCurrent(progressObserverUI);

    thread.start();

  }

 

  class EventDispatcher extends Thread {

    // ...

    public void run() {

      // Switch -- case statements to delegate

      // actions to the model layer

    }

  }

}

public class ProgressObserverUI extends Form

            implements ProgressObserver,

            CommandListener {

  private UIController uiController;

  private static final int GAUGE_MAX = 8;

  private static final int GAUGE_LEVELS = 4;

  int current = 0;

  Gauge gauge;

  Command stopCommand;

  boolean stoppable;

  boolean stopped;

  public ProgressObserverUI(UIController uiController) {

    super("");

    gauge = new Gauge("", false, GAUGE_MAX, 0);

    stopCommand = new

        Command(uiController.getString(UIConstants.STOP),

            Command.STOP, 10);

    append(gauge);

    setCommandListener(this);

  }

  public void init(String note, boolean stoppable) {

    gauge.setValue(0);

    setNote(note);

    setStoppable(stoppable);

    stopped = false;

  }

  public void setNote(String note) {

    setTitle(note);

  }

  public boolean isStoppable() {

    return stoppable;

  }

  public void setStoppable(boolean stoppable) {

    this.stoppable = stoppable;

    if (stoppable) {

      addCommand(stopCommand);

    } else {

      removeCommand(stopCommand);

    }

  }

  /**

   * Indicates whether the user has stopped the progress.

   * This message should be called before calling update.

   */

  public boolean isStopped() {

    return stopped;

  }

  public void updateProgress() {

    current = (current + 1) % GAUGE_LEVELS;

    gauge.setValue(current * GAUGE_MAX / GAUGE_LEVELS);

  }

  public void commandAction(Command c, Displayable d) {

    if (c == stopCommand) {

      stopped = true;

    }

  }

}

</td></tr></table>

Conclusion

This article introduced the all-new Smart Ticket blueprint v2.0. Several key improvements over the previous versions take advantage of the rich capability of smart clients. Smart Ticket shows you how to implement advanced features using several important design patterns, which we explored briefly. We hope our presentation will get you off to a fast start in the world of end-to-end design patterns!

Resources

  • Sun Java Wireless Blueprints
  • "Developing an End to End Wireless Application Using Java Smart Ticket Demo" by Eric Larson (covers Smart Ticket v1.1)

<table class=grey4 cellSpacing=0 cellPadding=0 width="100%" border=0> <tr> <td></td></tr></table>