Fundamental Concepts

In this chapter you'll be introduced to a few more concepts and components of Echo that are required to build a simple application. The example program for this chapter is a number guessing game. The program generates a number between 1 and 100, and the user tries to guess it. Each time the user makes an incorrect guess, the program will inform the user whether the guess was too high or too low. When the user guesses correctly, s/he will be congratulated and given the option to play the game again.

If you have the Echo Tutorial Examples installed, you may run this example by visiting http://localhost:8080/EchoTutorial/numberguess. You must change the hostname and port number if your server is not running on localhost on port 8080.

In previous sections of the tutorial a few basic components of the Echo framework have been introduced, such as Button, Label, ContentPane, and Window. These components by themselves are not capable of building even a trivial application such as this number guessing game, so we will need to introduce a few more components, such as TextField and Filler, as well as some new concepts such as working with images.

Working With Images

Echo provides a number of classes which represent images that come from various sources. All of these classes implement a common interface, ImageReference, such that they may work with any Echo component that can use images. Several components which were previously introduced, such as Label and Button have the capability to display images as well as text.

There are several different implementations of the ImageReference class. Each of these classes is used to display a type of image that is obtained from a particular source. For example, the HttpImageReference class is used to represent an image that may be displayed by obtaining its data from a Web URI. A ResourceImageReference represents an image that is contained within a WAR archive and may be obtained through the Java classloader. An AwtImageReference enables Echo to display a java.awt.Image object within an application. The StreamImageReference class pulls image data from a Java InputStream, which can be useful for displaying images stored in a database or external filesystem.

In the sample application screenshot shown above, a Label has been created which displays a banner containing the graphic text "Guess-A-Number". This banner is represented as an HttpImageReference. The following code creates the label containing the image:

Label titleLabel = new Label(new HttpImageReference(
"images/guess_a_number.png"));

The URI "images/guess_a_number.png" is relative to the application, so if this application were deployed such that its URI were "http://hostname/EchoTutorial/numberguess", then this image would be retrieved from the URI "http://hostname/EchoTutorial/images/guess_a_number.png".

The TextField Component

The TextField component provides a user with the ability to enter a single line of text. In the sample application above, a TextField is being used for the entry of guesses.

TextField offers many capabilities which are not demonstrated by the simple number guess application. TextFields provide the developer with the option of separating the displayed and edited data from the visual representation with a model-view-controller design pattern. These capabilities are discussed in a later chapter of the tutorial. In this application, we simply use the getText() method to retrieve the data entered in the text field.

Layout Struts

Layout struts are very simple components which are used to separate content. To create a layout strut, one of two static factory methods in the Filler class may be invoked. Invoking createVerticalStrut(int height) will return a strut component that separates content before and after with a vertical space of the specified height in pixels. Likewise, invoking createHorizontalStrut(int width) will create a horizontal separator of a specific pixel width.

import nextapp.echo.Button;
import nextapp.echo.Color;
import nextapp.echo.ContentPane;
import nextapp.echo.EchoInstance;
import nextapp.echo.Filler;
import nextapp.echo.Font;
import nextapp.echo.HttpImageReference;
import nextapp.echo.Label;
import nextapp.echo.TextField;
import nextapp.echo.Window;

import nextapp.echo.event.ActionListener;
import nextapp.echo.event.ActionEvent;

import nextapp.echoservlet.EchoServer;

public class NumberGuessServlet extends EchoServer {

    public EchoInstance newInstance() {
        return new NumberGuess();
    }
}

class NumberGuess extends EchoInstance {
    
    private Window window;
    
    public Window init() {
       window = new Window("Number Guessing Game");
       startNewGame();
       
       return window;
    }
       
    public void startNewGame() {
        // Set the content to be a new GamePane, so the 
        window.setContent(new GamePane(this));
    }
    
    public void congratulate(int numberOfTries) {
        window.setContent(new CongratulationsPane(this, 
                numberOfTries));
    }
}

// The GamePane class extends ContentPane.  The
// startNewGame() method in the NumberGuess class sets the 
// content of the browser's window to be a new GamePane. 
// The GamePane's instance variables contain information 
// about the state of the game, such as what the random 
// number is and how many guesses have been made.
class GamePane extends ContentPane 
implements ActionListener {

    private NumberGuess numberGuess;
    private int randomNumber 
            = ((int) Math.floor(Math.random() * 100)) + 1;
    private int lowerBound = 1;
    private int upperBound = 100;
    private int numberOfTries = 0;
    private TextField guessEntryField;
    private Label statusLabel = new Label();
    private Label countLabel 
            = new Label("You have made no guesses.");
    private Label promptLabel= new Label("Guess a number "
            + "between 1 and 100: ");
    private int guess;
    
    public GamePane(NumberGuess numberGuess) {
        super();
        
        this.numberGuess = numberGuess;
        
        // An example of a label that contains an image.  
        Label titleLabel = new Label(new HttpImageReference(
                "images/guess_a_number.png"));
        add(titleLabel);

        // Adding a vertical strut will cause a 10 pixel 
        // vertical margin to appear between the titleLabel 
        // added above and the statusLabel added below it.
        add(Filler.createVerticalStrut(10));

        add(statusLabel);
        
        add(Filler.createVerticalStrut(10));

        add(countLabel);

        add(Filler.createVerticalStrut(10));

        add(promptLabel);
        
        // Creates a new empty text field
        // that displays 3 characters of data.
        guessEntryField = new TextField(3);
        
        guessEntryField.setForeground(Color.WHITE);
        guessEntryField.setBackground(Color.BLUE);
        add(guessEntryField);
        
        add(Filler.createVerticalStrut(10));
        
        Button submitButton 
                = new Button("Submit Your Guess");
        submitButton.setActionCommand("submit guess");
        submitButton.setForeground(Color.BLACK);
        submitButton.setBackground(Color.GREEN);
        submitButton.addActionListener(this);
        add(submitButton);
        
        add(Filler.createVerticalStrut(10));

        Button newGameButton 
                = new Button("Start a New Game");
        newGameButton.setActionCommand("new game");
        newGameButton.setForeground(Color.WHITE);
        newGameButton.setBackground(Color.RED);
        newGameButton.addActionListener(this);
        add(newGameButton);
    }
    
    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("new game")) {
            numberGuess.startNewGame();
        } else if (e.getActionCommand()
                .equals("submit guess")) {
            ++numberOfTries;
            
            if (numberOfTries == 1) {
                countLabel.setText("You have made 1 guess.");
            } else {
                countLabel.setText("You have made " 
                        + numberOfTries + " guesses.");
            }

            try {
                guess = Integer.parseInt(
                        guessEntryField.getText());
            } catch (NumberFormatException ex) {
                statusLabel.setText(
                        "Your guess was not valid.");
                return;
            }

            if (guess == randomNumber) {
                numberGuess.congratulate(numberOfTries);
            } else if (guess < 1 || guess > 100) {
                statusLabel.setText("Your guess, " + guess 
                        + " was not between 1 and 100.");
            } else if (guess < randomNumber) {
                if (guess >= lowerBound) {
                    lowerBound = guess + 1;
                }
                statusLabel.setText("Your guess, " + guess
                        + " was too low.  Try again:");
            } else if (guess > randomNumber) {
                statusLabel.setText("Your guess, " + guess
                        + " was too high.  Try again:");
                if (guess <= upperBound) {
                    upperBound = guess - 1;
                }
            }
            
            promptLabel.setText("Guess a number between "
                    + lowerBound + " and " 
                    + upperBound + ": ");
        }
    }
}

// Like the GamePane class, the CongratulationsPane extends
// ContentPane.  The game will replace the GamePane being
// held in its window with a CongratulationsPane when the
// user guesses the number correctly, by calling the 
// congratulate() method in the NumberGuess class.
class CongratulationsPane extends ContentPane
implements ActionListener {

    private NumberGuess numberGuess;

    public CongratulationsPane(NumberGuess numberGuess, 
            int numberOfTries) {
        this.numberGuess = numberGuess;
        
        Label label = new Label(new HttpImageReference(
                "images/congratulations.png"));
        add(label);
        
        add(Filler.createVerticalStrut(20));

        add(new Label("You got the correct answer in " 
                + numberOfTries 
                + (numberOfTries == 1 ? 
                " try." : " tries.")));

        add(Filler.createVerticalStrut(40));
        
        Button button = new Button("Play Again");
        button.addActionListener(this);
        add(button);
    }

    public void actionPerformed(ActionEvent e) {
        numberGuess.startNewGame();
    }
}