Components and Validation

The previous section provided an extensive description of Krail’s I18N mechanism, and gave an example of using annotations to manage the captions of a Vaadin component. This section addresses the use of I18N annotations with Vaadin Components in more detail.

Preparation

Set up a page

We will build a new page:

  • in MyOtherPages add a new page entry ` addEntry(“i18n”, I18NDemoView.class, TutorialLabelKey.I18N, PageAccessControl.PUBLIC); `
  • in package ‘com.example.tutorial.pages’, create a new class ‘I18NDemoView’ extended from ViewBase
  • implement the doBuild() method
  • create the enum constant TutorialLabelKey.I18N
package com.example.tutorial.pages;

import com.google.inject.Inject;
import uk.q3c.krail.i18n.Translate;
import uk.q3c.krail.core.view.ViewBase;
import uk.q3c.krail.core.view.component.ViewChangeBusMessage;

public class I18NDemoView extends ViewBase {

    @Inject
    protected I18NDemoView(Translate translate) {
        super(translate);
    }

    @Override
    protected void doBuild(ViewChangeBusMessage busMessage) {

    }
}

Translations

  • add the following translations to TutorialLabels_de, creating keys where necessary
put(News, "Nachrichten");
put(Last_Name, "Nachname");
put(First_Name, "Vorname");
put(No, "Nein");
put(Yes, "Ja");
  • in the ‘com.example.tutorial.i18n’ package, create the ‘TutorialDescriptions’ class
package com.example.tutorial.i18n;

import uk.q3c.krail.i18n.EnumResourceBundle;

import static com.example.tutorial.i18n.TutorialDescriptionKey.*;

public class TutorialDescriptions extends EnumResourceBundle<TutorialDescriptionKey> {
    @Override
    protected void loadMap() {
        put(Interesting_Things, "Interesting things that have happened in the world.");
        put(Yes,"Press for Yes");
        put(No, "Press for No");
    }
}
  • also create the Descriptions_de class
package com.example.tutorial.i18n;

import static com.example.tutorial.i18n.TutorialDescriptionKey.*;

public class TutorialDescriptions_de extends TutorialDescriptions {
 @Override
 protected void loadMap() {
 put(Interesting_Things, "Interessante Dinge, die in der Welt haben geschehen");
 put(You_just_asked_for_a_pay_increase, "Sie haben für eine Lohnerhöhung gebeten");
 put(Yes, "Drücken Sie für Ja");
 put(No, "Drücken Sie für Nein");
 }
}

[source]
----

#Add different component types

The mix of components we will use should cover all the situations you will encounter - many of the components are treated the same way for I18N, so we do not need to use every available component.

----

package com.example.tutorial.pages;

import com.google.inject.Inject;
import com.vaadin.ui.*;
import uk.q3c.krail.core.view.ViewBase;
import uk.q3c.krail.core.view.component.ViewChangeBusMessage;
import uk.q3c.krail.i18n.Translate;

public class I18NDemoView extends ViewBase {
 private Grid grid;
 private Label label;
 private Table table;
 private TextField textField;

[source]
----
@Inject
protected I18NDemoView(Translate translate) {
    super(translate);
}

@Override
protected void doBuild(ViewChangeBusMessage busMessage) {
    textField = new TextField();
    label = new Label();
    table = new Table();
    grid = new Grid();
    VerticalLayout layout = new VerticalLayout(textField, label, table, grid);
    Panel panel = new Panel();
    panel.setContent(layout);
    setRootComponent(panel);
}
----

}

[source]
----
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">When you sub-class from ViewBase, make sure you set the root component in your doBuild() method</p>
</div>

- Add the same **@TutorialCaption** to each field:
----

@TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
`
- The result should be
`
package com.example.tutorial.pages;

import com.example.tutorial.i18n.TutorialCaption;
import com.example.tutorial.i18n.TutorialDescriptionKey;
import com.example.tutorial.i18n.TutorialLabelKey;
import com.google.inject.Inject;
import com.vaadin.ui.*;
import uk.q3c.krail.core.view.ViewBase;
import uk.q3c.krail.core.view.component.ViewChangeBusMessage;
import uk.q3c.krail.i18n.Translate;

public class I18NDemoView extends ViewBase {
 @TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
 private Grid grid;
 @TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
 private Label label;
 @TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
 private Table table;
 @TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
 private TextField textField;

[source]
----
@Inject
protected I18NDemoView(Translate translate) {
    super(translate);
}

@Override
protected void doBuild(ViewChangeBusMessage busMessage) {
    textField = new TextField();
    label = new Label();
    table = new Table();
    grid = new Grid();
    VerticalLayout layout = new VerticalLayout(textField, label, table, grid);
    Panel panel = new Panel();
    panel.setContent(layout);
    setRootComponent(panel);
}
----

}

[source]
----

- Run the application and go to the 'I18N' page
- All 4 components will be present, each with a caption of 'News' and a tooltip of 'Interesting things that have happened in the world.'
- Changing Locale with the Locale Selector changes the language
- but only the ```TextField``` looks complete

##Labels

Often with ```Label``` components you want to set the value of the component statically, which you can also do with an annotation.  Actually you can do that using Krail's I18N mechanism for any component which implements the ```com.vaadin.data.Property``` interface and accepts a ```String``` value.

We have a choice to make now.  Remember that:

1. The name of an I18N annotation does not matter, it just needs to be annotated with ```@I18NAnnotation```
1. The ```I18NAnnotationProcessor``` can handle multiple annotations on the same component
1. The annotation methods can be any combination of ```caption()```, ```description()```, ```value()``` or ```locale()```
1. We need to specify which ```I18NKey``` we use (that is, the enum class - Java will not allow an interface as a type)

We could:

1. Add the value() method to **@Caption**
1. We could create a **@Value** annotation with only the ```value()``` method
1. We could create a caption specifically for Labels

... and quite few more choices, too.  Remember, though, that you cannot specify a default value of **null** in an annotation, so if you want to have an annotation method that is often not used, the best way is to specify a "null key", which should probably return an empty ```String``` from ```Translate```

----

TutorialDescriptionKey value() default TutorialDescriptionKey.NULLKEY;

For the Tutorial, we will create a @TutorialValue annotation, which has only a value() method.

  • in the ‘com.example.tutorial.i18n’ package create a new annotation ‘Value’
  • we will use TutorialDescriptionKey for values, as they can be quite long
package com.example.tutorial.i18n;

import uk.q3c.krail.i18n.I18NAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@I18NAnnotation
public @interface TutorialValue {

    TutorialDescriptionKey value();
}
  • Add a @TutorialValue Annotation to the Label
@TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
@TutorialValue(value = TutorialDescriptionKey.You_just_asked_for_a_pay_increase)
private Label label;
[source]
----
- Run the application and go to the 'I18N' page
    - The ```Label``` now has a value.  Actually, we could have done the same with the ```TextField```, but that isn't usually what you would want.
    - Change the locale with the Locale Selector, and all the captions, tooltips & label value will change language

##Table

A ```Table``` has column headers which may need translation. If a ```Table``` propertyId is an ```I18NKey``` it will be translated - otherwise it is ignored by the Krail ```I18NProcessor```.

- add a 'setupTable' method to ```I18NDemoView```

----

private void setupTable() {
 table.addContainerProperty(TutorialLabelKey.First_Name, String.class, null);
 table.addContainerProperty(TutorialLabelKey.Last_Name, String.class, null);
 table.setHeight("100px");
 table.setWidth("200px");
}

Grid

In a very similar way to Table, Grid may need column headings translated. If a Grid propertyId is an I18NKey it will be translated - otherwise it is ignored by the Krail I18NProcessor.

  • add a ‘setupGrid()’ method
private void setupGrid(){
    grid.addColumn(TutorialLabelKey.First_Name, String.class);
    grid.addColumn(TutorialLabelKey.Last_Name, Integer.class);
}
  • call these setup methods from doBuild()
@Override
protected void doBuild(ViewChangeBusMessage busMessage) {
    textField = new TextField();
    label = new Label();
    table = new Table();
    grid = new Grid();
    setupTable();
    setupGrid();
    VerticalLayout layout = new VerticalLayout(textField, label, table, grid);
    Panel panel = new Panel();
    panel.setContent(layout);
    setRootComponent(panel);

}
  • Run the application and go to the I18N page
    • the Table and grid now have column headings
    • Change the locale with the Locale Selector, and all the captions, tooltips, column headings & label value will change language

Drilldown and Override

There is another scenario that Krail’s I18N processing supports. Assume you have a class which contains components with I18N annotations and you want to make it re-usable. Let’s see how that would work.

  • in the ‘com.example.tutorial.i18n’ package, create a new class ‘ButtonBar’, with @TutorialCaption on the buttons
  • annotate the class with @I18N - this tells the I18NProcessor to drill down into this class to look for more I18N annotations. This annotation can be applied to a field or a class, but for a re-usable component it makes more sense to put it on the class.
package com.example.tutorial.i18n;

import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Panel;
import uk.q3c.krail.core.i18n.I18N;

@TutorialCaption(caption = TutorialLabelKey.News, description = TutorialDescriptionKey.Interesting_Things)
@I18N
public class ButtonBar extends Panel {

    @TutorialCaption(caption = TutorialLabelKey.Yes, description = TutorialDescriptionKey.Yes)
    private Button yesButton;
    @TutorialCaption(caption = TutorialLabelKey.No, description = TutorialDescriptionKey.No)
    private Button noButton;

    public ButtonBar() {
        yesButton = new Button();
        noButton = new Button();
        HorizontalLayout layout = new HorizontalLayout(yesButton,noButton);
        this.setContent(layout);
    }
}
  • add two instances of this class to our I18NDemoView.doBuild(). Note that even when they are not directly nnotated, these still need to be fields (and not local variables) for the I18NProcessor to find the class annotations.
  • include them in the layout
@Override
protected void doBuild(ViewChangeBusMessage busMessage) {
textField = new TextField();
label = new Label();
table = new Table();
grid = new Grid();
buttonBar1 = new ButtonBar();
buttonBar2 = new ButtonBar();
setupTable();
setupGrid();
VerticalLayout layout = new VerticalLayout(buttonBar1,buttonBar2, textField, label, table, grid);
Panel panel = new Panel();
panel.setContent(layout);
setRootComponent(panel);
}
[source]
----
- on the buttonBar1 field, annotate with a different **@TutorialCaption**

----

@TutorialCaption(caption = TutorialLabelKey.CEO_News_Channel,description = TutorialDescriptionKey.Interesting_Things)
private ButtonBar buttonBar1;
  • Run the application and the two button bars will be at the top of the page
  • button bar 1 displays the caption you set at field level (overriding the class annotations)
  • button bar 2 displays the caption set at class level

You could also override the drilldown specified by the ButtonBar class, simply by annotating the field with @I18N(drilldown=false) - although we cannot think why you might want to do that !

Form

Vaadin replaced its original Form with a BeanFieldGroup, which is essentially a form without the layout. Krail replaces that with its own BeanFieldGroupBase, which also provides integration with Krail’s I18N.

To demonstrate this we need to create an entity.

  • create a new package ‘com.example.tutorial.form’
  • in this new package create a class ‘Person’, and include some familiar javax validation annotations, @Min and @Size
package com.example.tutorial.form;

import uk.q3c.krail.persist.KrailEntity;

import javax.persistence.Id;
import javax.persistence.Version;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;

public class Person implements KrailEntity<Long,Integer> {

    @Min(0) @Max(150)
    private int age;
    @Size(min = 3)
    private String firstName;
    @Id
    private Long id;

    @Size(min=3)
    private String lastName;
    @Version
    private Integer version;

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public String getFirstName() {
        return firstName;
    }

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public Integer getVersion() {
        return version;
    }

    public String getLastName() {
        return lastName;
    }
}
  • Modify build.gradle to include javax.persistence - we have not yet introduced persistence, but we need the API for the entity
  • Depending on the IDE you are using, you may need to refresh Gradle
dependencies {
    // remember to update the Vaadin version below if this version is changed
    compile(group: 'uk.q3c.krail', name: 'krail', version: '0.10.0.0')
    compile 'javax.persistence:persistence-api:1.0.2'
}
  • in package ‘com.example.tutorial.form’, create ‘PersonForm’ and create the enum constatns as required
package com.example.tutorial.form;

import com.example.tutorial.i18n.TutorialCaption;
import com.example.tutorial.i18n.TutorialDescriptionKey;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.vaadin.data.Property;
import com.vaadin.ui.Button;
import com.vaadin.ui.Panel;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;
import uk.q3c.krail.core.i18n.I18N;
import uk.q3c.krail.core.i18n.I18NProcessor;
import uk.q3c.krail.option.Option;
import uk.q3c.krail.core.ui.form.BeanFieldGroupBase;
import uk.q3c.krail.core.validation.BeanValidator;

import static com.example.tutorial.i18n.TutorialLabelKey.*;

@I18N
public class PersonForm extends BeanFieldGroupBase<Person> {
    @TutorialCaption(caption = Submit, description = TutorialDescriptionKey.Submit)
    private final Button submitButton;
    private final Person person;
    @TutorialCaption(caption = First_Name, description = TutorialDescriptionKey.Enter_your_first_name)
    private TextField firstName;

    @TutorialCaption(caption = Last_Name, description = TutorialDescriptionKey.Enter_your_last_name)
    private TextField lastName;
    @TutorialCaption(caption = Age, description = TutorialDescriptionKey.Age_of_the_Person)
    private TextField age;
    @TutorialCaption(caption = Person_Form, description = TutorialDescriptionKey.Person_Details_Form)
    private Panel layout;

    @Inject
    public PersonForm(I18NProcessor i18NProcessor, Provider<BeanValidator> beanValidatorProvider, Option option) {
        super(i18NProcessor, beanValidatorProvider, option);
        firstName = new TextField();
        lastName = new TextField();
        age = new TextField();

        person = new Person();
        person.setAge(44);
        person.setFirstName("Mango");
        person.setLastName("Chutney");
        submitButton = new Button();
        submitButton.addClickListener(event -> {
            try {
                this.commit();
            } catch (CommitException e) {
                e.printStackTrace();
            }
        });
        layout = new Panel(new VerticalLayout(firstName, lastName, age, submitButton));
        layout.setStyleName(ValoTheme.PANEL_WELL);
        setBean(person);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void optionValueChanged(Property.ValueChangeEvent event) {

    }

    public Panel getLayout() {
        return this.layout;
    }
}

About the form

The class simply extends BeanFieldGroupBase , with the required entity type as a generic parameter - in this case, Person. Like its Vaadin counterpart, BeanFieldGroupBase does not concern itself with the presentation of data, or the layout of that presentation. That is the part we must provide.

You will recognise the fields and captions from the earlier part of this Tutorial section - they are just Vaadin components with @TutorialCaption annotations. However, it should be noted that the names of the components must match the field names of the entity to enable automatic transfer of data between the presentation layer and data model.

The constructor simply extends BeanFieldGroupBase and your IDE will probably auto-complete the necessary parameters. Don’t forget the @Inject annotation though.

Within the constructor we simply build the presentation components, and define the submit button to invoke the commit() method, which will transfer data from the presentation layer back to the model - in this case the person bean.

Finally, the getLayout() method just enables a consumer class to identify the base component to place within a View.

There is an open ticket to provide more support for Forms.

  • Now we need to use the form, by injecting it in to I18NDemoView
@Inject
protected I18NDemoView(Translate translate, PersonForm personForm) {
    super(translate);
    this.personForm = personForm;
}

*

and add it to the layout in doBuild(): ` VerticalLayout layout = new VerticalLayout(personForm.getLayout(), buttonBar1, buttonBar2, textField, label, table, grid); `

*

Run the application, and navigate to the I18N page

  • The form will display at the top of the page with the values we have set
  • change a value which breaks validation (for example, age = 443), and a validation message will appear
  • change language with the Locale selector, and the language of the captions etc will change, including the validation message.

There is a more information about the Apache Bval validation integration in the Developer Guide

Summary

In this section we have:

  • created and used I18N @TutorialCaption and @TutorialValue annotations
  • seen how to manage Table and Grid column names for I18N
  • created a re-usable I18N enabled component
  • seen how to override a class I18N annotation
  • created a form, with I18N integrated validation

Download from GitHub

To get to this point straight from GitHub:

git clone https://github.com/davidsowerby/krail-tutorial.git
cd krail-tutorial
git checkout --track origin/krail_0.10.0.0

Revert to commit I18N Components and Validation Complete