3. Write the Model Code

3.1. Code the enumerations

Simple enumerations, like BookCategoryEL and PublicationFormEL, are coded in the following way with the help of Java's enum construct:

public enum BookCategoryEL {
  NOVEL, 
  BIOGRAPHY, 
  TEXTBOOK, 
  OTHER;
}

Notice how the enumeration literals are defined with capitalized names in a comma-separated list.

For a code list, like LanguageEL, where each enumeration instance consists of a code and a label, we use the codes as enumeration literals and define an attribute label for storing the labels, as well as a private constructor that allows creating enumeration instances consisting of a code and a label. This is possible because in Java, an enum is a special kind of class. Each entry in the list of enumeration literals, auch as EN("English"), represents an invocation of the enum's constructor:

public enum LanguageEL {
  EN( "English"),
  DE( "German"),
  FR( "French"),
  ES( "Spanish");

  private final String label;
  private LanguageEL( String label) {this.label = label;}
  public String getLabel() {return this.label;}
}

3.2. Code the JPA entity class

The entity class Book is coded with the help of JPA annotations for class attributes, as well as the setter and getter methods of every class attribute.

@Entity @Table( name="books")
@ViewScoped @ManagedBean( name="book")
public class Book {
  @Id @Column( length=10)
  @NotNull( message="An ISBN value is required!")
  @Pattern( regexp="\\b\\d{9}(\\d|X)\\b", message = "...")
  private String isbn;
  
  @Column( nullable=false)
  @NotNull( message="A title is required!")
  private String title;

  @Column( nullable=false)
  @NotNull( message="An original language is required!")
  @Enumerated( EnumType.STRING)
  private LanguageEL originalLanguage;

  @Column( nullable=false)
  @NotNull( message="A category is required!")
  @Enumerated( EnumType.STRING)
  private BookCategoryEL category;

  @Convert( converter = 
      pl.m.converter.OtherAvailableLanguagesConverter.class)
  private Set<LanguageEL> otherAvailableLanguages;
 
  @Column( nullable=false)
  @Size( min=1, message="At least one publication form is required!")
  @Convert( converter=pl.m.converter.PublicationFormsConverter.class)
  private Set<PublicationFormEL> publicationForms;

  ...
}

For the case of single-valued enumeration attributes (like originalLanguage and category), the JPA annotation @Enumerated is used for specifying the storage serialization. It takes one of the following two parameter values:

  • EnumType.STRING means that enumeration literals are converted to strings when they are serialized ( e.g., in the case of the originalLanguage attribute of the Book class, the values saved in the database are one of "EN", "DE", "FR" or "ES") ;

  • EnumType.ORDINAL means that enumeration literals are converted to their index integer when they are serialized (i.e.,the values saved in the database are 1, 2 ,3 etc.).

We store the collection values of a multi-valued enumeration attribute (like otherAvailableLanguages and publicationForms) in a database table column as serialized arrays in the form of ["value1", "value2", ...]. In the case of the originalLanguage attribute, an example of a saved value would be ["EN", "FR", "DE"]. Achieving this behavior is possible with JPA by using custom converters to map database table column values to Java types and vice versa. The @Convert annotation allows specifying a Java converter that is responsible for the mappings.

Also, in the case of a multi-valued enumeration attribute, we use the @Size annotation to specify the minimum, and if required also the maximum, number of elements stored by this attribute. For example, in the case of publicationForms, we use @Size( min=1, message = "At least one publication form is required!"), which enforces to have at least one value for this attribute, otherwise the error message is displayed.

@Entity @Table( name="books")
@ViewScoped @ManagedBean( name="book")
public class Book {
  ...
  public Book( String isbn, String title, Integer year, 
      LanguageEL originalLanguage, 
      Set<LanguageEL> otherAvailableLanguages, 
      BookCategoryEL category, 
      Set<PublicationFormEL> publicationForms) { ...}
  public LanguageEL getOriginalLanguage() {...}
  public void setOriginalLanguage( 
      LanguageEL originalLanguage) {...}  
  public Set<LanguageEL> getOtherAvailableLanguages() {...}
  public void setOtherAvailableLanguages( 
      Set<LanguageEL> otherAvailableLanguages) {...}
  public BookCategoryEL getCategory() {...}
  public void setCategory( BookCategoryEL category) {...}  
  public Set<PublicationFormEL> getPublicationForms() {...}
  public void setPublicationForms( 
      Set<PublicationFormEL> publicationForms) {...}

  public static Book retrieve(...) {...}
  public static List<Book> retrieveAll(...) {...}
  public static void create(...) throws Exception {...}
  public static void update(...) throws Exception {...}
  public static void delete(...) throws Exception {...}

  public String getOtherAvailableLanguagesValues() {...}
  public SelectItem[] getLanguageItems() {...}
  public SelectItem[] getCategoryItems() {...}
  public String getPublicationFormsValues() {...}
  public SelectItem[] getPublicationFormsItems() {...}
}

Notice the new methods in the last code block, like getPublicationFormsValues and getPublicationFormsItems, which are used for handling the enumeration attributes in the UI. We discuss each of them in the following subsections.

3.3. Define a converter for serializing enumeration attribute values

A JPA attribute converter is a special Java class that implements the AttributeConverter interface with the methods convertToDatabaseColumn and convertToEntityAttribute. For the otherAvailableLanguages attribute, we define the following converter class:

@Converter
public class OtherAvailableLanguagesConverter implements 
    AttributeConverter< Set<LanguageEL>, String> {
  @Override
  public String convertToDatabaseColumn( Set<LanguageEL> attrValue) {
    JSONArray jsonArr = new JSONArray( attrValue);
    return jsonArr.toString();
  }
  @Override
  public Set<LanguageEL> convertToEntityAttribute( String colValue) {
    Set<LanguageEL> result = new HashSet<LanguageEL>();
    try {
      JSONArray jsonArr = new JSONArray( colValue);
        for (int i = 0, length = jsonArr.length(); i < length; i++) {
          result.add( LanguageEL.valueOf( jsonArr.getString( i)));
        }
    } catch (JSONException e) {
      e.printStackTrace();
    }
    return result;
  }
}

An attribute converter class needs to be annotated with @Converter. In our example, the convertToDatabaseColumn method is responsible to convert the entity attribute value (e.g., otherAvailableLanguages) to a JSON array which is stored in the database as a String.

The convertToEntityAttribute method is responsible for de-serializing a table column's value to the corresponding entity attribute value. In our example, this means to map the JSON array string to a Java Set of enumeration literals of type LanguageEL.

The code above shows the custom attribute converter class for the otherAvailableLanguages attribute. The attribute converter class for the publicationForms attribute is defined in the same way.

3.4. Code the enumeration attribute setters

Both for single-valued and for multi-valued enumeration attributes an ordinary setter is defined. In the case of a multi-valued enumeration attribute, this setter assigns an entire set of values (in the form of a Java Set) to the attribute.

3.5. Write a serialization function

The object serialization function now needs to include the values of enumeration attributes:

public String toString() {
  int i = 0, n = this.publicationForms.size();
  String result = "{ isbn: '" + this.isbn + "', title:'" + this.title
    + "', year: " + this.year + ", originalLanguage: '" 
    + this.originalLanguage.getLabel();
  result += "', otherAvailableLanguages: [";
  for (LanguageEL oal : this.otherAvailableLanguages) {
    result += "'" + oal.getLabel() + "'";
    if (i < n-1) result += ", ";
    i++;
  }
  result += "]";
  result += ", category: '" + this.category.name().toLowerCase();
  result += "', publicationForms: [";
  i = 0;
  for (PublicationFormEL pf : this.publicationForms) {
    result += "'" + pf.name().toLowerCase() + "'";
    if (i < n-1) result += ", ";
    i++;
  }
  result += "]}";
  return result;
}

3.6. Database schema

As explained in Chapter 4, the database schema can be automatically generated by a Java EE server like TomEE. The generated schema for our Book entity class is like so:

CREATE TABLE IF NOT EXISTS `books` (
  `ISBN` varchar(10) NOT NULL,
  `CATEGORY` varchar(255) NOT NULL,
  `ORIGINALLANGUAGE` varchar(255) NOT NULL,
  `OTHERAVAILABLELANGUAGES` varchar(255) DEFAULT NULL,
  `PUBLICATIONFORMS` varchar(255) NOT NULL,
  `TITLE` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

For every attribute (like category), a column with the same name but using upper case is created, (e.g., CATEGORY). This is the default naming, which is fine for our example application, but it can be changed, if required, by using the @Column( name="...") annotation, as in @Column( name="book_category").

The single-valued and multi-valued enumeration attributes are by default created as varchar columns with the default maximum length of 255 characters. However, if a longer (or shorter) length is desirable, this can be enforced by using the @Column( length=...) annotation.

3.7. Creating test data

In the test data objects that are created by Book.createTestData, we now have to provide values for single- and multi-valued enumeration attributes:

public static void createTestData( 
    EntityManager em, UserTransaction ut)
    throws NotSupportedException, ..., RollbackException {
  Book book = null;
  // first clear existing data
  Book.clearData( em, ut);
  ut.begin();
  Book book = new Book( "006251587X", "Weaving the Web", 2000, 
      LanguageEL.EN,
      new HashSet<LanguageEL>() {
        { add( LanguageEL.DE); add( LanguageEL.FR);}
      }, 
      BookCategoryEL.TEXTBOOK, 
      new HashSet<PublicationFormEL>() {
        { add( PublicationFormEL.HARDCOVER);
          add( PublicationFormEL.PDF);}
      }
  );
  em.persist( book);
}