Using State Guard in GUI

The State Guard pattern works very well with the patterns Builder, Chained Creator and Context Switcher but here we will demonstrate how it can be used all by itself in a graphical user interface to handle validation and transition to a valid state. The example is written in Java and uses Swing with the layout manager SpringLayout. The source code is hosted at GitHub.

Lets say we want to create this GUI:

The program starts by executing the class Main:
package nu.tengstrand.stateguard.guiexample;

import nu.tengstrand.stateguard.guiexample.person.Person;
import nu.tengstrand.stateguard.guiexample.person.PersonStateGuard;

import javax.swing.*;

public class Main {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                final PersonStateGuard personStateGuard = new PersonStateGuard();

                new PersonFrame(personStateGuard, new SaveCommand() {
                    public void save() {
                        Person person = personStateGuard.asValidState();
                        new PopupFrame(person);
                   }
                });
            }
        });
    }
}
  • row 13:
    the class PersonStateGuard holds all the attributes used in the GUI.
  • row 15:
    references of PersonStateGuard and the interface SaveCommand is sent in to the constructor of PersonFrame:
package nu.tengstrand.stateguard.guiexample;

import nu.tengstrand.stateguard.Validatable;
import nu.tengstrand.stateguard.guiexample.person.PersonStateGuard;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ResourceBundle;

public class PersonFrame extends JFrame {
    static ResourceBundle resourceBundle = ResourceBundle.getBundle("validationMessages");

    public PersonFrame(final PersonStateGuard person, final SaveCommand saveCommand) {
        setTitle("State Guard example - by Joakim Tengstrand");
        setPreferredSize(new Dimension(450, 190));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Container contentPane = getContentPane();
        SpringLayout layout = new SpringLayout();
        contentPane.setLayout(layout);

        // Name
        JLabel nameLabel = new JLabel("Name: ");
        JTextField nameTextField = new JTextField("", 15);
        JLabel nameError = new JLabel();
        nameError.setForeground(Color.RED);
        contentPane.add(nameLabel);
        contentPane.add(nameTextField);
        contentPane.add(nameError);

        // Age
        JLabel ageLabel = new JLabel("Age: ");
        JTextField ageTextField = new JTextField("", 5);
        JLabel ageError = new JLabel();
        ageError.setForeground(Color.RED);
        contentPane.add(ageLabel);
        contentPane.add(ageTextField);
        contentPane.add(ageError);

        // Country
        JLabel countryLabel = new JLabel("Country: ");
        JTextField countryTextField = new JTextField("", 10);
        JLabel countryError = new JLabel();
        countryError.setForeground(Color.RED);
        contentPane.add(countryLabel);
        contentPane.add(countryTextField);
        contentPane.add(countryError);

        // Save button
        final JButton saveButton = new JButton("Save");
        saveButton.setEnabled(false);
        saveButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                saveCommand.save();
            }
        });
        contentPane.add(saveButton);

        // Validation explanation
        JLabel validationErrorExplanationLabel = new JLabel("* = Mandatory field");
        contentPane.add(validationErrorExplanationLabel);

        connectTextFieldToModel(person.name(), nameTextField, nameError, person, saveButton);
        connectTextFieldToModel(person.age(), ageTextField, ageError, person, saveButton);
        connectTextFieldToModel(person.country(), countryTextField, countryError, person, saveButton);

        // Spring layout constraints
        layout.putConstraint(SpringLayout.WEST, nameLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, nameLabel, 5, SpringLayout.NORTH, contentPane);
        layout.putConstraint(SpringLayout.WEST, nameTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, nameTextField, 5, SpringLayout.NORTH, contentPane);
        layout.putConstraint(SpringLayout.WEST, nameError, 20, SpringLayout.EAST, nameTextField);
        layout.putConstraint(SpringLayout.NORTH, nameError, 5, SpringLayout.NORTH, contentPane);

        layout.putConstraint(SpringLayout.WEST, ageLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, ageLabel, 25, SpringLayout.NORTH, nameTextField);
        layout.putConstraint(SpringLayout.WEST, ageTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, ageTextField, 25, SpringLayout.NORTH, nameTextField);
        layout.putConstraint(SpringLayout.WEST, ageError, 20, SpringLayout.EAST, ageTextField);
        layout.putConstraint(SpringLayout.NORTH, ageError, 25, SpringLayout.NORTH, nameTextField);

        layout.putConstraint(SpringLayout.WEST, countryLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, countryLabel, 25, SpringLayout.NORTH, ageTextField);
        layout.putConstraint(SpringLayout.WEST, countryTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, countryTextField, 25, SpringLayout.NORTH, ageTextField);
        layout.putConstraint(SpringLayout.WEST, countryError, 20, SpringLayout.EAST, countryTextField);
        layout.putConstraint(SpringLayout.NORTH, countryError, 25, SpringLayout.NORTH, ageTextField);

        layout.putConstraint(SpringLayout.WEST, validationErrorExplanationLabel, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, validationErrorExplanationLabel, 30, SpringLayout.NORTH, countryLabel);

        layout.putConstraint(SpringLayout.WEST, saveButton, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, saveButton, 30, SpringLayout.NORTH, validationErrorExplanationLabel);

        pack();
        setVisible(true);
    }

    private void connectTextFieldToModel(final ValidatableStringValue validatableStringValue, JTextField textField, final JLabel error, final Validatable person, final JButton saveButton) {
        error.setText(validatableStringValue.validationMessages().firstMessage(resourceBundle));

        textField.getDocument().addDocumentListener(new UpdateTextListener() {
            public void setText(String text) {
                validatableStringValue.setValue(text);
                error.setText(validatableStringValue.validationMessages().firstMessage(resourceBundle));
                saveButton.setEnabled(person.isValid());
            }
        });
    }
}
  • row 16-63, 69-98:
    GUI setup code
  • row 54:
    Callback to Main via the SaveCommand interface.
  • row 65-67:
    When a text field is edited, this will happen:
    • the corresponding attribute (name, age, country) in PersonStateGuard is updated.
    • if the text field does not validate, the validation message is shown (asterisk if empty).
    • the save button is enabled/disabled depending on if the PesonStateGuard instance is valid (line 108).
Lets demonstrate this with a couple of pictures:

The asterisk shows the mandatory fields. When the Name is filled in, the asterisk goes away. This is handled by the class NameStateGuard (the property mandatory.field at row 9 is stored in the property file validationMessages):
package nu.tengstrand.stateguard.guiexample.person;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.guiexample.ValidatableStringValue;
import nu.tengstrand.stateguard.validator.NonEmptyString;

public class NameStateGuard extends StateGuard implements ValidatableStringValue {
    private NonEmptyString name = NonEmptyString.attributeName("name")
                                  .messageKey("mandatory.field");

    public NameStateGuard() {
        addValidator(name);
    }

    public void setValue(String value) {
        name.setValue(value);
    }

    @Override
    protected Name createValidState() {
        return new Name(name.value());
    }
}

When an invalid integer is typed, a validation error is shown. This is handled by the class AgeValidator in AgeStateGuard:
package nu.tengstrand.stateguard.guiexample.person;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.ValidationMessages;
import nu.tengstrand.stateguard.guiexample.ValidatableStringValue;
import nu.tengstrand.stateguard.validator.IntegerValidator;

public class AgeStateGuard extends StateGuard implements ValidatableStringValue {
    private AgeValidator age = new AgeValidator();

    private static final int MIN_AGE = 0;
    private static final int MAX_AGE = 150;

    public AgeStateGuard() {
        addValidator(age);
    }

    public void setValue(String value) {
        age.setValue(value);
    }

    @Override
    protected Age createValidState() {
        return new Age(age.value());
    }


    private static class AgeValidator extends IntegerValidator {
        AgeValidator() {
            super("age");
        }

        @Override
        public boolean isValid() {
            return super.isValid() && value() >= MIN_AGE && value() <= MAX_AGE;
        }

        @Override
        public ValidationMessages validationMessages() {
            if (isValid()) {
                return ValidationMessages.withoutMessage();
            }
            if (stringValue() == null || stringValue().length() == 0) {
                return ValidationMessages.message("*");
            }
            return ValidationMessages.message("Enter a valid age");
        }
    }
}
The AgeValidator also checks if the age is within the range 0 to 150 (line 35).
The validation of Country is handled by the class CountryValidator in CountryStateGuard. The only valid countries are Sweden and Norway (case insensitive).
Now, when all fields are filled in correct, the save button is enabled.
When the save button is clicked we now know that the instance of PersonStateGuard is valid so we can safely let it create a valid instance of Person at line 17 in Main. The resulting instance of Person is shown in the popup window PopupFrame.

Best Regards,
Joakim Tengstrand


The State Guard Pattern

A common task is to write code that handles type conversions and validations, often in combination with conversion of data structures or transition from a mutable to an immutable state. One example is where you need to read data from a file that needs to be validated, type converted and packaged into a new class or class structure.

Let's take another example. You need to create a GUI that registers users and you want to store them in the class Person with the attributes ssn, name and country. Assume that you want to handle these attributes with the value objects SSN, PersonName and Country. The three attributes are handled as strings in the GUI and you also want to display error messages if any of them can't be translated to the corresponding value object.

The need here is to transform a structure of strings, and package it as an instance of Person. Before that can happen, the validations has to be performed on its internal representation, and any error messages should be displayed in the GUI. This pattern presents a solution that allows a dedicated class StateGuard to be responsible for type conversion, validations and state transitions.

In our example this is handled by the class PersonStateGuard and its attributes by the classes SSNStateGuard, PersonNameStateGuard and CountryStateGuard. This solution provides separation of concerns, the ability to build reusable validation components and ensures that all validations have passed before the transition to a valid state can happen. Here comes a more detailed description of the State Guard pattern.

Let's call the validated class with valid state X. The responsibility for validation and creation of instances of X belongs in this pattern to the class XStateGuard that inherits from StateGuard.
XStateGuard gives the advantage that class X can be immutable or it can be switchable from mutable to immutable in a controlled manner.

With the State Guard patten you can:
  • Ensure that all validations have been executed before the instance of X is created, example: X.create(params...).asValidState(), where create returns an instance of XStateGuard. If you want to allow creation of instances of X directly, you need to make the constructor of X public and add a check that throws an exception if new XStateGuard (params...).isValid () returns false.
  • Create State Guard components that can be used as building blocks for other state guard classes. The classes NotNull and NonEmptyString in package nu.tengstrand.stateguard.validator are examples of such validators.
  • Get access to the instance of XStateGuard with methods that faciliates validation and transition to valid state:
    • isValid(): returns true if in a valid state.
    • validationMessages(): returns any validation errors.
    • asValidState(): returns an instance of X if XStateGuard is in a valid state, otherwise an exception is thrown.
Example Main.java:
package nu.tengstrand.stateguard.example;

import nu.tengstrand.stateguard.example.book.Book;
import nu.tengstrand.stateguard.example.book.BookStateGuard;
import nu.tengstrand.stateguard.example.book.attributes.BookBinding;
import nu.tengstrand.stateguard.example.book.BookBuilder;
import nu.tengstrand.stateguard.example.book.BookCreator;

import java.util.ResourceBundle;

public class Main {
    static ResourceBundle resourceBundle = ResourceBundle.getBundle("validationMessages");

    public static void main(String[] args) {
        System.out.println("----- Build: Missing attributes ------");
        BookBuilder bookBuilder = Book.build().title("My Book");
        printValidationMessages(bookBuilder);

        System.out.println("\n----- Build: With binding + pages ------");
        bookBuilder.binding(BookBinding.PAPERBACK).pages(50);
        printValidationMessages(bookBuilder);
        Book bookWithMissingBinding = bookBuilder.asValidState();
        System.out.println("Book: " + bookWithMissingBinding);

        System.out.println("\n----- Create: Empty title ------");
        BookCreator bookCreatorWithEmptyTitle = Book.create().title("").paperback().pages(100);
        printValidationMessages(bookCreatorWithEmptyTitle);

        System.out.println("\n----- Create: Thick book ------");
        BookCreator thickBookCreator = Book.create().title("Thick book").paperback().pages(3000);
        printValidationMessages(thickBookCreator);

        System.out.println("\n----- Create: Valid book ------");
        BookCreator bookCreator = Book.create().title("The book").paperback().pages(200);
        printValidationMessages(bookCreator);
        Book book = bookCreator.asValidState();
        System.out.println("Book: " + book);
    }

    private static void printValidationMessages(BookStateGuard stateGuard) {
        System.out.println("Valid=" + stateGuard.isValid());
        for (String formattedMessage : stateGuard.validationMessages().formattedMessages(resourceBundle)) {
            System.out.println(formattedMessage);
        }
    }
}
validationMessages.properties:
missingvalue=Missing value, please enter a value for ''{0}''
book.pages=The attribute 'pages' must be greater than zero, but was {0}

This will generate the following output:
----- Build: Missing attributes ------
Valid=false
Attribute 'pages' must be greater than zero, but was 0
Attribute 'binding' can not be null

----- Build: With binding + pages ------
Valid=true
Book: Book{title='My Book', binding=PAPERBACK, pages=50}

----- Create: Empty title ------
Valid=false
Missing value, please enter a value for 'title'

----- Create: Thick book ------
Valid=true

----- Create: Valid book ------
Valid=true
Book: Book{title='The book', binding=PAPERBACK, pages=200}

Here we use a resourceBundle which replaces the default validation messages. If formattedMessages() at line 42 is called without arguments, the default messages are used.

The example makes use of the patterns builder (line 16) and chained creator (line 26, 30 and 34). This is not required but increase the readability of the code. The source is hosted at GitHub and has the following structure:


The package example.book contains all classes that handles a Book. In the package book.attributes you can see that all attributes also have a corresponding state guard. In package book the state guard classes for Book are handled by BookStateGuard, BookBuilder and BookCreator. Other classes directly under nu.tengstrand.stateguard are all part of the framework State Guard.

We have put the shared code for BookBuilder and BookCreator in the class BookStateGuard as we in this example can create instances of Book by using both Book.create() and Book.build():
package nu.tengstrand.stateguard.example.book;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.example.book.attributes.BookBindingStateGuard;
import nu.tengstrand.stateguard.example.book.attributes.BookPagesStateGuard;
import nu.tengstrand.stateguard.example.book.attributes.BookTitleStateGuard;

public abstract class BookStateGuard extends StateGuard<Book> {
    protected BookTitleStateGuard title = new BookTitleStateGuard();
    protected BookBindingStateGuard binding = new BookBindingStateGuard();
    protected BookPagesStateGuard pages = new BookPagesStateGuard();

    public BookStateGuard() {
        addValidators(title, binding, pages);
    }

    @Override
    protected Book createValidState() {
        return new Book(title.asValidState(), binding.asValidState(), pages.asValidState());
    }
}

This is an example of a state guard with more than one attribute, classes with only one attribute follows the same pattern:
  • row 8:
    inherits from the class StateGuard and indicates that there is a Book we want to create a valid instance of.
  • row 9-11:
    the class attributes in the form of three state guard which all have a counterpart in Book
  • row 14:
    register the attributes to be validated. You can also add other validators, the only requirement is that they implement the interface Validatable.
  • rad 18:
    returns a Book which represents our valid state. Set the method to protected so it can only be used by StateGuard.
BookBuilder:
package nu.tengstrand.stateguard.example.book;

import nu.tengstrand.stateguard.example.book.attributes.BookBinding;

public class BookBuilder extends BookStateGuard {
    BookBuilder() {
    }

    public BookBuilder title(String title) {
        this.title.setTitle(title);
        return this;
    }

    public BookBuilder binding(BookBinding binding) {
        this.binding.setBinding(binding);
        return this;
    }

    public BookBuilder pages(int pages) {
        this.pages.setPages(pages);
        return this;
    }
}

This StateGuard implements the pattern Builder:
  • row 5:
    inherits from the class BookStateGuard.
  • row 6:
    empty constructor with the visibility package-private, to force the usage of Book.build().
  • row 9, 14 and 19:
    set attributes and returns this.
BookCreator:
package nu.tengstrand.stateguard.example.book;

import nu.tengstrand.stateguard.example.book.attributes.BookBinding;

public class BookCreator extends BookStateGuard {
    BookCreator() {
    }

    public class Title {
        public Binding title(String title) {
            BookCreator.this.title.setTitle(title);
            return new Binding();
        }
    }

    public class Binding {
        public Pages paperback() {
            BookCreator.this.binding.setBinding(BookBinding.PAPERBACK);
            return new Pages();
        }
        public Pages hardback() {
            BookCreator.this.binding.setBinding(BookBinding.HARDBACK);
            return new Pages();
        }
    }

    public class Pages {
        public BookCreator pages(int pages) {
            BookCreator.this.pages.setPages(pages);
            return BookCreator.this;
        }
    }
}
Implements a chained creator for the Book class:
  • row 5:
    inherits from the class BookStateGuard.
  • row 6:
    empty constructor with visibility package-private, to force the usage of Book.create().
  • row 9-14:
    the first argument and main entrance of the chained constructor, returns the next argument Binding.
  • row 16-25:
    the middle argument in the constructor, returns the next argument Pages.
  • row 27-32:
    the last argument in the constructor, returns an instance of the surrounding class BookCreator.
The three attributes that are included in BookStateGuard (title, binding and pages) inherits all from StateGuard. These classes follow the same pattern so lets have a look at one of them, BookTitleStateGuard:
package nu.tengstrand.stateguard.example.book.attributes;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.validator.NonEmptyString;

public class BookTitleStateGuard extends StateGuard<BookTitle> {
    private NonEmptyString title = NonEmptyString.attributeName("title").messageKey("missingvalue");

    public BookTitleStateGuard() {
        addValidator(title);
    }

    public void setTitle(String title) {
        this.title.withValue(title);
    }

    @Override
    protected BookTitle createValidState() {
        return new BookTitle(title.value());
    }
}
  • row 6:
    inherits from the class StateGuard and indicates that it is the class BookTitle we want to protect.
  • row 7:
    validator for the attribute title where NonEmptyString states that the value may not be null or empty string. The first argument "title" will appear in the error message. The second argument "messageKey" is optional and replaces the default message for the component NonEmptyString with the message missingvalue in the file validationMessages.properties, which in this case is: Missing value, please enter a value for''{0 }''.
  • row 10:
    the validator title is registered in the constructor.
  • row 13:
    is used by BookBuilder and BookCreator.
  • row 18:
    returns a BookTitle. The fact that we inherit from StateGuard, has registered title at line 10 and set this method to protected will guarantee that title is validated before this method is called with the result that we can be sure that title is never null or empty string.
With these words I’m wishing you good luck with this pattern!

Best Regards,
Joakim Tengstrand