@@ -0,0 +1,790 @@ | |||
[[adding-jpa-to-the-address-book-demo]] | |||
Adding JPA to the address book demo | |||
----------------------------------- | |||
Petter Holmström | |||
[[introduction]] | |||
Introduction | |||
~~~~~~~~~~~~ | |||
The https://github.com/vaadin/addressbook/tree/v7[Vaading address book] tutorial (the one | |||
hour version, that is) does a very good job introducing the different | |||
parts of Vaadin. However, it only uses an in-memory data source with | |||
randomly generated data. This may be sufficient for demonstration | |||
purposes, but not for any real world applications that manage data. | |||
Therefore, in this article, we are going to replace the tutorial's | |||
in-memory data source with the Java Persistence API (JPA) and also | |||
utilize some of the new JEE 6 features of | |||
https://glassfish.dev.java.net/[GlassFish] 3. | |||
[[prerequisites]] | |||
Prerequisites | |||
^^^^^^^^^^^^^ | |||
In order to fully understand this article, you should be familiar with | |||
JEE and JPA development and you should also have read through the Vaadin | |||
tutorial. | |||
If you want to try out the code in this article you should get the | |||
latest version of GlassFish 3 (build 67 was used for this article) and | |||
http://ant.apache.org[Apache Ant 1.7]. You also need to download the | |||
https://github.com/eriklumme/doc-attachments/blob/master/attachments/addressbook.tar.gz[source code]. *Note, that you have to edit the | |||
_build.xml_ file to point to the correct location of the GlassFish | |||
installation directory before you can use it!* | |||
[[the-system-architecture]] | |||
The System Architecture | |||
~~~~~~~~~~~~~~~~~~~~~~~ | |||
The architecture of the application is presented in the following | |||
diagram: | |||
image:img/architecture2.png[System architecture diagram] | |||
In addition to the Vaadin UI created in the tutorial, we will add a | |||
stateless Enterprise Java Bean (EJB) to act as a facade to the database. | |||
The EJB will in turn use JPA to communicate with a JDBC data source (in | |||
this example, the built-in `jdbc/sample` data source). | |||
[[refactoring-the-domain-model]] | |||
Refactoring the Domain Model | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Before doing anything else, we have to modify the domain model of the | |||
Address Book example. | |||
[[the-person-class]] | |||
The Person class | |||
^^^^^^^^^^^^^^^^ | |||
In order to use JPA, we have to add JPA annotations to the `Person` | |||
class: | |||
[source,java] | |||
.... | |||
// Imports omitted | |||
@Entity | |||
public class Person implements Serializable { | |||
@Id | |||
@GeneratedValue(strategy = GenerationType.IDENTITY) | |||
private Long id; | |||
@Version | |||
@Column(name = "OPTLOCK") | |||
private Long version; | |||
private String firstName = ""; | |||
private String lastName = ""; | |||
private String email = ""; | |||
private String phoneNumber = ""; | |||
private String streetAddress = ""; | |||
private Integer postalCode = null; | |||
private String city = ""; | |||
public Long getId() { | |||
return id; | |||
} | |||
public Long getVersion() { | |||
return version; | |||
} | |||
// The rest of the methods omitted | |||
} | |||
.... | |||
As we do not need to fit the domain model onto an existing database, the | |||
annotations become very simple. We have only marked the class as being | |||
an entity and added an ID and a version field. | |||
[[the-personreference-class]] | |||
The PersonReference class | |||
^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
There are many advantages with using JPA or any other Object Persistence | |||
Framework (OPF). The underlying database gets completely abstracted away | |||
and we can work with the domain objects themselves instead of query | |||
results and records. We can detach domain objects, send them to a client | |||
using a remote invocation protocol, then reattach them again. | |||
However, there are a few use cases where using an OPF is not such a good | |||
idea: reporting and listing. When a report is generated or a list of | |||
entities is presented to the user, normally only a small part of the | |||
data is actually required. When the number of objects to fetch is large | |||
and the domain model is complex, constructing the object graphs from the | |||
database can be a very lengthy process that puts the users' patience to | |||
the test – especially if they are only trying to select a person's name | |||
from a list. | |||
Many OPFs support lazy loading of some form, where references and | |||
collections are fetched on demand. However, this rarely works outside | |||
the container, e.g. on the other side of a remoting connection. | |||
One way of working around this problem is to let reports and lists | |||
access the database directly using SQL. This is a fast approach, but it | |||
also couples the code to a particular SQL dialect and therefore to a | |||
particular database vendor. | |||
In this article, we are going to select the road in the middle – we will | |||
only fetch the property values we need instead of the entire object, but | |||
we will use PQL and JPA to do so. In this example, this is a slight | |||
overkill as we have a very simple domain model. However, we do this for | |||
two reasons: Firstly, as Vaadin is used extensively in business | |||
applications where the domain models are complex, we want to introduce | |||
this pattern in an early stage. Secondly, it makes it easier to plug | |||
into Vaadin's data model. | |||
In order to implement this pattern, we need to introduce a new class, | |||
namely `PersonReference`: | |||
[source,java] | |||
.... | |||
import com.vaadin.data.Item; | |||
import com.vaadin.data.Property; | |||
import com.vaadin.data.util.ObjectProperty; | |||
// Some imports omitted | |||
public class PersonReference implements Serializable, Item { | |||
private Long personId; | |||
private Map<Object, Property> propertyMap; | |||
public PersonReference(Long personId, Map<String, Object> propertyMap) { | |||
this.personId = personId; | |||
this.propertyMap = new HashMap<Object, Property>(); | |||
for (Map.Entry<Object, Property> entry : propertyMap.entrySet()) { | |||
this.propertyMap.put(entry.getKey(), new ObjectProperty(entry.getValue())); | |||
} | |||
} | |||
public Long getPersonId() { | |||
return personId; | |||
} | |||
public Property getItemProperty(Object id) { | |||
return propertyMap.get(id); | |||
} | |||
public Collection<?> getItemPropertyIds() { | |||
return Collections.unmodifiableSet(propertyMap.keySet()); | |||
} | |||
public boolean addItemProperty(Object id, Property property) { | |||
throw new UnsupportedOperationException("Item is read-only."); | |||
} | |||
public boolean removeItemProperty(Object id) { | |||
throw new UnsupportedOperationException("Item is read-only."); | |||
} | |||
} | |||
.... | |||
The class contains the ID of the actual `Person` object and a `Map` of | |||
property values. It also implements the `com.vaadin.data.Item` | |||
interface, which makes it directly usable in Vaadin's data containers. | |||
[[the-querymetadata-class]] | |||
The QueryMetaData class | |||
^^^^^^^^^^^^^^^^^^^^^^^ | |||
Before moving on to the EJB, we have to introduce yet another class, | |||
namely `QueryMetaData`: | |||
[source,java] | |||
.... | |||
// Imports omitted | |||
public class QueryMetaData implements Serializable { | |||
private boolean[] ascending; | |||
private String[] orderBy; | |||
private String searchTerm; | |||
private String propertyName; | |||
public QueryMetaData(String propertyName, String searchTerm, String[] orderBy, boolean[] ascending) { | |||
this.propertyName = propertyName; | |||
this.searchTerm = searchTerm; | |||
this.ascending = ascending; | |||
this.orderBy = orderBy; | |||
} | |||
public QueryMetaData(String[] orderBy, boolean[] ascending) { | |||
this(null, null, orderBy, ascending); | |||
} | |||
public boolean[] getAscending() { | |||
return ascending; | |||
} | |||
public String[] getOrderBy() { | |||
return orderBy; | |||
} | |||
public String getSearchTerm() { | |||
return searchTerm; | |||
} | |||
public String getPropertyName() { | |||
return propertyName; | |||
} | |||
} | |||
.... | |||
As the class name suggests, this class contains query meta data such as | |||
ordering and filtering information. We are going to look at how it is | |||
used in the next section. | |||
[[the-stateless-ejb]] | |||
The Stateless EJB | |||
~~~~~~~~~~~~~~~~~ | |||
We are now ready to begin designing the EJB. As of JEE 6, an EJB is no | |||
longer required to have an interface. However, as it is a good idea to | |||
use interfaces at the boundaries of system components, we will create | |||
one nonetheless: | |||
[source,java] | |||
.... | |||
// Imports omitted | |||
@TransactionAttribute | |||
@Local | |||
public interface PersonManager { | |||
public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames); | |||
public Person getPerson(Long id); | |||
public Person savePerson(Person person); | |||
} | |||
.... | |||
Please note the `@TransactionAttribute` and `@Local` annotations that | |||
instruct GlassFish to use container managed transaction handling, and to | |||
use local references, respectively. Next, we create the implementation: | |||
[source,java] | |||
.... | |||
// Imports omitted | |||
@Stateless | |||
public class PersonManagerBean implements PersonManager { | |||
@PersistenceContext | |||
protected EntityManager entityManager; | |||
public Person getPerson(Long id) { | |||
// Implementation omitted | |||
} | |||
public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames) { | |||
// Implementation omitted | |||
} | |||
public Person savePerson(Person person) { | |||
// Implementation omitted | |||
} | |||
} | |||
.... | |||
We use the `@Stateless` annotation to mark the implementation as a | |||
stateless session EJB. We also use the `@PersistenceContext` annotation | |||
to instruct the container to automatically inject the entity manager | |||
dependency. Thus, we do not have to do any lookups using e.g. JNDI. | |||
Now we can move on to the method implementations. | |||
[source,java] | |||
.... | |||
public Person getPerson(Long id) { | |||
return entityManager.find(Person.class, id); | |||
} | |||
.... | |||
This implementation is very straight-forward: given the unique ID, we | |||
ask the entity manager to look up the corresponding `Person` instance | |||
and return it. If no such instance is found, `null` is returned. | |||
[source,java] | |||
.... | |||
public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames) { | |||
StringBuffer pqlBuf = new StringBuffer(); | |||
pqlBuf.append("SELECT p.id"); | |||
for (int i = 0; i < propertyNames.length; i++) { | |||
pqlBuf.append(","); | |||
pqlBuf.append("p."); | |||
pqlBuf.append(propertyNames[i]); | |||
} | |||
pqlBuf.append(" FROM Person p"); | |||
if (queryMetaData.getPropertyName() != null) { | |||
pqlBuf.append(" WHERE p."); | |||
pqlBuf.append(queryMetaData.getPropertyName()); | |||
if (queryMetaData.getSearchTerm() == null) { | |||
pqlBuf.append(" IS NULL"); | |||
} else { | |||
pqlBuf.append(" = :searchTerm"); | |||
} | |||
} | |||
if (queryMetaData != null && queryMetaData.getAscending().length > 0) { | |||
pqlBuf.append(" ORDER BY "); | |||
for (int i = 0; i < queryMetaData.getAscending().length; i++) { | |||
if (i > 0) { | |||
pqlBuf.append(","); | |||
} | |||
pqlBuf.append("p."); | |||
pqlBuf.append(queryMetaData.getOrderBy()[i]); | |||
if (!queryMetaData.getAscending()[i]) { | |||
pqlBuf.append(" DESC"); | |||
} | |||
} | |||
} | |||
String pql = pqlBuf.toString(); | |||
Query query = entityManager.createQuery(pql); | |||
if (queryMetaData.getPropertyName() != null && queryMetaData.getSearchTerm() != null) { | |||
query.setParameter("searchTerm", queryMetaData.getSearchTerm()); | |||
} | |||
List<Object[]> result = query.getResultList(); | |||
List<PersonReference> referenceList = new ArrayList<PersonReference>(result.size()); | |||
HashMap<String, Object> valueMap; | |||
for (Object[] row : result) { | |||
valueMap = new HashMap<String, Object>(); | |||
for (int i = 1; i < row.length; i++) { | |||
valueMap.put(propertyNames[i - 1], row[i]); | |||
} | |||
referenceList.add(new PersonReference((Long) row[0], valueMap)); | |||
} | |||
return referenceList; | |||
} | |||
.... | |||
This method is a little more complicated and also demonstrates the usage | |||
of the `QueryMetaData` class. What this method does is that it | |||
constructs a PQL query that fetches the values of the properties | |||
provided in the `propertyNames` array from the database. It then uses | |||
the `QueryMetaData` instance to add information about ordering and | |||
filtering. Finally, it executes the query and returns the result as a | |||
list of `PersonReference` instances. | |||
The advantage with using `QueryMetaData` is that additional query | |||
options can be added without having to change the interface. We could | |||
e.g. create a subclass named `AdvancedQueryMetaData` with information | |||
about wildcards, result size limitations, etc. | |||
[source,java] | |||
.... | |||
public Person savePerson(Person person) { | |||
if (person.getId() == null) | |||
entityManager.persist(person); | |||
else | |||
entityManager.merge(person); | |||
return person; | |||
} | |||
.... | |||
This method checks if `person` is persistent or transient, merges or | |||
persists it, respectively, and finally returns it. The reason why | |||
`person` is returned is that this makes the method usable for remote | |||
method calls. However, as this example does not need any remoting, we | |||
are not going to discuss this matter any further in this article. | |||
[[plugging-into-the-ui]] | |||
Plugging Into the UI | |||
~~~~~~~~~~~~~~~~~~~~ | |||
The persistence component of our Address Book application is now | |||
completed. Now we just have to plug it into the existing user interface | |||
component. In this article, we are only going to look at some of the | |||
changes that have to be made to the code. That is, if you try to deploy | |||
the application with the changes presented in this article only, it will | |||
not work. For all the changes, please check the source code archive | |||
attached to this article. | |||
[[creating-a-new-container]] | |||
Creating a New Container | |||
^^^^^^^^^^^^^^^^^^^^^^^^ | |||
First of all, we have to create a Vaadin container that knows how to | |||
read data from a `PersonManager`: | |||
[source,java] | |||
.... | |||
// Imports omitted | |||
public class PersonReferenceContainer implements Container, Container.ItemSetChangeNotifier { | |||
public static final Object[] NATURAL_COL_ORDER = new String[] {"firstName", "lastName", "email", | |||
"phoneNumber", "streetAddress", "postalCode", "city"}; | |||
protected static final Collection<Object> NATURAL_COL_ORDER_COLL = Collections.unmodifiableList( | |||
Arrays.asList(NATURAL_COL_ORDER) | |||
); | |||
protected final PersonManager personManager; | |||
protected List<PersonReference> personReferences; | |||
protected Map<Object, PersonReference> idIndex; | |||
public static QueryMetaData defaultQueryMetaData = new QueryMetaData( | |||
new String[]{"firstName", "lastName"}, new boolean[]{true, true}); | |||
protected QueryMetaData queryMetaData = defaultQueryMetaData; | |||
// Some fields omitted | |||
public PersonReferenceContainer(PersonManager personManager) { | |||
this.personManager = personManager; | |||
} | |||
public void refresh() { | |||
refresh(queryMetaData); | |||
} | |||
public void refresh(QueryMetaData queryMetaData) { | |||
this.queryMetaData = queryMetaData; | |||
personReferences = personManager.getPersonReferences(queryMetaData, (String[]) NATURAL_COL_ORDER); | |||
idIndex = new HashMap<Object, PersonReference>(personReferences.size()); | |||
for (PersonReference pf : personReferences) { | |||
idIndex.put(pf.getPersonId(), pf); | |||
} | |||
notifyListeners(); | |||
} | |||
public QueryMetaData getQueryMetaData() { | |||
return queryMetaData; | |||
} | |||
public void close() { | |||
if (personReferences != null) { | |||
personReferences.clear(); | |||
personReferences = null; | |||
} | |||
} | |||
public boolean isOpen() { | |||
return personReferences != null; | |||
} | |||
public int size() { | |||
return personReferences == null ? 0 : personReferences.size(); | |||
} | |||
public Item getItem(Object itemId) { | |||
return idIndex.get(itemId); | |||
} | |||
public Collection<?> getContainerPropertyIds() { | |||
return NATURAL_COL_ORDER_COLL; | |||
} | |||
public Collection<?> getItemIds() { | |||
return Collections.unmodifiableSet(idIndex.keySet()); | |||
} | |||
public List<PersonReference> getItems() { | |||
return Collections.unmodifiableList(personReferences); | |||
} | |||
public Property getContainerProperty(Object itemId, Object propertyId) { | |||
Item item = idIndex.get(itemId); | |||
if (item != null) { | |||
return item.getItemProperty(propertyId); | |||
} | |||
return null; | |||
} | |||
public Class<?> getType(Object propertyId) { | |||
try { | |||
PropertyDescriptor pd = new PropertyDescriptor((String) propertyId, Person.class); | |||
return pd.getPropertyType(); | |||
} catch (Exception e) { | |||
return null; | |||
} | |||
} | |||
public boolean containsId(Object itemId) { | |||
return idIndex.containsKey(itemId); | |||
} | |||
// Unsupported methods omitted | |||
// addListener(..) and removeListener(..) omitted | |||
protected void notifyListeners() { | |||
ArrayList<ItemSetChangeListener> cl = (ArrayList<ItemSetChangeListener>) listeners.clone(); | |||
ItemSetChangeEvent event = new ItemSetChangeEvent() { | |||
public Container getContainer() { | |||
return PersonReferenceContainer.this; | |||
} | |||
}; | |||
for (ItemSetChangeListener listener : cl) { | |||
listener.containerItemSetChange(event); | |||
} | |||
} | |||
} | |||
.... | |||
Upon creation, this container is empty. When one of the `refresh(..)` | |||
methods is called, a list of `PersonReference`s are fetched from the | |||
`PersonManager` and cached locally. Even though the database is updated, | |||
e.g. by another user, the container contents will not change before the | |||
next call to `refresh(..)`. | |||
To keep things simple, the container is read only, meaning that all | |||
methods that are designed to alter the contents of the container throw | |||
an exception. Sorting, optimization and lazy loading has also been left | |||
out (if you like, you can try to implement these yourself). | |||
[[modifying-the-personform-class]] | |||
Modifying the PersonForm class | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
We now have to refactor the code to use our new container, starting with | |||
the `PersonForm` class. We begin with the part of the constructor that | |||
creates a list of all the cities currently in the container: | |||
[source,java] | |||
.... | |||
PersonReferenceContainer ds = app.getDataSource(); | |||
for (PersonReference pf : ds.getItems()) { | |||
String city = (String) pf.getItemProperty("city").getValue(); | |||
cities.addItem(city); | |||
} | |||
.... | |||
We have changed the code to iterate a collection of `PersonReference` | |||
instances instead of `Person` instances. | |||
Then, we will continue with the part of the `buttonClick(..)` method | |||
that saves the contact: | |||
[source,java] | |||
.... | |||
if (source == save) { | |||
if (!isValid()) { | |||
return; | |||
} | |||
commit(); | |||
person = app.getPersonManager().savePerson(person); | |||
setItemDataSource(new BeanItem(person)); | |||
newContactMode = false; | |||
app.getDataSource().refresh(); | |||
setReadOnly(true); | |||
} | |||
.... | |||
The code has actually become simpler, as the same method is used to save | |||
both new and existing contacts. When the contact is saved, the container | |||
is refreshed so that the new information is displayed in the table. | |||
Finally, we will add a new method, `editContact(..)` for displaying and | |||
editing existing contacts: | |||
[source,java] | |||
.... | |||
public void editContact(Person person) { | |||
this.person = person; | |||
setItemDataSource(new BeanItem(person)) | |||
newContactMode = false; | |||
setReadOnly(true); | |||
} | |||
.... | |||
This method is almost equal to `addContact()` but uses an existing | |||
`Person` instance instead of a newly created one. It also makes the form | |||
read only, as the user is expected to click an Edit button to make the | |||
form editable. | |||
[[modifying-the-addressbookapplication-class]] | |||
Modifying the AddressBookApplication class | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Finally, we are going to replace the old container with the new one in | |||
the main application class. We will start by adding a constructor: | |||
[source,java] | |||
.... | |||
public AddressBookApplication(PersonManager personManager) { | |||
this.personManager = personManager; | |||
} | |||
.... | |||
This constructor will be used by a custom application servlet to inject | |||
a reference to the `PersonManager` EJB. When this is done, we move on to | |||
the `init()` method: | |||
[source,java] | |||
.... | |||
public void init() { | |||
dataSource = new PersonReferenceContainer(personManager); | |||
dataSource.refresh(); // Load initial data | |||
buildMainLayout(); | |||
setMainComponent(getListView()); | |||
} | |||
.... | |||
The method creates a container and refreshes it in order to load the | |||
existing data from the database – otherwise, the user would be presented | |||
with an empty table upon application startup. | |||
Next, we modify the code that is used to select contacts: | |||
[source,java] | |||
.... | |||
public void valueChange(ValueChangeEvent event) { | |||
Property property = event.getProperty(); | |||
if (property == personList) { | |||
Person person = personManager.getPerson((Long) personList.getValue()); | |||
personForm.editContact(person); | |||
} | |||
} | |||
.... | |||
The method gets the ID of the currently selected person and uses it to | |||
lookup the `Person` instance from the database, which is then passed to | |||
the person form using the newly created `editContact(..)` method. | |||
Next, we modify the code that handles searches: | |||
[source,java] | |||
.... | |||
public void search(SearchFilter searchFilter) { | |||
QueryMetaData qmd = new QueryMetaData((String) searchFilter.getPropertyId(), searchFilter.getTerm(), | |||
getDataSource().getQueryMetaData().getOrderBy(), | |||
getDataSource().getQueryMetaData().getAscending()); | |||
getDataSource().refresh(qmd); | |||
showListView(); | |||
// Visual notification omitted | |||
} | |||
.... | |||
Instead of filtering the container, this method constructs a new | |||
`QueryMetaData` instance and refreshes the data source. Thus, the search | |||
operation is performed in the database and not in the container itself. | |||
As we have removed container filtering, we also have to change the code | |||
that is used to show all contacts: | |||
[source,java] | |||
.... | |||
public void itemClick(ItemClickEvent event) { | |||
if (event.getSource() == tree) { | |||
Object itemId = event.getItemId(); | |||
if (itemId != null) { | |||
if (itemId == NavigationTree.SHOW_ALL) { | |||
getDataSource().refresh(PersonReferenceContainer.defaultQueryMetaData); | |||
showListView(); | |||
} else if (itemId == NavigationTree.SEARCH) { | |||
showSearchView(); | |||
} else if (itemId instanceof SearchFilter) { | |||
search((SearchFilter) itemId); | |||
} | |||
} | |||
} | |||
} | |||
.... | |||
Instead of removing the filters, this method refreshes the data source | |||
using the default query meta data. | |||
[[creating-a-custom-servlet]] | |||
Creating a Custom Servlet | |||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
The original tutorial used an `ApplicationServlet` configured in | |||
_web.xml_ to start the application. In this version, however, we are | |||
going to create our own custom servlet. By doing this, we can let | |||
GlassFish inject the reference to the `PersonManager` EJB using | |||
annotations, which means that we do not need any JDNI look ups at all. | |||
As a bonus, we get rid of the _web.xml_ file as well thanks to the new | |||
JEE 6 `@WebServlet` annotation. The servlet class can be added as an | |||
inner class to the main application class: | |||
[source,java] | |||
.... | |||
@WebServlet(urlPatterns = "/*") | |||
public static class Servlet extends AbstractApplicationServlet { | |||
@EJB | |||
PersonManager personManager; | |||
@Override | |||
protected Application getNewApplication(HttpServletRequest request) throws ServletException { | |||
return new AddressBookApplication(personManager); | |||
} | |||
@Override | |||
protected Class<? extends Application> getApplicationClass() throws ClassNotFoundException { | |||
return AddressBookApplication.class; | |||
} | |||
} | |||
.... | |||
When the servlet is initialized by the web container, the | |||
`PersonManager` EJB will be automatically injected into the | |||
`personManager` field thanks to the `@EJB` annotation. This reference | |||
can then be passed to the main application class in the | |||
`getNewApplication(..)` method. | |||
[[classical-deployment]] | |||
Classical Deployment | |||
~~~~~~~~~~~~~~~~~~~~ | |||
Packaging this application into a WAR is no different from the Hello | |||
World example. We just have to remember to include the _persistence.xml_ | |||
file (we are not going to cover the contents of this file in this | |||
article), otherwise JPA will not work. Note, that as of JEE 6, we do not | |||
need to split up the application into a different bundle for the EJB and | |||
another for the UI. We also do not need any other configuration files | |||
than the persistence unit configuration file. | |||
The actual packaging can be done using the following Ant target: | |||
[source,xml] | |||
.... | |||
<target name="package-with-vaadin" depends="compile"> | |||
<mkdir dir="${dist.dir}"/> | |||
<war destfile="${dist.dir}/${ant.project.name}-with-vaadin.war" needxmlfile="false"> | |||
<lib file="${vaadin.jar}"/> | |||
<classes dir="${build.dir}"/> | |||
<fileset dir="${web.dir}" includes="**"/> | |||
</war> | |||
</target> | |||
.... | |||
Once the application has been packaged, it can be deployed like so, | |||
using the *asadmin* tool that comes with GlassFish: | |||
[source,bash] | |||
.... | |||
$ asadmin deploy /path/to/addressbook-with-vaadin.war | |||
.... | |||
Note, that the Java DB database bundled with GlassFish must be started | |||
prior to deploying the application. Now we can test the application by | |||
opening a web browser and navigating to | |||
http://localhost:8080/addressbook-with-vaadin. The running application | |||
should look something like this: | |||
image:img/ab-with-vaadin-scrshot.png[Running application screenshot] | |||
[[osgi-deployment-options]] | |||
OSGi Deployment Options | |||
~~~~~~~~~~~~~~~~~~~~~~~ | |||
The OSGi support of GlassFish 3 introduces some new possibilities for | |||
Vaadin development. If the Vaadin library is deployed as an OSGi bundle, we can package and | |||
deploy the address book application without the Vaadin library. The | |||
following Ant target can be used to create the WAR: | |||
[source,xml] | |||
.... | |||
<target name="package-without-vaadin" depends="compile"> | |||
<mkdir dir="${dist.dir}"/> | |||
<war destfile="${dist.dir}/${ant.project.name}-without-vaadin.war" needxmlfile="false"> | |||
<classes dir="${build.dir}"/> | |||
<fileset dir="${web.dir}" includes="**"/> | |||
</war> | |||
</target> | |||
.... | |||
[[summary]] | |||
Summary | |||
~~~~~~~ | |||
In this article, we have extended the Address Book demo to use JPA | |||
instead of the in-memory container, with an EJB acting as the facade to | |||
the database. Thanks to annotations, the application does not contain a | |||
single JNDI lookup, and thanks to JEE 6, the application can be deployed | |||
as a single WAR. |
@@ -0,0 +1,45 @@ | |||
[[auto-generating-a-form-based-on-a-bean-vaadin-6-style-form]] | |||
Auto-generating a form based on a bean - Vaadin 6 style Form | |||
------------------------------------------------------------ | |||
In Vaadin 6 it is easy to get a completely auto generated form based on | |||
a bean instance by creating a `BeanItem` and passing that to a Form. Using | |||
`FieldGroup` this requires a few extra lines as `FieldGroup` never adds | |||
fields automatically to any layout but instead gives that control to the | |||
developer. | |||
Given a bean such as this `Person`: | |||
[source,java] | |||
.... | |||
public class Person { | |||
private String firstName,lastName; | |||
private int age; | |||
// + setters and getters | |||
} | |||
.... | |||
You can auto create a form using FieldGroup as follows: | |||
[source,java] | |||
.... | |||
public class AutoGeneratedFormUI extends UI { | |||
@Override | |||
public void init(VaadinRequest request) { | |||
VerticalLayout layout = new VerticalLayout(); | |||
setContent(layout); | |||
FieldGroup fieldGroup = new BeanFieldGroup<Person>(Person.class); | |||
// We need an item data source before we create the fields to be able to | |||
// find the properties, otherwise we have to specify them by hand | |||
fieldGroup.setItemDataSource(new BeanItem<Person>(new Person("John", "Doe", 34))); | |||
// Loop through the properties, build fields for them and add the fields | |||
// to this UI | |||
for (Object propertyId : fieldGroup.getUnboundPropertyIds()) { | |||
layout.addComponent(fieldGroup.buildAndBind(propertyId)); | |||
} | |||
} | |||
} | |||
.... |
@@ -0,0 +1,584 @@ | |||
[[building-vaadin-applications-on-top-of-activiti]] | |||
Building Vaadin applications on top of Activiti | |||
----------------------------------------------- | |||
by Petter Holmström | |||
[[introduction]] | |||
Introduction | |||
~~~~~~~~~~~~ | |||
In this article, we are going to look at how the | |||
http://www.activiti.org[Activiti] BPM engine can be used together with | |||
Vaadin. We are going to do this in the form of a case study of a demo | |||
application that is available on | |||
https://github.com/peholmst/VaadinActivitiDemo[GitHub]. The code is | |||
licensed under Apache License 2.0 and can freely be used as a foundation | |||
for your own applications. | |||
[[the-example-process]] | |||
The Example Process | |||
^^^^^^^^^^^^^^^^^^^ | |||
The following process is used in the demo application: | |||
image:img/process.png[Example process] | |||
Compared to the capabilities of Activiti and BPMN 2.0, the above process | |||
is almost ridiculously simple. However, it allows us to test the | |||
following things: | |||
* *Process start forms*, i.e. forms that need to be filled in before a | |||
process instance is created. | |||
* *User task forms*, i.e. forms that need to be filled in before a task | |||
can be marked as completed. | |||
* Parallell tasks | |||
* Different candidate groups (i.e. groups whose users are potential | |||
assignees of a certain task) | |||
Here is a short walk-through of the process: | |||
1. Before a new process instance is created, the reporter has to fill | |||
in a _Submit bug report form_. | |||
2. Once the instance has been created, two tasks are created: | |||
* *Update bug report*: a manager assigns priority and target version to | |||
the report. Potential assignees are members of the *managers* group. | |||
* *Accept bug report*: a developer accepts the bug report. Potential | |||
assignees are members of the *developers* group. | |||
3. Both of these tasks require the assignee to fill in a form before | |||
they can be completed: the _Update bug report form_ and _Accept bug | |||
report form_, respectively. | |||
4. Once the tasks have been completed, a new task is created, namely | |||
_Resolve bug report_. Potential assignees are members of the | |||
*developers* group. Ideally, this task should automatically be assigned | |||
to whoever claimed the *Accept bug report* task, but currently this is | |||
not implemented. | |||
5. Before the task can be completed, the assignee has to fill in the | |||
_Resolve bug report form_. | |||
6. All tasks have been completed and the process instance ends. | |||
[[prerequisites]] | |||
Prerequisites | |||
^^^^^^^^^^^^^ | |||
In order to get the most out of this article, you should already be | |||
familiar with both Vaadin and Activiti. If not, there is enough free | |||
material available on both products' web sites to get you started. | |||
The demo application is a standard Java EE 6 web application and can be | |||
deployed to any JEE 6 web container, such as | |||
http://tomcat.apache.org[Tomcat 7]. It uses an embedded in-memory | |||
http://www.h2database.com[H2 database] for storing data, which means | |||
that all your data will be lost when the server is restarted. | |||
http://www.eclipse.org/downloads/packages/eclipse-ide-java-ee-developers/heliossr2[Eclipse | |||
3.6] and the http://vaadin.com/eclipse[Vaadin plugin] was used to create | |||
the application. Both the project files and the third-party libraries | |||
are included in the source code repository. At this point, I recommend | |||
you to download the source code before continuing. | |||
Once you have Eclipse, Tomcat and Git properly installed and configured, | |||
you can follow the following instructions to get the demo application up | |||
and running: | |||
1. Open a command line and clone the Git repository: | |||
`git clone git://github.com/peholmst/VaadinActivitiDemo.git` | |||
2. Start up Eclipse. | |||
3. From the *File* menu, select *Import*. | |||
4. Select *Existing Projects into Workspace* and click *Next*. | |||
5. In the *Select root directory* field, click the *Browse* button and | |||
locate the cloned Git repository directory. | |||
6. In the list of projects, check *VaadinActivitiDemo* and click | |||
*Finish*. | |||
7. In the *Project Explorer*, right-click on *VaadinActivitiDemo*, | |||
point to *Run As* and select *Run on Server*. | |||
8. Select the Tomcat 7 server and click *Finish*. | |||
9. Open a web browser and point it to | |||
_http://localhost:8080/VaadinActivitiDemo_. | |||
[[scope]] | |||
Scope | |||
^^^^^ | |||
As Activiti has a huge amount of features, we are only going to look at | |||
a small subset of them in order to keep the scope of this article under | |||
control. More specifically, we are going to look at the following two | |||
questions: | |||
1. How easy (or hard) is it to create custom-built forms using Vaadin | |||
and plug these into Activiti? | |||
2. How easy (or hard) is it to combine process data from Activiti with | |||
other domain data from e.g. JPA? | |||
[[application-architecture]] | |||
Application Architecture | |||
~~~~~~~~~~~~~~~~~~~~~~~~ | |||
In this section, we are going to briefly discuss the architecture of the | |||
demo application on a general level and show how it has been implemented | |||
on more technical level. A simplified version of the architecture is | |||
illustrated here: | |||
image:img/architecture.png[Application architecture] | |||
[[the-h2-database]] | |||
The H2 Database | |||
^^^^^^^^^^^^^^^ | |||
The H2 database is used in in-memory mode and will start when the | |||
process engine is initialized and stop when the engine is destroyed. All | |||
you have to do is specify some connection parameters when you | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/activiti.cfg.xml[configure | |||
Activiti] and the rest will be handled automatically. | |||
[[the-activiti-engine-and-process-definitions]] | |||
The Activiti Engine and Process Definitions | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
The Activiti engine is initialized and destroyed by a servlet context | |||
listener, like so: | |||
[source,java] | |||
.... | |||
@WebListener | |||
public class ProcessEngineServletContextListener implements ServletContextListener { | |||
@Override | |||
public void contextInitialized(ServletContextEvent event) { | |||
ProcessEngines.init(); | |||
deployProcesses(); | |||
} | |||
@Override | |||
public void contextDestroyed(ServletContextEvent event) { | |||
ProcessEngines.destroy(); | |||
} | |||
private void deployProcesses() { | |||
RepositoryService repositoryService = ProcessEngines.getDefaultProcessEngine().getRepositoryService(); | |||
repositoryService.createDeployment() | |||
.addClasspathResource("path/to/bpmn-document.bpmn20.xml") | |||
.deploy(); | |||
} | |||
} | |||
.... | |||
Once the process engine has been initialized, the context listener | |||
deploys the BPMN 2.0 process definitions to it. In other words, the | |||
Activiti process engine becomes available as soon as the web application | |||
starts and remains up and running until the application is stopped. All | |||
the Vaadin application instances use the same Activiti engine. | |||
[[the-vaadin-application]] | |||
The Vaadin Application | |||
^^^^^^^^^^^^^^^^^^^^^^ | |||
The Vaadin application is designed according to the | |||
http://en.wikipedia.org/wiki/Model-view-presenter[Model-View-Presenter] | |||
(MVP) pattern and is implemented using | |||
https://github.com/peholmst/MVP4Vaadin[MVP4Vaadin]. This gives us the | |||
following benefits: | |||
* Clear separation between logic and UI (makes unit testing easier). | |||
* View navigation becomes easier (e.g. the breadcrumb bar shown in the | |||
demo screencast is a built-in part of MVP4Vaadin). | |||
The following diagram illustrates the different views and potential | |||
navigation paths between them: | |||
image:img/views.png[Application views and navigation] | |||
When the application is first started, the | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/login[Login | |||
View] is displayed in the main window. Once the user has logged on, the | |||
main window is replaced with the | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/main[Main | |||
View]: | |||
[source,java] | |||
.... | |||
public class DemoApplication extends Application implements ViewListener { | |||
// Field declarations omitted | |||
@Override | |||
public void init() { | |||
createAndShowLoginWindow(); | |||
} | |||
private void createAndShowLoginWindow() { | |||
// Implementation omitted | |||
} | |||
private void createAndShowMainWindow() { | |||
// Implementation omitted | |||
} | |||
@Override | |||
public void handleViewEvent(ViewEvent event) { | |||
if (event instanceof UserLoggedInEvent) { | |||
// Some code omitted | |||
createAndShowMainWindow(); | |||
} // Other event handlers omitted | |||
} | |||
// Additional methods omitted. | |||
} | |||
.... | |||
The main view acts as a controller and container for a number of | |||
embedded views: | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/home[Home | |||
View] is the main menu. From here, you can navigate to the _Process | |||
Browser View_ and the _Identity Management View_. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/processes[Process | |||
Browser View] contains a list of all the available process definitions. | |||
From this view, you can start new process instances. If a process has a | |||
start form, you can also navigate to the _User Form View_. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/identity[Identity | |||
Management View] allows you to manage users and user groups. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/UnassignedTasksViewImpl.java[Unassigned | |||
Tasks View] contains a list of all unassigned tasks. You can navigate to | |||
this view from any other view. From this view, you can assign tasks to | |||
yourself. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/MyTasksViewImpl.java[My | |||
Tasks View] contains a list of all tasks currently assigned to you. You | |||
can navigate to this view from any other view. From this view, you can | |||
complete tasks. If a task has a form, you can also navigate to the _User | |||
Form View_. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms[User | |||
Form View] is responsible for displaying the _User Task Forms_, e.g. | |||
before a new process instance is created or before a task is completed. | |||
The information about which form to show (if any) is specified in the | |||
BPMN process definition. *Please note that when we are talking about | |||
forms in this article, we are referring to the Acticiti form concept. Do | |||
not confuse this with Vaadin forms.* | |||
These views (or technically speaking their corresponding presenters) | |||
communicate directly with the Activiti engine. For example, the | |||
following snippet is taken from the | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/processes/ProcessPresenter.java[`ProcessPresenter`] | |||
class: | |||
[source,java] | |||
.... | |||
@Override | |||
public void init() { | |||
getView().setProcessDefinitions(getAllProcessDefinitions()); | |||
} | |||
public void startNewInstance(ProcessDefinition processDefinition) { | |||
try { | |||
if (processDefinitionHasForm(processDefinition)) { | |||
openFormForProcessDefinition(processDefinition); | |||
} else { | |||
getRuntimeService().startProcessInstanceById(processDefinition.getId()); | |||
getView().showProcessStartSuccess(processDefinition); | |||
} | |||
} catch (RuntimeException e) { | |||
getView().showProcessStartFailure(processDefinition); | |||
} | |||
} | |||
private List<ProcessDefinition> getAllProcessDefinitions() { | |||
ProcessDefinitionQuery query = getRepositoryService().createProcessDefinitionQuery(); | |||
return query.orderByProcessDefinitionName().asc().list(); | |||
} | |||
private RepositoryService getRepositoryService() { | |||
return ProcessEngines.getDefaultProcessEngine().getRepositoryService(); | |||
} | |||
private RuntimeService getRuntimeService() { | |||
return ProcessEngines.getDefaultProcessEngine().getRuntimeService(); | |||
} | |||
.... | |||
The Main View also regularly checks if there are new tasks available and | |||
notifies the user if that is the case. The | |||
http://vaadin.com/addon/refresher[Refresher] add-on is used to handle | |||
the polling. | |||
[[some-notes-on-mvp4vaadin]] | |||
Some Notes on MVP4Vaadin | |||
^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Thanks to MVP4Vaadin, navigation between views is very simple. For | |||
example, the following code snippet is taken from the | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/main/components/WindowHeader.java[`WindowHeader`] | |||
component, a part of the Main View implementation: | |||
[source,java] | |||
.... | |||
@SuppressWarnings("serial") | |||
private Button createMyTasksButton() { | |||
Button button = new Button(); | |||
button.addListener(new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
mainPresenter.showMyTasks(); | |||
} | |||
}); | |||
button.addStyleName(Reindeer.BUTTON_SMALL); | |||
return button; | |||
} | |||
@SuppressWarnings("serial") | |||
private Button createUnassignedTasksButton() { | |||
Button button = new Button(); | |||
button.addListener(new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
mainPresenter.showUnassignedTasks(); | |||
} | |||
}); | |||
button.addStyleName(Reindeer.BUTTON_SMALL); | |||
return button; | |||
} | |||
.... | |||
The corresponding snippets from the | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/main/MainPresenter.java[`MainPresenter`] | |||
class are as follows: | |||
[source,java] | |||
.... | |||
public void showUnassignedTasks() { | |||
getViewController().goToView(UnassignedTasksView.VIEW_ID); | |||
} | |||
public void showMyTasks() { | |||
getViewController().goToView(MyTasksView.VIEW_ID); | |||
} | |||
.... | |||
[[custom-forms]] | |||
Custom Forms | |||
~~~~~~~~~~~~ | |||
As you may already know, it is possible to use automatic form generation | |||
with Activiti, but the generated forms are not Vaadin based. In this | |||
article, we are going to use custom-built Vaadin forms instead. Even | |||
though this forces us to write Java code for each form we want to use, | |||
it gives us some advantages: | |||
* It is possible to have more complex forms with differnt kinds of | |||
components. | |||
* It is possible to tailor the appearance and look and feel of the forms | |||
to the user's needs. | |||
* It is easy to plug in other infrastructure services such as EJBs and | |||
JPA entities. | |||
The following approach is used to implement custom forms in the demo | |||
application: | |||
image:img/customForms.png[Custom forms] | |||
Here is a short walk-through of the most important classes: | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/util/UserTaskForm.java[`UserTaskForm`] | |||
interface is implemented by all custom forms. This interface defines | |||
several methods, the most interesting of which are the following: | |||
** `populateForm(...)`: This method populates the form with initial data | |||
retrieved from the Activiti form service. | |||
** `getFormProperties()`: This method creates a map of the form data | |||
that will be sent to the Activiti form service when the form is | |||
submitted. | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/util/UserTaskFormContainer.java[`UserTaskFormContainer`] | |||
is a class that contains user task forms. Each form can be accessed by a | |||
unique form key, which in turn is used in BPMN-documents to refer to | |||
forms. The main Vaadin application class is responsible for creating and | |||
populating this container. *Please note, that this container class has | |||
nothing to do with Vaadin Data Containers.* | |||
* The | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms/UserFormViewImpl.java[`UserFormViewImpl`] | |||
class (and its corresponding presenter) is responsible for looking up | |||
the correct form (by its form key), populating it, displaying it to the | |||
user and finally submitting it. | |||
[[some-code-examples]] | |||
Some Code Examples | |||
^^^^^^^^^^^^^^^^^^ | |||
We are now going to look at some snippets from the demo application | |||
source code. | |||
First up is a method from the | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/MyTasksPresenter.java[`MyTasksPresenter`] | |||
class that is invoked when the user wants to open the form for a | |||
specific task: | |||
[source,java] | |||
.... | |||
public void openFormForTask(Task task) { | |||
String formKey = getFormKey(task); | |||
if (formKey != null) { | |||
HashMap<String, Object> params = new HashMap<String, Object>(); | |||
params.put(UserFormView.KEY_FORM_KEY, formKey); | |||
params.put(UserFormView.KEY_TASK_ID, task.getId()); | |||
getViewController().goToView(UserFormView.VIEW_ID, params); | |||
} | |||
} | |||
.... | |||
The method checks if the task has a form and asks the view controller (a | |||
part of MVP4Vaadin) to navigate to the User Form View if that is the | |||
case. The task ID and form key is passed to the view as a map of | |||
parameters. | |||
The next code example is a method of the | |||
https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms/UserFormPresenter.java[`UserFormPresenter`] | |||
class that is invoked when the view controller has navigated to the User | |||
Form View: | |||
[source,java] | |||
.... | |||
@Override | |||
protected void viewShown(ViewController viewController, | |||
Map<String, Object> userData, ControllableView oldView, | |||
Direction direction) { | |||
if (userData != null) { | |||
String formKey = (String) userData.get(UserFormView.KEY_FORM_KEY); | |||
if (userData.containsKey(UserFormView.KEY_TASK_ID)) { | |||
String taskId = (String) userData.get(UserFormView.KEY_TASK_ID); | |||
showTaskForm(formKey, taskId); | |||
} | |||
// The rest of the implementation is omitted | |||
} | |||
} | |||
private void showTaskForm(String formKey, String taskId) { | |||
UserTaskForm form = userTaskFormContainer.getForm(formKey); | |||
TaskFormData formData = getFormService().getTaskFormData(taskId); | |||
form.populateForm(formData, taskId); | |||
getView().setForm(form); | |||
} | |||
.... | |||
The method first extracts the task ID and form key from the parameter | |||
map. It then invokes a helper method that looks up the corresponding | |||
form data and form from the Activiti form service and the | |||
`UserTaskFormContainer`, respectively. Finally, the form is populated | |||
and shown to the user. | |||
The final example is a method (also from `UserFormPresenter`) that is | |||
invoked when the user submits the form: | |||
[source,java] | |||
.... | |||
public void submitForm(UserTaskForm form) { | |||
if (form.getFormType().equals(UserTaskForm.Type.START_FORM)) { | |||
getFormService().submitStartFormData(form.getProcessDefinitionId(), form.getFormProperties()); | |||
} else if (form.getFormType().equals(UserTaskForm.Type.TASK_FORM)) { | |||
getFormService().submitTaskFormData(form.getTaskId(), form.getFormProperties()); | |||
} | |||
getViewController().goBack(); | |||
} | |||
.... | |||
As there are two different kinds of forms (process start forms and user | |||
task forms, respectively), the method has to start by checking which | |||
kind it is currently processing. Then, the information is submitted to | |||
the Activiti form service. Finally, the view controller is asked to | |||
navigate back to what ever page it was on before the User Form View | |||
became visible. | |||
[[complex-domain-objects]] | |||
Complex Domain Objects | |||
~~~~~~~~~~~~~~~~~~~~~~ | |||
The demo application does not use any domain objects as all the | |||
information can be represented as Activiti process variables. However, | |||
in most real-world applications you probably want to use a dedicated | |||
domain model. | |||
We are now going to look at a potential design for combining Activiti | |||
with a complex domain model. *Please note that the design has not been | |||
tested in practice* - feel free to test it if you feel like it (and | |||
remember to tell me the results)! | |||
Here is a sketch of a process that involves a more complicated domain | |||
model than just a few strings: | |||
image:img/complexdomain.png[Complex domain] | |||
The idea is that although many different entities need to be created and | |||
stored throughout the process, only some small parts of the information | |||
is actually required to drive the process forward. For example, the | |||
*Send invoice* task does not necessarily need the entire invoice object; | |||
only the invoice number, order number and due date should be sufficient. | |||
Likewise, the *Receive payment* task needs only the invoice number to be | |||
able to check that the invoice has been paid, the timer needs the due | |||
date to be able to send out a new invoice, etc. | |||
[[implementation-ideas]] | |||
Implementation Ideas | |||
^^^^^^^^^^^^^^^^^^^^ | |||
The actual forms that the users fill in could be implemented in Vaadin, | |||
as described previously in this article. When the form is submitted, the | |||
entities are saved to some data store (e.g. a relational database). | |||
After this, the necessary form properties are submitted to the Activiti | |||
form service, completing the task in question. In other words, Activiti | |||
is used to drive the process forward (i.e. define the business logic), | |||
whereas JPA or any other object persistence solution is used to store | |||
data. | |||
There are a few things to keep in mind, though: | |||
* How are transactions handled? | |||
* How is data validation performed? | |||
* How is security enforced? | |||
* Is versioning of the domain data required? How should it be | |||
implemented if so? (Activiti already maintains a history log of the | |||
process operations.) | |||
In smaller applications, the following design could be sufficient: | |||
image:img/complexdomain_saving.png[Complex domain saving] | |||
Here, the Presenter (in the MVP-pattern) is responsible for extracting | |||
the needed form properties from the domain data, saving the entity and | |||
submitting the form. This moves some of the logic to the UI layer, but | |||
for small applications this is not a big problem as the presenter is | |||
itself decoupled from the actual UI code. | |||
For larger applications, the following design could be a better | |||
approach: | |||
image:img/complexdomain_saving2.png[Complex domain saving 2] | |||
Here, both the repository and the form service engine is hidden behind a | |||
facade. A Data Transfer Object (DTO) is used to convey the data from the | |||
Presenter to the facade. This approach requires more code, but decouples | |||
the business layer from the UI layer even more. Security enforcement and | |||
transaction handling also become easier. | |||
[[summary]] | |||
Summary | |||
~~~~~~~ | |||
In this article, we have looked at how the Activiti BPM engine and | |||
Vaadin fit together. We have covered how the engine is initialized and | |||
accessed by Vaadin application instances. We have also covered how | |||
custom-made Vaadin forms can be used instead of Activiti's own form | |||
generation. Finally, we have discussed a way of combining Activiti | |||
processes with a more complex domain model. | |||
The Activiti API is clear and does not force adopters to use a specific | |||
GUI technology. Therefore, it plays really well with Vaadin and should | |||
be concidered a serious alternative for process centric enterprise | |||
applications. | |||
Likewise, Vaadin should be considered a serious alternative as a front | |||
end technology for applications based on Activiti. | |||
If you have any comments or questions, for example if something in the | |||
article is unclear or confusing, feel free to either post them below or | |||
send them to me directly by e-mail. |
@@ -0,0 +1,76 @@ | |||
[[changing-the-default-converters-for-an-application]] | |||
Changing the default converters for an application | |||
-------------------------------------------------- | |||
Each Vaadin session instance has a `ConverterFactory` that provides | |||
converters to Fields and Table. The defaults might not be ideal for your | |||
case so it is possible for you to change the defaults by providing your | |||
own ConverterFactory. If you, for instance, want to format all (or most) | |||
doubles from your data model with 3 decimals and no thousand separator | |||
(but still allow the user to input with any number of decimals) you can | |||
do this by first creating your own Converter: | |||
[source,java] | |||
.... | |||
public class MyStringToDoubleConverter extends StringToDoubleConverter { | |||
@Override | |||
protected NumberFormat getFormat(Locale locale) { | |||
NumberFormat format = super.getFormat(locale); | |||
format.setGroupingUsed(false); | |||
format.setMaximumFractionDigits(3); | |||
format.setMinimumFractionDigits(3); | |||
return format; | |||
} | |||
} | |||
.... | |||
and then extending the default converter factory to use your converter | |||
for all `Double` <-> `String` conversions. | |||
[source,java] | |||
.... | |||
public class MyConverterFactory extends DefaultConverterFactory { | |||
@Override | |||
protected <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> findConverter( | |||
Class<PRESENTATION> presentationType, Class<MODEL> modelType) { | |||
// Handle String <-> Double | |||
if (presentationType == String.class && modelType == Double.class) { | |||
return (Converter<PRESENTATION, MODEL>) new MyStringToDoubleConverter(); | |||
} | |||
// Let default factory handle the rest | |||
return super.findConverter(presentationType, modelType); | |||
} | |||
} | |||
.... | |||
You still need to tell your application to always use | |||
`MyConverterFactory`: | |||
[source,java] | |||
.... | |||
VaadinSession.getCurrent().setConverterFactory(new MyConverterFactory()); | |||
.... | |||
Now we can test it using | |||
[source,java] | |||
.... | |||
public class MyUI extends UI { | |||
public void init(VaadinRequest request) { | |||
TextField tf = new TextField("This is my double field"); | |||
tf.setImmediate(true); | |||
tf.setConverter(Double.class); | |||
setContent(tf); | |||
tf.setConvertedValue(50.1); | |||
} | |||
} | |||
.... | |||
This will not enforce the contents of the field to the format specified | |||
by the converter. Only data from the data source is formatted to adhere | |||
to the format set in the converter. | |||
If you want to force the user to enter data with a given number of | |||
decimals you need to create your own converter instead of only | |||
overriding the format for `StringToDoubleConverter`. |
@@ -0,0 +1,72 @@ | |||
[[configuring-grid-column-widths]] | |||
Configuring Grid column widths | |||
------------------------------ | |||
To try out how the widths of Grid columns work in different situations, | |||
we'll use the same base implementation as in the | |||
link:UsingGridWithAContainer.asciidoc[Using Grid with a Container] | |||
example. | |||
Grid does by default check the widths of all cells on the first pageful | |||
of data and allocate column widths based on that. If there's room to | |||
spare, each column gets and equal share of the extra pixels. | |||
There is usually one or maybe two columns that would most benefit from | |||
some additional breathing room, but Grid can't know which columns that | |||
is unless you tell it. You can do so using the `setExpandRatio(int)` | |||
method for a column. | |||
[source,java] | |||
.... | |||
grid.getColumn("name").setExpandRatio(1); | |||
.... | |||
When setting one column to expand, all the extra space gets allocated to | |||
that column. This might instead cause the other columns to be too | |||
tightly spaced. One easy way of avoiding this is to use `setWidth(double)` | |||
to set a pixel size for columns that are not expanded. | |||
[source,java] | |||
.... | |||
grid.getColumn("name").setExpandRatio(1); | |||
grid.getColumn("amount").setWidth(100); | |||
grid.getColumn("count").setWidth(100); | |||
.... | |||
Reducing the width of Grid does now cause the `Name` column to shrink | |||
while the two other columns keep their defined original sizes. We might, | |||
however, want to prevent the `Name` column from becoming too narrow by | |||
giving it a minimum width. Without any defined minimum width, the widths | |||
of the cell contents of the first pageful of data will define the | |||
minimum width. If there's not enough room for all columns, Grid will | |||
automatically enable horizontal scrolling so that all columns can still | |||
be accessed. | |||
[source,java] | |||
.... | |||
grid.setWidth("400px"); | |||
grid.getColumn("name").setMinimumWidth(250); | |||
grid.getColumn("amount").setWidth(100); | |||
grid.getColumn("count").setWidth(100); | |||
.... | |||
With horizontal scrolling, it might be desirable to still keep columns | |||
identifying each row visible all the time so that it's easier for the | |||
user to interpret the data. This can be done by freezing a number of | |||
columns, counted from the left, using the `setFrozenColumnCount(int)` | |||
method. By default, only the column showing selection state in | |||
multiselect mode is frozen. This column can also be unfrozen by setting | |||
the count to -1. | |||
[source,java] | |||
.... | |||
grid.setWidth("400px"); | |||
grid.setFrozenColumnCount(1); | |||
grid.getColumn("name").setMinimumWidth(250); | |||
grid.getColumn("amount").setWidth(100); | |||
grid.getColumn("count").setWidth(100); | |||
.... | |||
If the width of Grid is again increased so that all columns can fit | |||
without scrolling, the frozen columns will behave just as any other | |||
column. |
@@ -0,0 +1,74 @@ | |||
[[creating-a-basic-application]] | |||
Creating a basic application | |||
---------------------------- | |||
To create a Vaadin application you need two files. A class that extends | |||
UI which is your main view and entry point to the application as well as | |||
a web.xml referring to the UI. | |||
With Eclipse and the Vaadin plugin you will get all of this | |||
automatically by opening the New wizard (File -> New -> Other) and | |||
choosing Vaadin -> Vaadin Project. From there you can give the new | |||
project a name and the wizard takes care of the rest. | |||
In other environments you can create the standard java web application | |||
project. Create one file which extends UI into the source folder. Let's | |||
call it MyApplicationUI: | |||
[source,java] | |||
.... | |||
package com.example.myexampleproject; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.UI; | |||
import com.vaadin.ui.VerticalLayout; | |||
import com.vaadin.ui.Label; | |||
public class MyApplicationUI extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
VerticalLayout view = new VerticalLayout(); | |||
view.addComponent(new Label("Hello Vaadin!")); | |||
setContent(view); | |||
} | |||
} | |||
.... | |||
This application creates a new main layout to the UI and adds the text | |||
"Hello Vaadin!" into it. | |||
Your web deployment descriptor, web.xml, has to point at your UI as | |||
well. This is done with an defining a Vaadin servlet and giving the UI | |||
as a parameter to it: | |||
[source,xml] | |||
.... | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" | |||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> | |||
<display-name>MyApplication</display-name> | |||
<context-param> | |||
<description>Vaadin production mode</description> | |||
<param-name>productionMode</param-name> | |||
<param-value>false</param-value> | |||
</context-param> | |||
<servlet> | |||
<servlet-name>My Vaadin App</servlet-name> | |||
<servlet-class>com.vaadin.server.VaadinServlet</servlet-class> | |||
<init-param> | |||
<description>Vaadin UI</description> | |||
<param-name>UI</param-name> | |||
<param-value>com.example.myexampleproject.MyApplicationUI</param-value> | |||
</init-param> | |||
</servlet> | |||
<servlet-mapping> | |||
<servlet-name>My Vaadin App</servlet-name> | |||
<url-pattern>/*</url-pattern> | |||
</servlet-mapping> | |||
</web-app> | |||
.... | |||
Now you're able to package your application into a war and deploy it on | |||
a servlet container. |
@@ -0,0 +1,266 @@ | |||
[[creating-a-customfield-for-editing-the-address-of-a-person]] | |||
Creating a CustomField for editing the address of a person | |||
---------------------------------------------------------- | |||
A normal use case is that you want to create a form out a bean that the | |||
user can edit. Often these beans contain references to other beans as | |||
well, and you have to create a separate editor for those. This tutorial | |||
goes through on how to edit an `Address` bean which is inside a `Person` | |||
bean with the use of `CustomField` and `FieldGroup`. | |||
Here are the `Person` and `Address` beans | |||
[source,java] | |||
.... | |||
public class Person { | |||
private String firstName; | |||
private String lastName; | |||
private Address address; | |||
private String phoneNumber; | |||
private String email; | |||
private Date dateOfBirth; | |||
private String comments; | |||
//Getters and setters | |||
} | |||
.... | |||
[source,java] | |||
.... | |||
public class Address { | |||
private String street; | |||
private String zip; | |||
private String city; | |||
private String country; | |||
// Getters and setters | |||
} | |||
.... | |||
[[creating-a-new-field]] | |||
Creating a new field | |||
~~~~~~~~~~~~~~~~~~~~ | |||
The first step is to create a new field which represents the editor for | |||
the address. In this case the field itself will be a button. The button | |||
will open a window where you have all the address fields. The address | |||
will be stored back when the user closes the window. | |||
[source,java] | |||
.... | |||
public class AddressPopup extends CustomField<Address> { | |||
@Override | |||
protected Component initContent() { | |||
return null; | |||
} | |||
@Override | |||
public Class<Address> getType() { | |||
return Address.class; | |||
} | |||
} | |||
.... | |||
CustomField requires that you implement two methods, `initContent()` and | |||
`getType()`. `initContent()` creates the actual visual representation of | |||
your field. `getType()` tells the field which type of data will be handled | |||
by the field. In our case it is an `Address` object so we return | |||
`Address.class` in the method. | |||
[[creating-the-content]] | |||
Creating the content | |||
~~~~~~~~~~~~~~~~~~~~ | |||
Next up we create the actual button that will be visible in the person | |||
editor when the CustomField is rendered. This button should open up a | |||
new window where the user can edit the address. | |||
[source,java] | |||
.... | |||
@Override | |||
protected Component initContent() { | |||
final Window window = new Window("Edit address"); | |||
final Button button = new Button("Open address editor", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
getUI().addWindow(window); | |||
} | |||
}); | |||
return button; | |||
} | |||
.... | |||
This is enough to attach the field to the person editor, but the window | |||
will be empty and it won't modify the data in any way. | |||
[[creating-the-editable-fields]] | |||
Creating the editable fields | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
The address object contains four strings - street, zip, city and | |||
country. For the three latter a `TextField` is good for editing, but the | |||
street address can contain multiple row so a `TextArea` is better here. | |||
All the fields have to be put into a layout and the layout has to be set | |||
as the content of the window. `FormLayout` is a good choice here to nicely | |||
align up the captions and fields of the window. | |||
[source,java] | |||
.... | |||
FormLayout layout = new FormLayout(); | |||
TextArea street = new TextArea("Street address:"); | |||
TextField zip = new TextField("Zip code:"); | |||
TextField city = new TextField("City:"); | |||
TextField country = new TextField("Country:"); | |||
layout.addComponent(street); | |||
layout.addComponent(zip); | |||
layout.addComponent(city); | |||
layout.addComponent(country); | |||
window.setContent(layout); | |||
.... | |||
The field is now visually ready but it doesn't contain or affect any | |||
data. You want to also modify the sizes as well to make it look a bit | |||
nicer: | |||
[source,java] | |||
.... | |||
window.center(); | |||
window.setWidth(null); | |||
layout.setWidth(null); | |||
layout.setMargin(true); | |||
.... | |||
[[binding-the-address-to-the-field]] | |||
Binding the address to the field | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
A FieldGroup can be used to bind the data of an Address bean into the | |||
fields. We create a member variable for a FieldGroup and initialize it | |||
within the createContent() -method: | |||
[source,java] | |||
.... | |||
fieldGroup = new BeanFieldGroup<Address>(Address.class); | |||
fieldGroup.bind(street, "street"); | |||
fieldGroup.bind(zip, "zip"); | |||
fieldGroup.bind(city, "city"); | |||
fieldGroup.bind(country, "country"); | |||
.... | |||
The `FieldGroup` of the person editor will call | |||
`AddressPopup.setValue(person.getAddress())` when we start to edit our | |||
person. We need to override `setInternalValue(Address)` to get the `Address` | |||
object and pass it to the `FieldGroup` of the address editor. | |||
[source,java] | |||
.... | |||
@Override | |||
protected void setInternalValue(Address address) { | |||
super.setInternalValue(address); | |||
fieldGroup.setItemDataSource(new BeanItem<Address>(address)); | |||
} | |||
.... | |||
The last thing that has to be done is save the modifications made by the | |||
user back into the `Address` bean. This is done with a `commit()` call to | |||
the `FieldGroup`, which can be made for example when the window is closed: | |||
[source,java] | |||
.... | |||
window.addCloseListener(new CloseListener() { | |||
public void windowClose(CloseEvent e) { | |||
try { | |||
fieldGroup.commit(); | |||
} catch (CommitException ex) { | |||
ex.printStackTrace(); | |||
} | |||
} | |||
}); | |||
.... | |||
Now you need to attach the `AddressPopup` custom field into the person | |||
editor through it's `FieldGroup` and you have a working editor. | |||
[[complete-code]] | |||
Complete code | |||
~~~~~~~~~~~~~ | |||
[source,java] | |||
.... | |||
package com.example.addressforms.fields; | |||
import com.example.addressforms.data.Address; | |||
import com.vaadin.data.fieldgroup.BeanFieldGroup; | |||
import com.vaadin.data.fieldgroup.FieldGroup; | |||
import com.vaadin.data.fieldgroup.FieldGroup.CommitException; | |||
import com.vaadin.data.util.BeanItem; | |||
import com.vaadin.ui.Button; | |||
import com.vaadin.ui.Button.ClickEvent; | |||
import com.vaadin.ui.Button.ClickListener; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.CustomField; | |||
import com.vaadin.ui.FormLayout; | |||
import com.vaadin.ui.TextArea; | |||
import com.vaadin.ui.TextField; | |||
import com.vaadin.ui.Window; | |||
import com.vaadin.ui.Window.CloseEvent; | |||
import com.vaadin.ui.Window.CloseListener; | |||
public class AddressPopup extends CustomField<Address> { | |||
private FieldGroup fieldGroup; | |||
@Override | |||
protected Component initContent() { | |||
FormLayout layout = new FormLayout(); | |||
final Window window = new Window("Edit address", layout); | |||
TextArea street = new TextArea("Street address:"); | |||
TextField zip = new TextField("Zip code:"); | |||
TextField city = new TextField("City:"); | |||
TextField country = new TextField("Country:"); | |||
layout.addComponent(street); | |||
layout.addComponent(zip); | |||
layout.addComponent(city); | |||
layout.addComponent(country); | |||
fieldGroup = new BeanFieldGroup<Address>(Address.class); | |||
fieldGroup.bind(street, "street"); | |||
fieldGroup.bind(zip, "zip"); | |||
fieldGroup.bind(city, "city"); | |||
fieldGroup.bind(country, "country"); | |||
Button button = new Button("Open address editor", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
getUI().addWindow(window); | |||
} | |||
}); | |||
window.addCloseListener(new CloseListener() { | |||
public void windowClose(CloseEvent e) { | |||
try { | |||
fieldGroup.commit(); | |||
} catch (CommitException ex) { | |||
ex.printStackTrace(); | |||
} | |||
} | |||
}); | |||
window.center(); | |||
window.setWidth(null); | |||
layout.setWidth(null); | |||
layout.setMargin(true); | |||
return button; | |||
} | |||
@Override | |||
public Class<Address> getType() { | |||
return Address.class; | |||
} | |||
@Override | |||
protected void setInternalValue(Address address) { | |||
super.setInternalValue(address); | |||
fieldGroup.setItemDataSource(new BeanItem<Address>(address)); | |||
} | |||
} | |||
.... | |||
image:img/person%20editor.png[Address editor] | |||
image:img/address%20editor.png[Address editor window] |
@@ -0,0 +1,382 @@ | |||
[[creating-a-master-details-view-for-editing-persons]] | |||
Creating a master details view for editing persons | |||
-------------------------------------------------- | |||
[[set-up]] | |||
Set-up | |||
~~~~~~ | |||
In this tutorial we go through a standard use case where you have a bean | |||
and a backend ready with create, read, update and delete capabilities on | |||
that bean. You want to create a view where you can view all the beans | |||
and edit them. This example is an address book where you edit person | |||
information. The bean and the backend that we're going to use looks like | |||
this: | |||
[[person]] | |||
Person | |||
^^^^^^ | |||
[source,java] | |||
.... | |||
public class Person { | |||
private int id = -1; | |||
private String firstName = ""; | |||
private String lastName = ""; | |||
private Address address = new Address(); | |||
private String phoneNumber = ""; | |||
private String email = ""; | |||
private Date dateOfBirth = null; | |||
private String comments = ""; | |||
.... | |||
[[ibackend]] | |||
IBackend | |||
^^^^^^^^ | |||
[source,java] | |||
.... | |||
public interface IBackend { | |||
public List<Person> getPersons(); | |||
public void storePerson(Person person); | |||
public void deletePerson(Person person); | |||
} | |||
.... | |||
The view will contain a table, with all the persons in it, and a form | |||
for editing a single person. Additionally the table will have buttons | |||
too add or remove persons and the form will have buttons to save and | |||
discard changes. The UI wireframe looks like this: | |||
image:img/master%20detail%20wireframe.jpg[Master detail UI wireframe] | |||
[[building-the-basic-ui]] | |||
Building the basic UI | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
We start off with creating a basic UIfor our application | |||
[source,java] | |||
.... | |||
public class AddressFormsUI extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
VerticalLayout mainLayout = new VerticalLayout(); | |||
mainLayout.setSpacing(true); | |||
mainLayout.setMargin(true); | |||
mainLayout.addComponent(new Label("Hello Vaadiners!")); | |||
setContent(mainLayout); | |||
} | |||
} | |||
.... | |||
The first things that we want to add to it is the table and the form. | |||
The table should be selectable and immediate so that we're able to pass | |||
person objects from it to the form. I will create all the fields for our | |||
person editor by hand to get more flexibility in how the fields will be | |||
built and laid out. You could also let Vaadin `FieldGroup` take care of | |||
creating the standard fields with the `buildAndBind` -methods if you don't | |||
need to customize them. | |||
[source,java] | |||
.... | |||
package com.example.addressforms; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.DateField; | |||
import com.vaadin.ui.GridLayout; | |||
import com.vaadin.ui.UI; | |||
import com.vaadin.ui.Table; | |||
import com.vaadin.ui.TextArea; | |||
import com.vaadin.ui.TextField; | |||
import com.vaadin.ui.VerticalLayout; | |||
public class AddressFormsUI extends UI { | |||
private GridLayout form; | |||
private Table table; | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
VerticalLayout mainLayout = new VerticalLayout(); | |||
mainLayout.setSpacing(true); | |||
mainLayout.setMargin(true); | |||
mainLayout.addComponent(buildTable()); | |||
mainLayout.addComponent(buildForm()); | |||
setContent(mainLayout); | |||
} | |||
private Component buildTable() { | |||
table = new Table(null); | |||
table.setWidth("500px"); | |||
table.setSelectable(true); | |||
table.setImmediate(true); | |||
return table; | |||
} | |||
private Component buildForm() { | |||
form = new GridLayout(2, 3); | |||
TextField firstName = new TextField("First name:"); | |||
TextField lastName = new TextField("Last name:"); | |||
TextField phoneNumber = new TextField("Phone Number:"); | |||
TextField email = new TextField("E-mail address:"); | |||
DateField dateOfBirth = new DateField("Date of birth:"); | |||
TextArea comments = new TextArea("Comments:"); | |||
form.addComponent(firstName); | |||
form.addComponent(lastName); | |||
form.addComponent(phoneNumber); | |||
form.addComponent(email); | |||
form.addComponent(dateOfBirth); | |||
form.addComponent(comments); | |||
return form; | |||
} | |||
} | |||
.... | |||
image:img/table%20and%20form.png[Address form] | |||
We also want the add, remove, save and discard buttons so let's create | |||
them as well. I've positioned the add and remove above the table and | |||
save and discard below the form. | |||
[source,java] | |||
.... | |||
private GridLayout form; | |||
private HorizontalLayout tableControls; | |||
private Table table; | |||
private HorizontalLayout formControls; | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
VerticalLayout mainLayout = new VerticalLayout(); | |||
mainLayout.setSpacing(true); | |||
mainLayout.setMargin(true); | |||
mainLayout.addComponent(buildTableControls()); | |||
mainLayout.addComponent(buildTable()); | |||
mainLayout.addComponent(buildForm()); | |||
mainLayout.addComponent(buildFormControls()); | |||
setContent(mainLayout); | |||
} | |||
... | |||
private Component buildTableControls() { | |||
tableControls = new HorizontalLayout(); | |||
Button add = new Button("Add"); | |||
Button delete = new Button("Delete"); | |||
tableControls.addComponent(add); | |||
tableControls.addComponent(delete); | |||
return tableControls; | |||
} | |||
private Component buildFormControls() { | |||
formControls = new HorizontalLayout(); | |||
Button save = new Button("Save"); | |||
Button discard = new Button("Discard"); | |||
formControls.addComponent(save); | |||
formControls.addComponent(discard); | |||
return formControls; | |||
} | |||
.... | |||
The buttons doesn't do anything yet but we have all the components that | |||
we need in the view now. | |||
image:img/buttons%20added.png[Address form with add, delete, save and discard buttons] | |||
[[connecting-the-backend-to-the-view]] | |||
Connecting the backend to the view | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
The backend reference is store as a field so that all methods have | |||
access to it. | |||
[source,java] | |||
.... | |||
... | |||
private IBackend backend; | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
backend = new Backend(); | |||
... | |||
.... | |||
Then we have to build a container for the table. I will do it in a | |||
separate method from the table building so that it can be rebuilt for | |||
refreshing the table after the initial rendering. We call this method | |||
once in the initial rendering as well on every button click that | |||
modifies the list of persons. A good choice of container in this case is | |||
the `BeanItemContainer` where we specify to the table which columns we | |||
want to show, and sort the table based on the name. | |||
[source,java] | |||
.... | |||
... | |||
private Component buildTable() { | |||
table = new Table(null); | |||
table.setSelectable(true); | |||
table.setImmediate(true); | |||
updateTableData(); | |||
return table; | |||
} | |||
... | |||
private void updateTableData() { | |||
List<Person> persons = backend.getPersons(); | |||
BeanItemContainer<Person> container = new BeanItemContainer<Person>( | |||
Person.class, persons); | |||
table.setContainerDataSource(container); | |||
table.setVisibleColumns(new String[] { "firstName", "lastName", | |||
"phoneNumber", "email", "dateOfBirth" }); | |||
table.setColumnHeaders(new String[] { "First name", "Last name", | |||
"Phone number", "E-mail address", "Date of birth" }); | |||
table.sort(new Object[] { "firstName", "lastName" }, new boolean[] { | |||
true, true }); | |||
} | |||
... | |||
.... | |||
To get the data from the selected person's data into the fields, and the | |||
changes back into the bean, we will use a FieldGroup. The `FieldGroup` | |||
should be defined as class variable and it should bind the fields that | |||
is initialized in `buildForm()`. | |||
[source,java] | |||
.... | |||
... | |||
private FieldGroup fieldGroup = new FieldGroup(); | |||
... | |||
private Component buildForm() { | |||
form = new GridLayout(2, 3); | |||
TextField firstName = new TextField("First name:"); | |||
TextField lastName = new TextField("Last name:"); | |||
TextField phoneNumber = new TextField("Phone Number:"); | |||
TextField email = new TextField("E-mail address:"); | |||
DateField dateOfBirth = new DateField("Date of birth:"); | |||
TextArea comments = new TextArea("Comments:"); | |||
fieldGroup.bind(firstName, "firstName"); | |||
fieldGroup.bind(lastName, "lastName"); | |||
fieldGroup.bind(phoneNumber, "phoneNumber"); | |||
fieldGroup.bind(email, "email"); | |||
fieldGroup.bind(dateOfBirth, "dateOfBirth"); | |||
fieldGroup.bind(comments, "comments"); | |||
form.addComponent(firstName); | |||
form.addComponent(lastName); | |||
form.addComponent(phoneNumber); | |||
form.addComponent(email); | |||
form.addComponent(dateOfBirth); | |||
form.addComponent(comments); | |||
return form; | |||
} | |||
.... | |||
Additionally the table requires a value change listener and the | |||
currently selected person in the table has to be passed to the | |||
`FieldGroup`. | |||
[source,java] | |||
.... | |||
private Component buildTable() { | |||
... | |||
table.addValueChangeListener(new ValueChangeListener() { | |||
public void valueChange(ValueChangeEvent event) { | |||
editPerson((Person) table.getValue()); | |||
} | |||
}); | |||
... | |||
} | |||
... | |||
private void editPerson(Person person) { | |||
if (person == null) { | |||
person = new Person(); | |||
} | |||
BeanItem<Person> item = new BeanItem<Person>(person); | |||
fieldGroup.setItemDataSource(item); | |||
} | |||
.... | |||
[[putting-the-buttons-in-use]] | |||
Putting the buttons in use | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Last thing we have to do is implement all the buttons that we have in | |||
the application. Add should create a new Person object and give it to | |||
the form. Delete should tell the backend to remove the selected person | |||
and update the table. Save should store the changes into the bean and | |||
the bean into the backend and update the table. Discard should reset the | |||
form. | |||
[source,java] | |||
.... | |||
private Component buildTableControls() { | |||
tableControls = new HorizontalLayout(); | |||
Button add = new Button("Add", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
editPerson(new Person()); | |||
} | |||
}); | |||
Button delete = new Button("Delete", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
backend.deletePerson((Person) table.getValue()); | |||
updateTableData(); | |||
} | |||
}); | |||
tableControls.addComponent(add); | |||
tableControls.addComponent(delete); | |||
return tableControls; | |||
} | |||
private Component buildFormControls() { | |||
formControls = new HorizontalLayout(); | |||
Button save = new Button("Save", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
try { | |||
fieldGroup.commit(); | |||
backend.storePerson(((BeanItem<Person>) fieldGroup | |||
.getItemDataSource()).getBean()); | |||
updateTableData(); | |||
editPerson(null); | |||
} catch (CommitException e) { | |||
e.printStackTrace(); | |||
} | |||
} | |||
}); | |||
Button discard = new Button("Discard", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
fieldGroup.discard(); | |||
} | |||
}); | |||
formControls.addComponent(save); | |||
formControls.addComponent(discard); | |||
return formControls; | |||
} | |||
.... | |||
image:img/database%20connected.png[Form with database connected] | |||
That's it! Now you have a full working CRUD view with total control over | |||
the components and layouts. A little theming and layout adjustments and | |||
it is ready for production. | |||
You might have noticed that the person bean contains a reference to | |||
another bean, a address, which is not editable here. The tutorial | |||
link:CreatingACustomFieldForEditingTheAddressOfAPerson.asciidoc[Creating a custom field for editing the address of a person] goes | |||
through on how to edit beans within beans with a `CustomField`, which can | |||
be used directly as a field for the `FieldGroup`. |
@@ -0,0 +1,135 @@ | |||
[[creating-a-reusable-vaadin-theme-in-eclipse]] | |||
Creating a reusable Vaadin theme in Eclipse | |||
------------------------------------------- | |||
This tutorial teaches you how to create a standalone Vaadin theme that | |||
can be reused in other Vaadin projects as an add-on. | |||
*Requirements:* | |||
* https://www.eclipse.org/downloads/[Eclipse IDE for Java EE developers] | |||
* https://vaadin.com/eclipse/[Vaadin plug-in for Eclipse] | |||
[[create-a-project-for-your-theme]] | |||
Create a project for your theme | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Create a new Java project. | |||
image:img/New%20Java%20Project.png[Create a new Java project] | |||
https://vaadin.com/download[Download a Vaadin JAR] and add it to your | |||
project’s build path. | |||
image:img/Vaadin%20to%20build%20path.png[Add Vaadin to build path] | |||
In the src folder, create a class for your theme: | |||
[source,java] | |||
.... | |||
package com.example.mytheme; | |||
import com.vaadin.ui.themes.BaseTheme; | |||
public class MyTheme extends BaseTheme { | |||
public static final String THEME_NAME = "my-theme"; | |||
} | |||
.... | |||
This makes your theme extend Vaadin’s | |||
https://vaadin.com/api/com/vaadin/ui/themes/BaseTheme.html[BaseTheme], | |||
which will let you fully customize your theme from scratch. On the other | |||
hand, if you don't have very specific theming needs and just want | |||
good-looking results quickly, try extending | |||
https://vaadin.com/api/com/vaadin/ui/themes/ChameleonTheme.html[ChameleonTheme] | |||
instead. In any case, both of these themes are designed for extension | |||
and therefore your best choices to start with. | |||
In the root of your project, create the following folder and files: | |||
* META-INF | |||
** MANIFEST.MF | |||
* VAADIN | |||
** themes | |||
*** my-theme | |||
**** addons.scss | |||
**** my-theme.scss | |||
**** styles.scss | |||
The MANIFEST.MF file should contain the following: | |||
.... | |||
Manifest-Version: 1.0 | |||
Implementation-Title: My Theme | |||
Implementation-Version: 1.0.0 | |||
Vaadin-Package-Version: 1 | |||
Class-Path: | |||
.... | |||
Your `Implementation-Title` and `Implementation-Version` should reflect | |||
how you want your theme to be visible in the | |||
https://vaadin.com/directory[Vaadin directory]. | |||
[[create-a-demo-app-for-your-theme]] | |||
Create a demo app for your theme | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Create a new Vaadin project. | |||
image:img/New%20Vaadin%20project%20(1).png[Create a new Vaadin project] | |||
image:img/New%20Vaadin%20project%20(2).png[Create a new Vaadin project 2] | |||
Add your *theme* project to your *demo* project’s Java build path. | |||
image:img/Theme%20to%20build%20path.png[Add theme to build path] | |||
Add your *theme* project to your *demo* project’s _deployment assembly_. | |||
This will automatically convert your theme project to a Java EE Utility | |||
project. | |||
image:img/Theme%20to%20deployment%20assembly.png[Add theme to Deployment Assembly] | |||
Now that your theme project is a Java EE Utility project, it will also | |||
have a deployment assembly. Add your *theme project*’s VAADIN folder to | |||
there and make sure you specify its deploy path as `VAADIN`. | |||
image:img/VAADIN%20to%20deployment%20assembly.png[Add theme to Deployment Assembly 2] | |||
In your *demo* application class, add the following line to your | |||
`init()` method: | |||
[source,java] | |||
.... | |||
setTheme(MyTheme.THEME_NAME); | |||
.... | |||
To try if it works, right-click on your *demo* project and choose _Run | |||
As > Run on Server_. | |||
[[develop-your-theme]] | |||
Develop your theme | |||
~~~~~~~~~~~~~~~~~~ | |||
Create a new style name constant in your theme class for each new CSS | |||
class name you add to your stylesheets. These can then be passed to the | |||
`Component.addStyleName(String)` method. This will make it easier for | |||
other developers to use your theme! | |||
Changes to your stylesheets will be visible in your demo app almost | |||
instantly. All you need to do is save the file, wait for the server to | |||
automatically pick up the changes, then refresh your browser. | |||
[[export-your-theme-as-a-vaadin-add-on]] | |||
Export your theme as a Vaadin add-on | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Right-click on your *theme* project, choose _Export… > Java > Jar file_ | |||
and make sure your settings match those in the following two images. | |||
image:img/JAR%20Export%20(1).png[Export as JAR] | |||
image:img/JAR%20Export%20(2).png[Export as JAR 2] | |||
Finally, upload your theme add-on Jar to the | |||
https://vaadin.com/directory[Vaadin directory]! |
@@ -0,0 +1,63 @@ | |||
[[creating-a-textfield-for-integer-only-input-using-a-data-source]] | |||
Creating a TextField for integer only input using a data source | |||
--------------------------------------------------------------- | |||
A `TextField` is a component that always has a value of type `String`. When | |||
binding a property of another type to a text field, the value is | |||
automatically converted if the conversion between the two types is | |||
supported. | |||
[source,java] | |||
.... | |||
public class MyBean { | |||
private int value; | |||
public int getValue() { | |||
return value; | |||
} | |||
public void setValue(int integer) { | |||
value = integer; | |||
} | |||
} | |||
.... | |||
The property named "value" from a `BeanItem` constructed from `MyBean` will | |||
be of type `Integer`. Binding the property to a `TextField` will | |||
automatically make validation fail for texts that can not be converted | |||
to an `Integer`. | |||
[source,java] | |||
.... | |||
final MyBean myBean = new MyBean(); | |||
BeanItem<MyBean> beanItem = new BeanItem<MyBean>(myBean); | |||
final Property<Integer> integerProperty = (Property<Integer>) beanItem | |||
.getItemProperty("value"); | |||
final TextField textField = new TextField("Text field", integerProperty); | |||
Button submitButton = new Button("Submit value", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
String uiValue = textField.getValue(); | |||
Integer propertyValue = integerProperty.getValue(); | |||
int dataModelValue = myBean.getValue(); | |||
Notification.show("UI value (String): " + uiValue | |||
+ "\nProperty value (Integer): " + propertyValue | |||
+ "\nData model value (int): " + dataModelValue); | |||
} | |||
}); | |||
addComponent(new Label("Text field type: " + textField.getType())); | |||
addComponent(new Label("Text field type: " + integerProperty.getType())); | |||
addComponent(textField); | |||
addComponent(submitButton); | |||
.... | |||
With this example, entering a number and pressing the button causes the | |||
value of the `TextField` to be a `String`, the property value will be an | |||
`Integer` representing the same value and the value in the bean will be | |||
the same int. If e.g. a letter is entered to the field and the button is | |||
pressed, the validation will fail. This causes a notice to be displayed | |||
for the field. The field value is still updated, but the property value | |||
and the bean value are kept at their previous values. |
@@ -0,0 +1,44 @@ | |||
[[creating-a-textfield-for-integer-only-input-when-not-using-a-data-source]] | |||
Creating a TextField for integer only input when not using a data source | |||
------------------------------------------------------------------------ | |||
A `TextField` is a component that always has a value of type `String`. By | |||
adding a converter to a field, the field will automatically validate | |||
that the entered value can be converted and it will provide the | |||
converted value using the `getConvertedValue()` method. | |||
[source,java] | |||
.... | |||
final TextField textField = new TextField("Text field"); | |||
textField.setConverter(Integer.class); | |||
Button submitButton = new Button("Submit value", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
String uiValue = textField.getValue(); | |||
try { | |||
Integer convertedValue = (Integer) textField | |||
.getConvertedValue(); | |||
Notification.show( | |||
"UI value (String): " + uiValue | |||
+ "<br />Converted value (Integer): " | |||
+ convertedValue); | |||
} catch (ConversionException e) { | |||
Notification.show( | |||
"Could not convert value: " + uiValue); | |||
} | |||
} | |||
}); | |||
addComponent(new Label("Text field type: " + textField.getType())); | |||
addComponent(new Label("Converted text field type: " | |||
+ textField.getConverter().getModelType())); | |||
addComponent(textField); | |||
addComponent(submitButton); | |||
.... | |||
With this example, entering a number and pressing the button causes the | |||
value of the `TextField` to be a `String` while the converted value will be | |||
an `Integer` representing the same value. If e.g. a letter is entered to | |||
the field and the button is pressed, the validation will fail. This | |||
causes a notice to be displayed for the field and an exception to be | |||
thrown from `getConvertedValue()`. |
@@ -0,0 +1,71 @@ | |||
[[creating-an-application-with-different-features-for-different-clients]] | |||
Creating an application with different features for different clients | |||
--------------------------------------------------------------------- | |||
Providing different features for different clients can be done by | |||
creating a specialized UIProvider for the application. | |||
We start by creating the specialized UI's | |||
[source,java] | |||
.... | |||
public class DefaultUI extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
setContent( | |||
new Label("This browser does not support touch events")); | |||
} | |||
} | |||
.... | |||
[source,java] | |||
.... | |||
public class TouchUI extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
WebBrowser webBrowser = getPage().getWebBrowser(); | |||
String screenSize = "" + webBrowser.getScreenWidth() + "x" | |||
+ webBrowser.getScreenHeight(); | |||
setContent(new Label("Using a touch enabled device with screen size" | |||
+ screenSize)); | |||
} | |||
} | |||
.... | |||
We then define an UIProvider which knows what UI the application should | |||
return. | |||
[source,java] | |||
.... | |||
public class DifferentFeaturesForDifferentClients extends UIProvider { | |||
@Override | |||
public Class<? extends UI> getUIClass(UIClassSelectionEvent event) { | |||
// could also use browser version etc. | |||
if (event.getRequest().getHeader("user-agent").contains("mobile")) { | |||
return TouchUI.class; | |||
} else { | |||
return DefaultUI.class; | |||
} | |||
} | |||
} | |||
.... | |||
Now that we have an `UIProvider` we need to tell Vaadin to use it. This is | |||
most easily done by defining the `UIProvider` class in web.xml instead of | |||
defining a UI class. | |||
[source,xml] | |||
.... | |||
<servlet> | |||
<servlet-name>My Vaadin App</servlet-name> | |||
<servlet-class>com.vaadin.server.VaadinServlet</servlet-class> | |||
<init-param> | |||
<description>Vaadin UI</description> | |||
<param-name>UIProvider</param-name> | |||
<param-value>com.example.myexampleproject.DifferentFeaturesForDifferentClients</param-value> | |||
</init-param> | |||
</servlet> | |||
.... | |||
Each UI can have its own feature set, layout and theme. |
@@ -0,0 +1,103 @@ | |||
[[creating-your-own-converter-for-string-mytype-conversion]] | |||
Creating your own converter for String - MyType conversion | |||
---------------------------------------------------------- | |||
If you have custom types that you want to represent using the built in | |||
field components, you can easily create your own converter to take care | |||
of converting between your own type and the native data type of the | |||
field. | |||
A sample custom type, in this case a Name object with separate fields | |||
for first and last name. | |||
[source,java] | |||
.... | |||
public class Name { | |||
private String firstName; | |||
private String lastName; | |||
public Name(String firstName, String lastName) { | |||
this.firstName = firstName; | |||
this.lastName = lastName; | |||
} | |||
public String getFirstName() { | |||
return firstName; | |||
} | |||
public void setFirstName(String firstName) { | |||
this.firstName = firstName; | |||
} | |||
public String getLastName() { | |||
return lastName; | |||
} | |||
public void setLastName(String lastName) { | |||
this.lastName = lastName; | |||
} | |||
} | |||
.... | |||
A converter for the name, assuming the parts are separated with a space | |||
and that there are only two parts of a name. | |||
[source,java] | |||
.... | |||
public class StringToNameConverter implements Converter<String, Name> { | |||
public Name convertToModel(String text, Locale locale) | |||
throws ConversionException { | |||
if (text == null) { | |||
return null; | |||
} | |||
String[] parts = text.split(" "); | |||
if (parts.length != 2) { | |||
throw new ConversionException("Can not convert text to a name: " + text); | |||
} | |||
return new Name(parts[0], parts[1]); | |||
} | |||
public String convertToPresentation(Name name, Locale locale) | |||
throws ConversionException { | |||
if (name == null) { | |||
return null; | |||
} else { | |||
return name.getFirstName() + " " + name.getLastName(); | |||
} | |||
} | |||
public Class<Name> getModelType() { | |||
return Name.class; | |||
} | |||
public Class<String> getPresentationType() { | |||
return String.class; | |||
} | |||
} | |||
.... | |||
Hooking up the Name type and its Converter to a TextField can then be | |||
done like this | |||
[source,java] | |||
.... | |||
Name name = new Name("Rudolph", "Reindeer"); | |||
final TextField textField = new TextField("Name"); | |||
textField.setConverter(new StringToNameConverter()); | |||
textField.setConvertedValue(name); | |||
addComponent(textField); | |||
addComponent(new Button("Submit value", new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
try { | |||
Name name = (Name) textField.getConvertedValue(); | |||
Notification.show( | |||
"First name: " + name.getFirstName() + | |||
"<br />Last name: " + name.getLastName()); | |||
} catch (ConversionException e) { | |||
Notification.show(e.getCause().getMessage()); | |||
} | |||
} | |||
})); | |||
.... |
@@ -0,0 +1,44 @@ | |||
[[finding-the-current-root-and-application]] | |||
Finding the current root and application | |||
---------------------------------------- | |||
There are many cases where you need a reference to the active | |||
`Application` or `Root`, for instance for showing notifications in a click | |||
listener. It is possible to get a reference to the component from the | |||
event and then a reference from the component to the `Root` but Vaadin | |||
also offers an easier way through two static methods: | |||
[source,java] | |||
.... | |||
Root.getCurrent() | |||
Application.getCurrent() | |||
.... | |||
For example when you want to show the name of the current Root class: | |||
[source,java] | |||
.... | |||
Button helloButton = new Button("Say Hello"); | |||
helloButton.addListener(new ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
Notification.show("This Root is " + Root.getCurrent().getClass().getSimpleName()); | |||
} | |||
}); | |||
.... | |||
Similarly for `Application`, for instance to find out if the application | |||
is running in production mode: | |||
[source,java] | |||
.... | |||
public void buttonClick(ClickEvent event) { | |||
String msg = "Running in "; | |||
msg += Application.getCurrent().isProductionMode() ? | |||
"production" : "debug"; | |||
msg += " mode"; | |||
Notification.show(msg); | |||
} | |||
.... | |||
*Note* that these are based on `ThreadLocal` so they won't work in a | |||
background thread (or otherwise outside the standard request scope). |
@@ -0,0 +1,177 @@ | |||
[[formatting-data-in-grid]] | |||
Formatting data in grid | |||
----------------------- | |||
Without any special configuration, Grid tries to find a `Converter` for | |||
converting the property value into a String that can be shown in the | |||
browser. The `ConverterFactory` configured for the session is used for | |||
this purpose. If no compatible converter is found, Grid will instead | |||
fall back to using `toString()` on the property value. | |||
[[cellstylegenerator]] | |||
CellStyleGenerator | |||
^^^^^^^^^^^^^^^^^^ | |||
Grid does also provide a couple of mechanisms for fine-tuning how the | |||
data is displayed. The simplest way of controlling the data output is to | |||
use a `CellStyleGenerator` to add custom stylenames to individual cells, | |||
thus affecting which CSS rules from the theme are applied to each cell. | |||
[source,java] | |||
.... | |||
grid.setCellStyleGenerator(new CellStyleGenerator() { | |||
@Override | |||
public String getStyle(CellReference cellReference) { | |||
if ("amount".equals(cellReference.getPropertyId())) { | |||
Double value = (Double) cellReference.getValue(); | |||
if (value.doubleValue() == Math.round(value.doubleValue())) { | |||
return "integer"; | |||
} | |||
} | |||
return null; | |||
} | |||
}); | |||
getPage().getStyles().add(".integer { color: blue; }"); | |||
.... | |||
We have not yet defined any `Converter` or `Renderer` in this example. This | |||
means that Grid will use a `StringToDoubleConverter` to convert the Double | |||
values from the data source into Strings that are sent to the browser | |||
and displayed in each cell. | |||
To keep this example as simple as possible, we are dynamically injecting | |||
new CSS styles into the page. In a real application, it's recommended to | |||
instead add the styles to the theme since that helps with separation of | |||
concerns. | |||
[[renderer]] | |||
Renderer | |||
^^^^^^^^ | |||
The next thing you can do to control how the data is displayed is to use | |||
a `Renderer`. The `Renderer` will receive the value from the data source | |||
property, possibly after it has been converted to the `Renderer`{empty}'s input | |||
type using a `Converter`. The `Renderer` is will then make sure the value | |||
gets show in an appropriate way in the browser. A simple renderer might | |||
just show the data as text, but there are also more complex `Renderer`{empty}s | |||
that e.g. show a numerical value as a progress bar. | |||
Will will use a `NumberRenderer` using a currency format to render the | |||
cell values for the `Amount` column. To do this, we simply create and | |||
configure it and then set it as the `Renderer` for the designated column. | |||
No `Converter` will be used in this case since `NumberRenderer` already | |||
knows ho to handle values of the type Double. | |||
[source,java] | |||
.... | |||
NumberFormat poundformat = NumberFormat.getCurrencyInstance(Locale.UK); | |||
NumberRenderer poundRenderer = new NumberRenderer(poundformat); | |||
grid.getColumn("amount").setRenderer(poundRenderer); | |||
.... | |||
[[converter]] | |||
Converter | |||
^^^^^^^^^ | |||
The last way of controlling how data is displayed is to use a `Converter`. | |||
The framework will in most cases find and use a suitable `Converter`, but | |||
you can also supply your own if the default conversion is not what you | |||
need. The following example uses a custom `Converter` for the `Count` column | |||
to change the value into HTML strings with different HTML for even and | |||
odd counts. Grid will by default protect you from cross-site scripting | |||
vulnerabilities by not interpreting HTML in cell values. This can be | |||
overridden by setting the column to use a `HtmlRenderer` instead of the | |||
default `TextRenderer`. Both those renderers expect String values. Since | |||
we will not be editing any values, the Converter doesn't need to support | |||
changing the HTML strings back into integers. | |||
[source,java] | |||
.... | |||
grid.getColumn("count").setConverter(new StringToIntegerConverter() { | |||
@Override | |||
public String convertToPresentation(Integer value, Class<? extends String> targetType, Locale locale) | |||
throws Converter.ConversionException { | |||
String stringRepresentation = super.convertToPresentation(value, targetType, locale); | |||
if (value.intValue() % 2 == 0) { | |||
return "<strong>" + stringRepresentation + "</strong>"; | |||
} else { | |||
return "<em>" + stringRepresentation + "</em>"; | |||
} | |||
} | |||
}); | |||
grid.getColumn("count").setRenderer(new HtmlRenderer()); | |||
.... | |||
[[full-example]] | |||
Full example | |||
^^^^^^^^^^^^ | |||
Putting all these pieces together, we end up with this class that uses | |||
the same data as in the link:UsingGridWithAContainer.asciidoc[Using | |||
Grid with a Container] example. | |||
[source,java] | |||
.... | |||
import java.text.NumberFormat; | |||
import java.util.Locale; | |||
import com.vaadin.annotations.Theme; | |||
import com.vaadin.data.util.converter.Converter; | |||
import com.vaadin.data.util.converter.StringToIntegerConverter; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.Grid.CellReference; | |||
import com.vaadin.ui.Grid.CellStyleGenerator; | |||
import com.vaadin.ui.UI; | |||
import com.vaadin.ui.renderers.HtmlRenderer; | |||
import com.vaadin.ui.renderers.NumberRenderer; | |||
@Theme("valo") | |||
public class FormattingDataInGrid extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
Grid grid = new Grid(GridExampleHelper.createContainer()); | |||
setContent(grid); | |||
grid.setCellStyleGenerator(new CellStyleGenerator() { | |||
@Override | |||
public String getStyle(CellReference cellReference) { | |||
if ("amount".equals(cellReference.getPropertyId())) { | |||
Double value = (Double) cellReference.getValue(); | |||
if (value.doubleValue() == Math.round(value.doubleValue())) { | |||
return "integer"; | |||
} | |||
} | |||
return null; | |||
} | |||
}); | |||
getPage().getStyles().add(".integer { color: blue; }"); | |||
NumberFormat poundformat = NumberFormat.getCurrencyInstance(Locale.UK); | |||
NumberRenderer poundRenderer = new NumberRenderer(poundformat); | |||
grid.getColumn("amount").setRenderer(poundRenderer); | |||
grid.getColumn("count").setConverter(new StringToIntegerConverter() { | |||
@Override | |||
public String convertToPresentation(Integer value, | |||
Class<? extends String> targetType, Locale locale) | |||
throws Converter.ConversionException { | |||
String stringRepresentation = super.convertToPresentation( | |||
value, targetType, locale); | |||
if (value.intValue() % 2 == 0) { | |||
return "<strong>" + stringRepresentation + "</strong>"; | |||
} else { | |||
return "<em>" + stringRepresentation + "</em>"; | |||
} | |||
} | |||
}); | |||
grid.getColumn("count").setRenderer(new HtmlRenderer()); | |||
} | |||
} | |||
.... |
@@ -0,0 +1,405 @@ | |||
[[how-to-test-vaadin-web-application-performance-with-jmeter]] | |||
How to test Vaadin web application performance with JMeter | |||
---------------------------------------------------------- | |||
This article describes how to make load testing of your Vaadin web | |||
application with http://jakarta.apache.org/jmeter/[JMeter]. | |||
[[get-the-latest-jmeter]] | |||
Get the latest JMeter | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
Download JMeter from http://jmeter.apache.org/download_jmeter.cgi | |||
[[configure-jmeter]] | |||
Configure JMeter | |||
~~~~~~~~~~~~~~~~ | |||
Unzip the apache-jmeter-x.x.x.zip file. | |||
Edit `JMETERHOME/bin/jmeter.bat` (or `jmeter.sh`) and check that the JVM | |||
memory parameters are ok (e.g. `set HEAP=-Xms512m -Xmx1500m -Xss128k`). | |||
The maximum heap size (`-Xmx`) should be at least 1024m. I would also | |||
recommend that the thread stack size is set to 512k or below if a large | |||
number of threads is used in testing. | |||
You should read this to ensure you follow best-practices: | |||
* http://jmeter.apache.org/usermanual/best-practices.html + | |||
* http://www.ubik-ingenierie.com/blog/jmeter_performance_tuning_tips/ | |||
[[start-jmeter]] | |||
Start JMeter | |||
~~~~~~~~~~~~ | |||
E.g. double clicking jmeter.bat | |||
[[configure-your-test-plan-and-workbench]] | |||
Configure your Test Plan and WorkBench | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Right click the WorkBench icon and select 'Add' -> 'Non-Test Elements' | |||
-> 'HTTP(S) Test Script Recorder'. Edit the Recorder parameters and set | |||
Port: to e.g. 9999 (you could leave it to 8080 if your web application | |||
servers do not use the port 8080). Typically requests related to loading | |||
static web content like css files and images are excluded from the load | |||
test. You can use 'Url Patterns to Exclude' section of the recorder to | |||
define what content is excluded. For instance to exclude css files add | |||
following pattern: `.*\.css` | |||
image:img/jm1B.png[JMeter patterns to exclude] | |||
Right click the Recorder icon and select 'Add' -> 'Timer' -> 'Uniform | |||
random timer'. Configure timer by setting the *Constant Delays Offset | |||
into $\{T}*. This setting means that JMeter records also the delays | |||
between the http requests. You could also test without the timer but | |||
with the timer your test is more realistic. | |||
image:img/jm3B.png[JMeter uniform random timer] | |||
Optionally you could also add 'View Result Tree' listener under the | |||
Recorder. With 'View Result Tree' listener it is possible to inspect | |||
every recorded request and response. | |||
*Note since JMeter you can do this in one step using menu item | |||
"Templates..." and selecting "Recording" template.* | |||
image:img/jm2B.png[JMeter View Results Tree] | |||
Next, configure the Test Plan. | |||
Add a 'Thread Group' to it and then add a 'Config Element' -> 'HTTP | |||
Cookie Manager' to the thread group. Set Cookie policy of the cookie | |||
manager to be 'compatibility'. *Remember also to set the "Clear cookies | |||
each iteration" setting to 'checked'*. Add also a 'Config Element' -> | |||
'HTTP Request Defaults' into Thread group. | |||
You could also add a 'Config Element' -> 'User Defined Variables' and a | |||
'Logic Controller' -> 'Recording Controller' into Thread Group. | |||
Your JMeter should now looks something like the screenshot below: | |||
image:img/jm4.png[JMeter User Defined Variables] | |||
[[configure-your-vaadin-application]] | |||
Configure your Vaadin application | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
[[disable-the-xsrf-protection]] | |||
Disable the xsrf-protection | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
In Vaadin you have to disable the xsrf-protection of your application or | |||
otherwise the JMeter test may fail. The way how xsrf protection is | |||
disabled differs in Vaadin 6 and Vaadin 7. | |||
*In Vaadin 7* | |||
If you use web.xml in your Vaadin 7 project, add the following | |||
context-parameter in the web.xml or optionally add it as an init | |||
parameter just like in the Vaadin 6 project below. | |||
[source,xml] | |||
.... | |||
<context-param> | |||
<param-name>disable-xsrf-protection</param-name> | |||
<param-value>true</param-value> | |||
</context-param> | |||
.... | |||
If you use annotation based (Servlet 3.0) Vaadin servlet configuration, | |||
you can currently (in Vaadin 7.1.11) either fall back to Servlet 2.4 | |||
web.xml configuration or set the parameter value for | |||
'disable-xsrf-protection' as a `java.lang.System property` before the | |||
Vaadin's `DefaultDeploymentConfiguration` is loaded. This can be done for | |||
example by extending `VaadinServlet` class. At the end of this Wiki | |||
article there is an example servlet (`JMeterServlet`) that implements this | |||
functionality. See the example code below for how to replace the default | |||
`VaadinServlet` with your custom `VaadinServlet` in the UI class. | |||
[source,java] | |||
.... | |||
public class YourUI extends UI { | |||
@WebServlet(value = "/*", asyncSupported = true) | |||
@VaadinServletConfiguration(productionMode = false, ui = YourUI.class) | |||
public static class Servlet extends JMeterServlet { | |||
} | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
//... | |||
} | |||
.... | |||
*In Vaadin 6* | |||
See the example below for how to disable the protection from the web.xml | |||
file: | |||
[source,xml] | |||
.... | |||
<servlet> | |||
<servlet-name>FeatureBrowser</servlet-name> | |||
<servlet-class>com.vaadin.terminal.gwt.server.ApplicationServlet</servlet-class> | |||
<init-param> | |||
<param-name>application</param-name> | |||
<param-value>com.vaadin.demo.featurebrowser.FeatureBrowser</param-value> | |||
</init-param> | |||
<init-param> | |||
<param-name>disable-xsrf-protection</param-name> | |||
<param-value>true</param-value> | |||
</init-param> | |||
</servlet> | |||
.... | |||
*Important! Remember to enable the protection after the testing is | |||
done!* | |||
[[disabling-syncid-happens-with-similar-parameter]] | |||
Disabling syncId happens with similar parameter | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
[source,xml] | |||
.... | |||
<context-param> | |||
<param-name>syncId</param-name> | |||
<param-value>false</param-value> | |||
</context-param> | |||
.... | |||
If you want to do the above with Java Servlet 3.0 annotations, use the | |||
following: | |||
[source,java] | |||
.... | |||
initParams = { | |||
@WebInitParam(name = "disable-xsrf-protection", value = "true"), | |||
@WebInitParam(name = "syncIdCheck", value = "false")} | |||
.... | |||
[[use-debug-ids-within-your-vaadin-application]] | |||
Use debug id:s within your Vaadin application | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Normally a Vaadin application sets a sequential id for each user | |||
interface component of the application. These ids are used in the | |||
ajax-requests when the component state is synchronized between the | |||
server and the client side. The aforementioned id sequence is likely the | |||
same between different runs of the application, but this is not | |||
guaranteed. *In Vaadin 6* these ids can be manually set by calling | |||
http://vaadin.com/api/com/vaadin/ui/AbstractComponent.html#setDebugId%28java.lang.String%29[`setDebugId()`] | |||
method. | |||
*In Vaadin 7* there no more exists a `setDebugId()` method; instead there | |||
is | |||
https://vaadin.com/api/com/vaadin/ui/Component.html#setId(java.lang.String)[`setId()`] | |||
method. Unfortunately this method won't set component ids used in the | |||
ajax-request. Therefore, by default, JMeter tests of a Vaadin 7 | |||
application are not stable to UI changes. To overcome this problem you | |||
can use our `JMeterServlet` (see the end of this article) instead of the | |||
default `VaadinServlet`. When using the `JMeterServlet` component ids are | |||
again used in the ajax requests. See example above for how to replace | |||
default `VaadinServlet` with JMeterServlet. For additional information, | |||
see the Vaadin ticket http://dev.vaadin.com/ticket/13396[#13396]. | |||
[[use-named-windows-in-your-application]] | |||
Use named windows in your application | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Setting the name for the Windows *in the Vaadin (< 6.4.X)* application | |||
is important since otherwise these names are randomly generated. Window | |||
name could be set using the `setName()`{empty}-method. | |||
[[configure-your-browser]] | |||
Configure your browser | |||
~~~~~~~~~~~~~~~~~~~~~~ | |||
Since JMeter is used as a proxy server, you have to configure the proxy | |||
settings of your browser. You can find the proxy settings of Firefox | |||
from Tools -> Options -> Connections -> Settings: 'Manual proxy | |||
configuration'. Set the correct IP of your computer (or 'localhost' | |||
string) and the same port that you set into proxy server settings above. | |||
[[start-recording]] | |||
Start recording | |||
~~~~~~~~~~~~~~~ | |||
Start your web application server. Start the proxy server from the | |||
JMeter. Open the URL of your web application into the browser configured | |||
above. You should append `?restartApplication` to the URL used when | |||
recording the tests to make sure that the UI gets initialized properly. | |||
Thus the URL becomes something like | |||
(http://localhost:8080/test/TestApplication/?restartApplication). If | |||
everything is ok your web application opens normally and you can see how | |||
the different HTTP requests appear into JMeter's thread group (see | |||
screenshot below). When you have done the recording, stop the proxy | |||
server. | |||
image:img/jm5.png[JMeter Thread Groups] | |||
[[performance-testing]] | |||
Performance testing | |||
~~~~~~~~~~~~~~~~~~~ | |||
[[clean-up-the-recorded-request]] | |||
Clean up the recorded request | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Before you start the test, you may have to delete the first timer object | |||
which is located below the first HTTP request in the thread group since | |||
its time delay may be unrealistically big (see the screenshot above). | |||
*It is also very much recommended to check the recorded data and delete | |||
all unessential requests.* | |||
[[detecting-out-of-sync-errors]] | |||
Detecting Out of Sync errors | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
If your test results in the application being in an Out of Sync error | |||
state it is not by default detected by JMeter (because the response code | |||
is still HTTP/1.1 200 OK). To make an assertion for detecting this kind | |||
of error you should add a Response Assertion to your test plan. | |||
Right-click on the thread group and select Add -> Assertions -> Response | |||
Assertion. Configure the assertion to assert that the Text Response does | |||
NOT contain a pattern "Out of sync". | |||
[[optional-parameterization-of-the-request]] | |||
Optional parameterization of the request | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Sometimes, it is useful to parameterize the recorded requests. | |||
Parameterization of a request is easily done in JMeter: | |||
1. add a "User Defined Variables"-element into the first place of your Test Plan. | |||
2. Copy paste the whole parameter value of wanted UIDL-request into the | |||
newly made variable (e.g. `PARAM1`). | |||
3. Replace the value of the UIDL-request with the parameter reference (e.g. `${PARAM1}`). | |||
[[start-testing]] | |||
Start testing | |||
^^^^^^^^^^^^^ | |||
Now, it is time to do the actual testing. Configure the thread group | |||
with proper 'Number of Threads' (e.g. 100) and set also the 'Ramp-Up | |||
Period' to some realistic value (e.g. 120). Then, add e.g. 'Listener' -> | |||
'Graph Results' to monitor how your application is performing. Finally, | |||
start the test from the Run -> Start. | |||
[[stop-on-error]] | |||
Stop on Error | |||
^^^^^^^^^^^^^ | |||
When you are pushing your Vaadin application to the limits, you might | |||
get into a situation where some of the UIDL requests fail. Because of | |||
the server-driven nature of Vaadin, it's likely that subsequent requests | |||
will cause errors like "_Warning: Ignoring variable change for | |||
non-existent component_", as the state stored on the server-side is no | |||
longer in sync with the JMeter test script. In these cases, it's often | |||
best to configure your JMeter thread group to stop the thread on sampler | |||
error. However, if you have configured your test to loop, you might want | |||
to still continue (and ignore the errors), if the next iteration will | |||
start all over again with fresh state. | |||
[[continuous-integration]] | |||
Continuous Integration | |||
^^^^^^^^^^^^^^^^^^^^^^ | |||
If you want to integrate load testing in your CI, you can use this | |||
http://jmeter.lazerycode.com/[plugin]. | |||
You can read this for full integration with Jenkins : | |||
* https://blog.codecentric.de/en/2014/01/automating-jmeter-tests-maven-jenkins/ | |||
[[jmeterservlet]] | |||
JMeterServlet | |||
^^^^^^^^^^^^^ | |||
In Vaadin 7 we recommend using the following or similar customized | |||
`VaadinServlet`. | |||
[source,java] | |||
.... | |||
package com.example.vaadin7jmeterservlet; | |||
import com.vaadin.server.ClientConnector; | |||
import com.vaadin.server.DeploymentConfiguration; | |||
import com.vaadin.server.ServiceException; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.server.VaadinService; | |||
import com.vaadin.server.VaadinServlet; | |||
import com.vaadin.server.VaadinServletService; | |||
import com.vaadin.server.VaadinSession; | |||
import com.vaadin.ui.Component; | |||
/** | |||
* @author Marcus Hellberg (marcus@vaadin.com) | |||
* Further modified by Johannes Tuikkala (johannes@vaadin.com) | |||
*/ | |||
public class JMeterServlet extends VaadinServlet { | |||
private static final long serialVersionUID = 898354532369443197L; | |||
public JMeterServlet() { | |||
System.setProperty(getPackageName() + "." + "disable-xsrf-protection", | |||
"true"); | |||
} | |||
@Override | |||
protected VaadinServletService createServletService( | |||
DeploymentConfiguration deploymentConfiguration) | |||
throws ServiceException { | |||
JMeterService service = new JMeterService(this, deploymentConfiguration); | |||
service.init(); | |||
return service; | |||
} | |||
private String getPackageName() { | |||
String pkgName; | |||
final Package pkg = this.getClass().getPackage(); | |||
if (pkg != null) { | |||
pkgName = pkg.getName(); | |||
} else { | |||
final String className = this.getClass().getName(); | |||
pkgName = new String(className.toCharArray(), 0, | |||
className.lastIndexOf('.')); | |||
} | |||
return pkgName; | |||
} | |||
public static class JMeterService extends VaadinServletService { | |||
private static final long serialVersionUID = -5874716650679865909L; | |||
public JMeterService(VaadinServlet servlet, | |||
DeploymentConfiguration deploymentConfiguration) | |||
throws ServiceException { | |||
super(servlet, deploymentConfiguration); | |||
} | |||
@Override | |||
protected VaadinSession createVaadinSession(VaadinRequest request) | |||
throws ServiceException { | |||
return new JMeterSession(this); | |||
} | |||
} | |||
public static class JMeterSession extends VaadinSession { | |||
private static final long serialVersionUID = 4596901275146146127L; | |||
public JMeterSession(VaadinService service) { | |||
super(service); | |||
} | |||
@Override | |||
public String createConnectorId(ClientConnector connector) { | |||
if (connector instanceof Component) { | |||
Component component = (Component) connector; | |||
return component.getId() == null ? super | |||
.createConnectorId(connector) : component.getId(); | |||
} | |||
return super.createConnectorId(connector); | |||
} | |||
} | |||
} | |||
.... |
@@ -0,0 +1,185 @@ | |||
[[jasper-reports-on-vaadin-sample]] | |||
Jasper reports on Vaadin sample | |||
------------------------------ | |||
[[introduction]] | |||
Introduction | |||
~~~~~~~~~~~~ | |||
I meet JasperReports some years ago and I liked this report library; | |||
this year I did need to implement a report on a personal project using | |||
Vaadin, but surprisingly I was not able to found a sample of this, so I | |||
did this little sample and article. | |||
First, you will need a JDK Maven and Mysql in order to try the sample, | |||
and you can download the code here: | |||
http://sourceforge.net/projects/jrtutorial/files/VaadinJRSample/ | |||
There is a README.txt file you can follow in order to run the sample, | |||
basically you need to: | |||
1. Create database running resources/database.sql on Mysql or MariaDB | |||
2. Compile the entire project: run "mvn install”. | |||
3. Deploy the application in Jetty: run "mvn jetty:run" | |||
4. Go to http://localhost:8080/ in your browser | |||
[[implementation]] | |||
Implementation | |||
~~~~~~~~~~~~~~ | |||
Let’s see the sample code step by step. + | |||
The data is only a _person_ table with some data. + | |||
The main class _MyUI.java_ has two UI components (the report generating | |||
button and a list component used to show current data in database.): | |||
[source,java] | |||
.... | |||
final Button reportGeneratorButton = new Button("Generate report"); | |||
… | |||
layout.addComponent(reportGeneratorButton); | |||
layout.addComponent(new PersonList()); | |||
.... | |||
The list is implemented on _PersonList.java_, I am using a | |||
_FilteringTable_ (https://vaadin.com/directory/component/filteringtable), | |||
that loads the data using a Vaadin _SQLContainer_: | |||
[source,java] | |||
.... | |||
SQLContainer container=null; | |||
… | |||
TableQuery tq = new TableQuery("person", new ConnectionUtil().getJDBCConnectionPool()); | |||
container = new SQLContainer(tq); | |||
filterTable = buildPagedTable(container); | |||
.... | |||
And the _SQLContainer_ is provided with a _JDBCConnectionPool_ created | |||
from a properties file (_resources/database.properties_): | |||
[source,java] | |||
.... | |||
Properties prop=PropertiesUtil.getProperties(); | |||
… | |||
public JDBCConnectionPool getJDBCConnectionPool(){ | |||
JDBCConnectionPool pool = null; | |||
try { | |||
pool = new SimpleJDBCConnectionPool( | |||
prop.getProperty("database.driver"), | |||
prop.getProperty("database.url"), | |||
prop.getProperty("database.userName"), | |||
prop.getProperty("database.password")); | |||
} catch (SQLException e) { | |||
e.printStackTrace(); | |||
} | |||
return pool; | |||
.... | |||
The report generation is implemented on _ReportGenerator_ class, this | |||
class loads the report template: | |||
[source,java] | |||
.... | |||
File templateFile=new File(templatePath); | |||
JasperDesign jasperDesign = JRXmlLoader.load(templateFile); | |||
.... | |||
Compile report template: | |||
[source,java] | |||
.... | |||
jasperReport = JasperCompileManager.compileReport(jasperDesign); | |||
.... | |||
Fill report with data: | |||
[source,java] | |||
.... | |||
HashMap fillParameters=new HashMap(); | |||
JasperPrint jasperPrint = JasperFillManager.fillReport( | |||
jasperReport, | |||
fillParameters, | |||
conn); | |||
.... | |||
Export the _jasperPrint_ object to Pdf format: | |||
[source,java] | |||
.... | |||
JRPdfExporter exporter = new JRPdfExporter(); | |||
exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); | |||
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputStream)); | |||
exporter.exportReport(); | |||
.... | |||
And finally execute all the logic to generate the report and sent it to | |||
an _OutputStream_: | |||
[source,java] | |||
.... | |||
JasperDesign jasperDesign=loadTemplate(templatePath); | |||
setTempDirectory(templatePath); | |||
JasperReport jasperReport=compileReport(jasperDesign); | |||
JasperPrint jasperPrint=fillReport(jasperReport, conn); | |||
exportReportToPdf(jasperPrint, outputStream); | |||
.... | |||
But all the logic at _ReportGenerator.java_ is called from the | |||
_ReportUtil_ class, this class is the responsible to connect Vaadin | |||
layer with _ReportGenerator_ layer. There are two methods: the first one | |||
is _prepareForPdfReport_, this method creates a database connection, | |||
generates the report as a StreamResource (calling the another method) | |||
and finally extends the source button with a _FileDownloader_ component | |||
in order to upload the generated report stream, so all the uploading | |||
magic is done by _FileDownloader_ extension | |||
(https://vaadin.com/api/com/vaadin/server/FileDownloader.html): | |||
[source,java] | |||
.... | |||
Connection conn=new ConnectionUtil().getSQLConnection(); | |||
reportOutputFilename+=("_"+getDateAsString()+".pdf"); | |||
StreamResource myResource =createPdfResource(conn,reportTemplate,reportOutputFilename); | |||
FileDownloader fileDownloader = new FileDownloader(myResource); | |||
fileDownloader.extend(buttonToExtend); | |||
.... | |||
The second method _createPdfResource_, uses _ReportGenerator_ class in | |||
order to return the generated report as a _StreamResource_: | |||
[source,java] | |||
.... | |||
return new StreamResource(new StreamResource.StreamSource() { | |||
@Override | |||
public InputStream getStream () { | |||
ByteArrayOutputStream pdfBuffer = new ByteArrayOutputStream(); | |||
ReportGenerator reportGenerator=new ReportGenerator(); | |||
try { | |||
reportGenerator.executeReport(baseReportsPath+templatePath, conn, pdfBuffer); | |||
} catch (JRException e) { | |||
e.printStackTrace(); | |||
} | |||
return new ByteArrayInputStream( | |||
pdfBuffer.toByteArray()); | |||
} | |||
}, reportFileName); | |||
.... | |||
So, in order to call the report generator process when only need to call | |||
ReportUtil like we did in ‘MyUI.java’: | |||
[source,java] | |||
.... | |||
final Button reportGeneratorButton = new Button("Generate report"); | |||
new ReportsUtil().prepareForPdfReport("/reports/PersonListReport.jrxml", | |||
"PersonList", | |||
reportGeneratorButton); | |||
.... | |||
Finally, the jasper report design can be found in the | |||
_WEB-INF/personListReport.jrxml_ file | |||
This is a picture of the sample running and the generated report: | |||
image:img/VaadinJasperReportsSample_small.jpg[Running sample] | |||
And that’s all, I expect to help someone with this sample, thanks for | |||
reading. |
@@ -0,0 +1,462 @@ | |||
[[lazy-query-container]] | |||
Lazy query container | |||
-------------------- | |||
[[when-to-use-lazy-query-container]] | |||
When to Use Lazy Query Container? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Typical usage scenario is browsing a large persistent data set in Vaadin | |||
Table. LQC minimizes the complexity of the required custom | |||
implementation while retaining all table features like sorting and lazy | |||
loading. LQC delegates sorting of the data set to the backend data store | |||
instead of sorting in memory. Sorting in memory would require entire | |||
data set to be loaded to application server. | |||
[[what-is-lazy-loading]] | |||
What is Lazy Loading? | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
In this context lazy loading refers to loading items to table on demand | |||
in batches instead of loading the entire data set to memory at once. | |||
This is useful in most business applications as row counts often range | |||
from thousands to millions. Loading more than few hundred rows to memory | |||
often causes considerable delay in page response. | |||
[[getting-started]] | |||
Getting Started | |||
~~~~~~~~~~~~~~~ | |||
To use LQC you need to get the add-on from add-ons page and drop it to | |||
your projects WEB-INF/lib directory. After this you can use existing | |||
query factory (`JpaQueryFactory`), extend `AbstractBeanQuery` or proceed to | |||
implement custom `Query` and `QueryFactory`. Finally you need to instantiate | |||
`LazyQueryContainer` and give your query factory as constructor parameter. | |||
[[how-to-use-lazy-query-container-with-table]] | |||
How to Use Lazy Query Container with Table? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
LQC is implementation of Vaadin Container interface. Please refer to | |||
Book of Vaadin for usage details of Table and Container implementations | |||
generally. | |||
[[how-to-use-entitycontainer]] | |||
How to Use EntityContainer | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
`EntityContainer` is specialized version `LazyQueryContainer` allowing easy | |||
use of JPA as persistence layer and supports defining where criteria and | |||
corresponding parameter map in addition to normal `LazyQueryContainer` | |||
features: | |||
[source,java] | |||
.... | |||
entityContainer = new EntityContainer<Task>(entityManager, true, Task.class, 100, | |||
new Object[] { "name" }, new boolean[] { true }); | |||
entityContainer.addContainerProperty("name", String.class, "", true, true); | |||
entityContainer.addContainerProperty("reporter", String.class, "", true, true); | |||
entityContainer.addContainerProperty("assignee", String.class, "", true, true); | |||
whereParameters = new HashMap<String, Object>(); | |||
whereParameters.put("name", nameFilter); | |||
entityContainer.filter("e.name=:name", whereParameters); | |||
table.setContainerDataSource(entityContainer); | |||
.... | |||
[[how-to-use-beanqueryfactory]] | |||
How to Use BeanQueryFactory | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
`BeanQueryFactory` and `AbstractBeanQuery` are used to implement queries | |||
saving and loading JavaBeans. | |||
The `BeanQueryFactory` is used as follows with the Vaadin table. Usage of | |||
`queryConfiguration` is optional and enables passing objects to the | |||
constructed queries: | |||
[source,java] | |||
.... | |||
Table table = new Table(); | |||
BeanQueryFactory<TaskBeanQuery> queryFactory = new | |||
BeanQueryFactory<TaskBeanQuery>(TaskBeanQuery.class); | |||
Map<String,Object> queryConfiguration = new HashMap<String,Object>(); | |||
queryConfiguration.put("taskService",new TaskService()); | |||
queryFactory.setQueryConfiguration(queryConfiguration); | |||
LazyQueryContainer container = new LazyQueryContainer(queryFactory,50); | |||
table.setContainerDataSource(container); | |||
.... | |||
Here is a simple example of `AbstractBeanQuery` implementation: | |||
[source,java] | |||
.... | |||
public class TaskBeanQuery extends AbstractBeanQuery<Task> { | |||
public TaskBeanQuery(QueryDefinition definition, | |||
Map<String, Object> queryConfiguration, Object[] sortPropertyIds, | |||
boolean[] sortStates) { | |||
super(definition, queryConfiguration, sortPropertyIds, sortStates); | |||
} | |||
@Override | |||
protected Task constructBean() { | |||
return new Task(); | |||
} | |||
@Override | |||
public int size() { | |||
TaskService taskService = | |||
(TaskService)queryConfiguration.get("taskService"); | |||
return taskService.countTasks(); | |||
} | |||
@Override | |||
protected List<Task> loadBeans(int startIndex, int count) { | |||
TaskService taskService = | |||
(TaskService)queryConfiguration.get("taskService"); | |||
return taskService.loadTasks(startIndex, count, sortPropertyIds, sortStates); | |||
} | |||
@Override | |||
protected void saveBeans(List<Task> addedTasks, List<Task> modifiedTasks, | |||
List<Task> removedTasks) { | |||
TaskService taskService = | |||
(TaskService)queryConfiguration.get("taskService"); | |||
taskService.saveTasks(addedTasks, modifiedTasks, removedTasks); | |||
} | |||
} | |||
.... | |||
[[how-to-implement-custom-query-and-queryfactory]] | |||
How to Implement Custom Query and QueryFactory? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
`QueryFactory` instantiates new query whenever sort state changes or | |||
refresh is requested. Query can construct for example named JPA query in | |||
constructor. Data loading starts by invocation of `Query.size()` method | |||
and after this data is loaded in batches by invocations of | |||
`Query.loadItems()`. | |||
Please remember that the idea is to load data in batches. You do not | |||
need to load the entire data set to memory. If you do that you are | |||
better of with some other container implementation like | |||
`BeanItemContainer`. To be able to load database in batches you need your | |||
storage to provide you with the result set size and ability to load rows | |||
in batches as illustrated by the following pseudo code: | |||
[source,java] | |||
.... | |||
int countObjects(SearchCriteria searchCriteria); | |||
List<Object> getObjects(SearchCriteria searchCriteria, int startIndex, int batchSize); | |||
.... | |||
Here is simple read only JPA example to illustrate the idea. You can | |||
find further examples from add-on page. | |||
[source,java] | |||
.... | |||
package com.logica.portlet.example; | |||
import javax.persistence.EntityManager; | |||
import org.vaadin.addons.lazyquerycontainer.Query; | |||
import org.vaadin.addons.lazyquerycontainer.QueryDefinition; | |||
import org.vaadin.addons.lazyquerycontainer.QueryFactory; | |||
public class MovieQueryFactory implements QueryFactory { | |||
private EntityManager entityManager; | |||
private QueryDefinition definition; | |||
public MovieQueryFactory(EntityManager entityManager) { | |||
super(); | |||
this.entityManager = entityManager; | |||
} | |||
@Override | |||
public void setQueryDefinition(QueryDefinition definition) { | |||
this.definition = definition; | |||
} | |||
@Override | |||
public Query constructQuery(Object[] sortPropertyIds, boolean[] sortStates) { | |||
return new MovieQuery(entityManager,definition,sortPropertyIds,sortStates); | |||
} | |||
} | |||
.... | |||
[source,java] | |||
.... | |||
package com.logica.portlet.example; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import javax.persistence.EntityManager; | |||
import org.vaadin.addons.lazyquerycontainer.Query; | |||
import org.vaadin.addons.lazyquerycontainer.QueryDefinition; | |||
import com.logica.example.jpa.Movie; | |||
import com.vaadin.data.Item; | |||
import com.vaadin.data.util.BeanItem; | |||
public class MovieQuery implements Query { | |||
private EntityManager entityManager; | |||
private QueryDefinition definition; | |||
private String criteria = ""; | |||
public MovieQuery(EntityManager entityManager, | |||
QueryDefinition definition, | |||
Object[] sortPropertyIds, | |||
boolean[] sortStates) { | |||
super(); | |||
this.entityManager = entityManager; | |||
this.definition = definition; | |||
for(int i=0;i<sortPropertyIds.length;i++) { | |||
if(i==0) { | |||
criteria = " ORDER BY"; | |||
} else { | |||
criteria+ = ","; | |||
} | |||
criteria += " m." + sortPropertyIds[i]; | |||
if(sortStates[i]) { | |||
criteria += " ASC"; | |||
} | |||
else { | |||
criteria += " DESC"; | |||
} | |||
} | |||
} | |||
@Override | |||
public Item constructItem() { | |||
return new BeanItem<Movie>(new Movie()); | |||
} | |||
@Override | |||
public int size() { | |||
javax.persistence.Query query = entityManager. | |||
createQuery("SELECT count(m) from Movie as m"); | |||
return (int)((Long) query.getSingleResult()).longValue(); | |||
} | |||
@Override | |||
public List<Item> loadItems(int startIndex, int count) { | |||
javax.persistence.Query query = entityManager. | |||
createQuery("SELECT m from Movie as m" + criteria); | |||
query.setFirstResult(startIndex); | |||
query.setMaxResults(count); | |||
List<Movie> movies=query.getResultList(); | |||
List<Item> items=new ArrayList<Item>(); | |||
for(Movie movie : movies) { | |||
items.add(new BeanItem<Movie>(movie)); | |||
} | |||
return items; | |||
} | |||
@Override | |||
public void saveItems(List<Item> addedItems, List<Item> modifiedItems, | |||
List<Item> removedItems) { | |||
throw new UnsupportedOperationException(); | |||
} | |||
@Override | |||
public boolean deleteAllItems() { | |||
throw new UnsupportedOperationException(); | |||
} | |||
} | |||
.... | |||
[[how-to-implement-editable-table]] | |||
How to Implement Editable Table? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
First you need to implement the `Query.saveItems()` method. After this you | |||
need to set some of the properties editable in your items and set table | |||
in editable mode as well. After user has made changes you need to call | |||
`container.commit()` or `container.discard()` to commit or rollback | |||
respectively. Please find complete examples of table handing and | |||
editable JPA query from add-on page. | |||
[[how-to-use-debug-properties]] | |||
How to Use Debug Properties? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
LQC provides set of debug properties which give information about | |||
response times, number of queries constructed and data batches loaded. | |||
To use these properties the items used need to contain these properties | |||
with correct ids and types. If you use dynamic items you can defined | |||
them in the query definition and add them on demand in the query | |||
implementation. | |||
[source,java] | |||
.... | |||
container.addContainerProperty(LazyQueryView.DEBUG_PROPERTY_ID_QUERY_INDEX, Integer.class, 0, true, false); | |||
container.addContainerProperty(LazyQueryView.DEBUG_PROPERTY_ID_BATCH_INDEX, Integer.class, 0, true, false); | |||
container.addContainerProperty(LazyQueryView.DEBUG_PROPERTY_ID_BATCH_QUERY_TIME, Integer.class, 0, true, false); | |||
.... | |||
[[how-to-use-row-status-indicator-column-in-table]] | |||
How to Use Row Status Indicator Column in Table? | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
When creating editable tables LCQ provides | |||
`QueryItemStatusColumnGenerator` which can be used to generate the status | |||
column cells to the table. In addition you need to have the status | |||
property in your items. If your items respect the query definition you | |||
can implement this as follows: | |||
[source,java] | |||
.... | |||
container.addContainerProperty(LazyQueryView.PROPERTY_ID_ITEM_STATUS, | |||
QueryItemStatus.class, QueryItemStatus.None, true, false); | |||
.... | |||
[[how-to-use-status-column-and-debug-columns-with-beans]] | |||
How to Use Status Column and Debug Columns with Beans | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Here is example query implementation which shows how JPA and beans can | |||
be used together with status and debug properties: | |||
[source,java] | |||
.... | |||
package org.vaadin.addons.lazyquerycontainer.example; | |||
import java.beans.BeanInfo; | |||
import java.beans.Introspector; | |||
import java.beans.PropertyDescriptor; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import javax.persistence.EntityManager; | |||
import org.vaadin.addons.lazyquerycontainer.CompositeItem; | |||
import org.vaadin.addons.lazyquerycontainer.Query; | |||
import org.vaadin.addons.lazyquerycontainer.QueryDefinition; | |||
import com.vaadin.data.Item; | |||
import com.vaadin.data.util.BeanItem; | |||
import com.vaadin.data.util.ObjectProperty; | |||
public class TaskQuery implements Query { | |||
private EntityManager entityManager; | |||
private QueryDefinition definition; | |||
private String criteria=" ORDER BY t.name ASC"; | |||
public TaskQuery(EntityManager entityManager, QueryDefinition definition, | |||
Object[] sortPropertyIds, boolean[] sortStates) { | |||
super(); | |||
this.entityManager = entityManager; | |||
this.definition = definition; | |||
for(int i=0; i<sortPropertyIds.length; i++) { | |||
if(i==0) { | |||
criteria = " ORDER BY"; | |||
} else { | |||
criteria+ = ","; | |||
} | |||
criteria += " t." + sortPropertyIds[i]; | |||
if(sortStates[i]) { | |||
criteria += " ASC"; | |||
} | |||
else { | |||
criteria += " DESC"; | |||
} | |||
} | |||
} | |||
@Override | |||
public Item constructItem() { | |||
Task task=new Task(); | |||
try { | |||
BeanInfo info = Introspector.getBeanInfo( Task.class ); | |||
for ( PropertyDescriptor pd : info.getPropertyDescriptors() ) { | |||
for(Object propertyId : definition.getPropertyIds()) { | |||
if(pd.getName().equals(propertyId)) { | |||
pd.getWriteMethod().invoke(task, | |||
definition.getPropertyDefaultValue(propertyId)); | |||
} | |||
} | |||
} | |||
} catch(Exception e) { | |||
throw new RuntimeException("Error in bean property population"); | |||
} | |||
return toItem(task); | |||
} | |||
@Override | |||
public int size() { | |||
javax.persistence.Query query = entityManager.createQuery( | |||
"SELECT count(t) from Task as t"); | |||
return (int)((Long) query.getSingleResult()).longValue(); | |||
} | |||
@Override | |||
public List<Item> loadItems(int startIndex, int count) { | |||
javax.persistence.Query query = entityManager.createQuery( | |||
"SELECT t from Task as t" + criteria); | |||
query.setFirstResult(startIndex); | |||
query.setMaxResults(count); | |||
List<Task> tasks=query.getResultList(); | |||
List<Item> items=new ArrayList<Item>(); | |||
for(Task task : tasks) { | |||
items.add(toItem(task)); | |||
} | |||
return items; | |||
} | |||
@Override | |||
public void saveItems(List<Item> addedItems, List<Item> modifiedItems, | |||
List<Item> removedItems) { | |||
entityManager.getTransaction().begin(); | |||
for(Item item : addedItems) { | |||
entityManager.persist(fromItem(item)); | |||
} | |||
for(Item item : modifiedItems) { | |||
entityManager.persist(fromItem(item)); | |||
} | |||
for(Item item : removedItems) { | |||
entityManager.remove(fromItem(item)); | |||
} | |||
entityManager.getTransaction().commit(); | |||
} | |||
@Override | |||
public boolean deleteAllItems() { | |||
throw new UnsupportedOperationException(); | |||
} | |||
private Item toItem(Task task) { | |||
BeanItem<Task> beanItem= new BeanItem<Task>(task); | |||
CompositeItem compositeItem=new CompositeItem(); | |||
compositeItem.addItem("task", beanItem); | |||
for(Object propertyId : definition.getPropertyIds()) { | |||
if(compositeItem.getItemProperty(propertyId)==null) { | |||
compositeItem.addItemProperty(propertyId, new ObjectProperty( | |||
definition.getPropertyDefaultValue(propertyId), | |||
definition.getPropertyType(propertyId), | |||
definition.isPropertyReadOnly(propertyId))); | |||
} | |||
} | |||
return compositeItem; | |||
} | |||
private Task fromItem(Item item) { | |||
return (Task)((BeanItem)(((CompositeItem)item).getItem("task"))).getBean(); | |||
} | |||
} | |||
.... |
@@ -0,0 +1,648 @@ | |||
[[migrating-from-vaadin-6-to-vaadin-7]] | |||
Migrating from Vaadin 6 to Vaadin 7 | |||
----------------------------------- | |||
For migration to Vaadin 7.1, see | |||
link:MigratingFromVaadin7.0ToVaadin7.1.asciidoc[Migrating | |||
from Vaadin 7.0 to Vaadin 7.1] | |||
[[getting-started]] | |||
Getting Started | |||
~~~~~~~~~~~~~~~ | |||
Most Vaadin 7 APIs are compatible with Vaadin 6, but there are some | |||
changes that affect every application. | |||
Moving to Vaadin 7 brings a number of features designed to make the | |||
lives of developers easier. It is a major version where we could improve | |||
(and break) some parts of the API that have been stagnant and in need of | |||
improvement for years. | |||
Fear not, though, as the vast majority of the API is unchanged or | |||
practically so - many parts even for the last 10 years apart for some | |||
package name changes. While every application requires some migration | |||
steps, the minimal steps needed for many applications are simple enough, | |||
although a few more changes can be useful to benefit from some of the | |||
new features such as improvements to data binding. | |||
The first step is to *update Vaadin libraries*. While Vaadin 6 had a | |||
single JAR and separate GWT JARs, Vaadin 7 is packaged as multiple JARs | |||
that also include GWT. The easiest way to get all you need is to use Ivy | |||
(see below in the section on updating an existing Eclipse project) or | |||
Maven (see below on updating a Maven project). If you are using the latest version of | |||
the Vaadin Eclipse plug-in, upgrading the facet version creates an Ivy | |||
configuration. | |||
The first code change that applies to every Vaadin 6 application | |||
concerns the *com.vaadin.Application* class - it *exists no more*. The | |||
main entry point to your application is now a *com.vaadin.ui.UI*, which | |||
replaces Application and its main window. When switching to UI, you also | |||
get multi-window support out of the box, so bye bye to any old hacks to | |||
make it work. On the flip side, a new UI is created on page reload. If | |||
you prefer to keep the UI state over page reloads in the same way Vaadin | |||
6 does, just add *@PreserveOnRefresh* annotation on your UI class. | |||
For minimal migration, though, it is possible to replace Application | |||
with *LegacyApplication* and its main Window with *LegacyWindow* and | |||
postpone a little dealing with UIs, but when migrating to UIs, you get | |||
more out of the box. The class *Window* is now only used for | |||
"sub-windows" (windows floating inside the page) , not "browser level" | |||
windows or tabs (the whole web page). | |||
An example should clarify things better than lengthy explanations, | |||
so:Vaadin 6: | |||
[source,java] | |||
.... | |||
package com.example.myexampleproject; | |||
import com.vaadin.Application; | |||
import com.vaadin.ui.*; | |||
public class V6tm1Application extends Application { | |||
@Override | |||
public void init() { | |||
Window mainWindow = new Window("V6tm1 Application"); | |||
Label label = new Label("Hello Vaadin!"); | |||
mainWindow.addComponent(label); | |||
setMainWindow(mainWindow); | |||
setTheme(“mytheme”); | |||
} | |||
} | |||
.... | |||
Vaadin 7: | |||
[source,java] | |||
.... | |||
package com.example.myexampleproject; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.*; | |||
@Theme("mytheme") | |||
public class MyApplicationUI extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
VerticalLayout view = new VerticalLayout(); | |||
view.addComponent(new Label("Hello Vaadin!")); | |||
setContent(view); | |||
} | |||
} | |||
.... | |||
In addition, replace `com.vaadin.terminal.gwt.server.ApplicationServlet` | |||
with com.vaadin.server.*VaadinServlet* in web.xml and its parameter | |||
"application" with "*UI*" pointing to your UI class, and the application | |||
is ready to go. Likewise, *ApplicationPortlet* has become *VaadinPortlet*. | |||
Some package names have also been changed, but a simple import | |||
reorganization in your IDE should take care of this. | |||
If you have a custom theme, import e.g. | |||
"../reindeer/*legacy-styles.css*" instead of "../reindeer/styles.css". | |||
The theme is now selected with an *@Theme* annotation on your UI class | |||
rather than a call to *setTheme()*, the usage should be clear from the | |||
example above. | |||
Most remaining issues should show up as compilation errors and in most | |||
cases should be easy to fix in your IDE. | |||
Now you should be ready to compile your widgetset (if any) and take the | |||
application for a first test drive. If you have customized themes, they | |||
will probably also need other updates - see the section on themes below. | |||
Note that support for some older browser versions - including IE6 and | |||
IE7 - has been dropped in Vaadin 7. If you absolutely need them, Vaadin | |||
6 will continue to support them until its planned end of life (June | |||
2014, five years from release of 6.0). | |||
If you have problems with specific topics, see the related sections of | |||
the migration guide. | |||
In case you need more help with the migration, the Vaadin team also | |||
provides https://vaadin.com/services#professionalservices[professional | |||
services]. | |||
[[converting-an-eclipse-project]] | |||
Converting an Eclipse project | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
If you have an existing Vaadin 6 Eclipse project, the easiest way to get | |||
up and running with Vaadin 7 is to switch to *Ivy for dependency | |||
management*. In the project properties, select Project Facets and change | |||
the Vaadin plug-in version to 7.0. If necessary, upgrade also the Java | |||
and Dynamic Web Module facets. _Make sure you use the latest version of | |||
the *Eclipse plug-in* from the update site | |||
https://vaadin.com/framework/get-started#eclipse for this, and note that currently | |||
installing it also requires that the IvyDE update site is configured. We | |||
will attempt to eliminate this additional complication soon._ | |||
Ivy dependency management can also be configured by hand by adding the | |||
files ivy.xml and ivysettings.xml to the root of the project and using | |||
them from Eclipse (with the IvyDE plug-in), Ant or other build system. | |||
For examples of the two files, see e.g. | |||
http://dev.vaadin.com/svn/integration/eclipse/plugins/com.vaadin.integration.eclipse/template/ivy/[here] | |||
and update VAADIN_VERSION in the file ivy.xml. | |||
Note that Vaadin 7 requires *Java version 6* or higher and *servlet | |||
version 2.4* or higher (or portlet 2.0 or higher). If your project is | |||
set up for older versions, update the corresponding facets. | |||
[[converting-a-maven-project]] | |||
Converting a Maven project | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Converting a *Maven* project is usually quite straightforward: replace | |||
the Vaadin dependency with dependencies to the needed Vaadin artifacts, | |||
remove any dependencies on GWT JARs, replace the GWT plug-in with the | |||
Vaadin plug-in and recompile everything. The easiest way to get the | |||
required sections and dependencies is to create a new project from the | |||
vaadin-application-archetype and copy the relevant sections from it to | |||
your project. | |||
Note that Vaadin 7 requires Java version 6 or higher and servlet version | |||
2.4 or higher (or portlet 2.0 or higher). If your project is set up for | |||
older versions, update the corresponding dependencies and compiler | |||
version. | |||
[[content-for-windows-panels-and-more]] | |||
Content for Windows, Panels and More | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
In Vaadin 6, Window, Panel and some other components had a *default | |||
layout* and addComponent() etc. As this often caused confusion and | |||
caused layout problems when unaware of the implicit layout or forgetting | |||
to set its layout parameters, Vaadin 7 now requires *explicitly setting | |||
the content*. See See e.g. | |||
link:CreatingABasicApplication.asciidoc[Creating | |||
a basic application] | |||
If you want to minimize the impact of this on the look and theme of an | |||
old application, you can reproduce the *old structure* simply by setting | |||
a `VerticalLayout` (with margins enabled) as the content and add your | |||
components to it rather than the Panel/UI/Window. | |||
Note that the class *Window* is now only used for sub-windows, not | |||
browser level windows. | |||
Information related to browser windows in now in *Page*, including | |||
browser window size, URI fragment and page title. Setting the browser | |||
location (redirecting to a URL) can also be performed via Page. | |||
The API for *Notifications* has also changed, static methods | |||
`Notification.show()` are now used instead of `Window.showNotification()`. | |||
The current *UI*, *Page*, *VaadinService*, *VaadinRequest* and *VaadinResponse* | |||
instances are easily accessible using *UI.getCurrent()*, | |||
*Page.getCurrent()* etc. The session can be obtained using | |||
*UI.getSession()* and the request and response are available from | |||
*VaadinService.getCurrent()*. Thus, no more need for an explicit | |||
*ThreadLocal* to keep track of them. | |||
VaadinSession also provides the new entry point for *locking* access to | |||
Vaadin components from *background threads*, replacing the old approach | |||
of synchronizing to the Application instance - see the javadoc for | |||
*VaadinSession.lock()* for more details. | |||
To customize the creation of UIs - for instance to create different UIs | |||
for mobile and desktop devices - | |||
*link:CreatingAnApplicationWithDifferentFeaturesForDifferentClients.asciidoc[a | |||
custom UIProvider]* can be used. | |||
[[forms-and-data-binding]] | |||
Forms and Data Binding | |||
~~~~~~~~~~~~~~~~~~~~~~ | |||
What enterprise applications are all about is data, and the data entry | |||
side in Vaadin 6 has been lacking in customizability. While it has been | |||
possible to create arbitrary forms for data input, many situations have | |||
required either bypassing the Form mechanism or using complicated tricks | |||
to customize their layouts etc. | |||
Although *Form* is still there in Vaadin 7 and a lot of old code for | |||
data binding works mostly as is, version 7 brings something better: | |||
* *FieldGroup* supporting *automated data binding*, whether for a hand-designed | |||
form or | |||
link:AutoGeneratingAFormBasedOnABeanVaadin6StyleForm.asciidoc[creating | |||
the fields automatically] | |||
* *link:CreatingATextFieldForIntegerOnlyInputUsingADataSource.asciidoc[typed | |||
fields and properties]* | |||
* *link:CreatingYourOwnConverterForString.asciidoc[converters]*, | |||
both | |||
link:ChangingTheDefaultConvertersForAnApplication.asciidoc[automatic | |||
via ConverterFactory] and | |||
link:CreatingATextFieldForIntegerOnlyInputWhenNotUsingADataSource.asciidoc[explicitly set] | |||
* improved *validation* (performed on data model values after | |||
conversion) - see e.g. | |||
link:UsingBeanValidationToValidateInput.asciidoc[bean validation example] | |||
* and more | |||
If you want to keep using the old mechanisms, just note that e.g. | |||
*TextField* now has the type String, and automatic conversions are applied | |||
as well as *validation* performed on values converted to the *data model | |||
type*. You can migrate data entry views form by form. | |||
The ancient *QueryContainer* has been removed, so it is time to switch | |||
to *SQLContainer* or some other container implementation. | |||
If you are using a custom implementation of *Container.Indexed*, there | |||
is one more method to implement - see the javadoc of *getItemIds(int, | |||
int)* for details and a utility making implementing it easy. | |||
*Property.toString()* should not be used to try to get the value of the | |||
property, use *Property.getValue()* instead. | |||
[[add-ons]] | |||
Add-ons | |||
~~~~~~~ | |||
If your project relies on add-ons from Vaadin Directory, note that not | |||
all of them have been updated for Vaadin 7, and a few might only be | |||
compatible with older Vaadin 7 beta versions. *Check the add-ons* you | |||
use before committing to migration. | |||
You may need to click "*Available for 7*" on the add-on page to get the | |||
correct add-on version. | |||
You can see a list of add-ons with a version available for Vaadin 7 using https://vaadin.com/directory/search[the search], | |||
although some of them might only be compatible with older alpha and beta | |||
versions of Vaadin 7 at the moment. | |||
Note also that a handful of add-ons you might have used are now obsolete | |||
as e.g. *CustomField* is integrated in Vaadin 7. | |||
[[widgetset]] | |||
Widgetset | |||
~~~~~~~~~ | |||
As long as you use the *correct version of* the Eclipse or Maven | |||
*plug-in* to compile your widgetset and remove any old GWT libraries | |||
from your classpath, not much changes for widgetsets. | |||
The current default widgetset is *com.vaadin.DefaultWidgetSet* and | |||
should be inherited by custom widgetsets, although | |||
*com.vaadin.terminal.gwt.DefaultWidgetset* still exists for backwards | |||
compatibility. *DefaultWidgetSet* is also used on portals, replacing | |||
*PortalDefaultWidgetSet*. | |||
If you are compiling your widgetset e.g. with Ant, there are some | |||
changes to the class to execute and its parameters. The class and | |||
parameters to use are now "com.google.gwt.dev.Compiler -workDir (working | |||
directory) -war (output directory) (widgetset module name)" with | |||
optional additional optional parameters before the module name. | |||
If you have optimized your widgetset to limit what components to load | |||
initially, see | |||
link:OptimizingTheWidgetSet.asciidoc[this | |||
tutorial] and the | |||
https://vaadin.com/directory/component/widget-set-optimizer[WidgetSet | |||
Optimizer add-on]. | |||
[[themes]] | |||
Themes | |||
~~~~~~ | |||
The *HTML5 DOCTYPE* is used by Vaadin 7, which can affect the behavior | |||
of some CSS rules.Vaadin 7 brings a new option to create your themes, | |||
with SCSS syntax of *SASS* supporting *variables, nested blocks and | |||
mix-ins* for easier reuse of definitions etc. | |||
To get your old application running without bigger changes, just import | |||
e.g. "../reindeer/*legacy-styles.css*" instead of | |||
"../reindeer/styles.css" and take the application for a spin. There will | |||
most likely be some changes to be done in your theme, but the main parts | |||
should be there. | |||
The themes also support *mixing components from multiple themes* and | |||
using multiple applications with *different themes on the same page*, | |||
which can be especially useful for portlets. However, these depend on | |||
fully migrating your themes to the SCSS format with a theme name | |||
selector. | |||
To take advantage of the new features, see | |||
link:CreatingAThemeUsingSass.asciidoc[Creating | |||
a theme using sass] and | |||
link:CustomizingComponentThemeWithSass.asciidoc[Customizing | |||
component theme with Sass]. | |||
Note that the SCSS theme needs to be *compiled* to CSS before use - in | |||
development mode, this takes place automatically on the fly whenever the | |||
theme is loaded, but when moving to production mode, you need to run the | |||
theme compiler on it to produce a pre-compiled static theme. | |||
link:WidgetStylingUsingOnlyCSS.asciidoc[CSS | |||
can be used to style components] somewhat more freely than in Vaadin 6. | |||
The DOM structure of several layouts has changed, which might require | |||
changes to themes for layouts. See also the section on layouts below. | |||
[[navigation]] | |||
Navigation | |||
~~~~~~~~~~ | |||
In addition to low-level support for handling URI fragments Vaadin 7 | |||
also provides a higher level *navigation* framework, allowing you to | |||
focus on the content of your views rather than the mechanics of how to | |||
navigate to them. | |||
The best way to get acquainted with the new navigation features is to | |||
check the tutorials on | |||
link:CreatingABookmarkableApplicationWithBackButtonSupport.asciidoc[creating | |||
a bookmarkable application], | |||
link:UsingParametersWithViews.asciidoc[using | |||
parameters with views], | |||
link:AccessControlForViews.asciidoc[access | |||
control for views] and | |||
link:ViewChangeConfirmations.asciidoc[view | |||
change confirmations]. | |||
When logging out a user, you can use *Page.setLocation()* to redirect | |||
the user to a suitable page. | |||
[[extending-the-servlet]] | |||
Extending the Servlet | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
As ApplicationServlet moved to history and is replaced by | |||
*VaadinServlet*, many customizations you have made to it need a rewrite. | |||
The most common customizations: | |||
* link:CustomizingTheStartupPageInAnApplication.asciidoc[Customizing | |||
the bootstrap page]: JavaScript, headers, ... | |||
* Add-ons using customized servlets for other purposes (e.g. customizing | |||
communication between client and server) probably need more extensive | |||
rework | |||
Note also that *TransactionListener*, *ServletRequestListener* and | |||
*PortletRequestListener* have been removed. | |||
Many things that used to be taken care of by *ApplicationServlet* are now | |||
distributed among *VaadinServletService*, *VaadinSession*, *VaadinService* | |||
etc. You can get a *VaadinSession* with *Component.getSession()* and | |||
*VaadinService* e.g. with *VaadinSession.getService()*. | |||
System messages that used to be configured by "overriding" a static | |||
method *Application.getSystemMessages()* are now set in *VaadinService* | |||
using a *SystemMessagesProvider*. | |||
[[client-side-widgets]] | |||
Client side widgets | |||
~~~~~~~~~~~~~~~~~~~ | |||
For add-on authors and creators of custom widgets, the biggest changes | |||
in Vaadin 7 have perhaps taken place on the client side and in | |||
client-server communication. | |||
The first big change is a separation of the client side UI *widgets* and | |||
the code handling communication with the server (*Connector*). The | |||
familiar VLabel is still the client side widget corresponding to the | |||
server side component Label, but the communication part has been split | |||
off into LabelConnector. The annotations linking the client side and the | |||
server side have also changed, now the LabelConnector has an *@Connect* | |||
annotation linking it to the server side component Label. | |||
https://vaadin.com/book/vaadin7/-/page/architecture.client-side.html[the | |||
book] provides some background and the tutorial on | |||
link:CreatingASimpleComponent.asciidoc[creating | |||
a simple component] shows an example. | |||
The connector communicates with the server primarily via shared | |||
state from the server to the client and **RPC | |||
calls **link:SendingEventsFromTheClientToTheServerUsingRPC.asciidoc[from | |||
client to server] and | |||
link:UsingRPCToSendEventsToTheClient.asciidoc[from | |||
server to client], with a larger set of supported data types. For | |||
component containers, | |||
link:CreatingASimpleComponentContainer.asciidoc[the | |||
hierarchy of the contained components is sent separately]. | |||
The old mechanism with UIDL, *paintContent()* and *changeVariables()* is | |||
still there for a while to ease migration, but it is recommended to | |||
update your components to the new mechanisms, which also tend to result | |||
in much cleaner code. Using the old mechanisms requires implementing | |||
*LegacyComponent*. | |||
There are also new features such as support for *Extensions* (components | |||
which | |||
link:CreatingAUIExtension.asciidoc[extend | |||
the UI] or | |||
link:CreatingAComponentExtension.asciidoc[other | |||
components] without having a widget in a layout) and | |||
link:UsingAJavaScriptLibraryOrAStyleSheetInAnAddOn.asciidoc[support | |||
for JavaScript], also for | |||
link:IntegratingAJavaScriptComponent.asciidoc[implementing | |||
components] and | |||
link:IntegratingAJavaScriptLibraryAsAnExtension.asciidoc[extensions], | |||
which might simplify the implementation of some components. Shared state | |||
and RPC can also be used from JavaScript, and there are other techniques | |||
for client-server communication. | |||
*Package names* for the client side have changed but a simple import | |||
reorganization by the IDE should be able to take care of that, the new | |||
packages are under *com.vaadin.client.ui*. | |||
If you have implemented a *component that contains other components* | |||
(HasComponents, ComponentContainer) or have client side widgets which do | |||
size calculations etc, see the layouts chapter - these should now be | |||
much simpler to implement than previously, although much of custom | |||
layout widgets will probably need to be rewritten. | |||
A final note about client side development: | |||
*https://vaadin.com/blog/vaadin-and-superdevmode[SuperDevMode]* | |||
has been integrated to Vaadin 7, eliminating the need for browser | |||
plug-ins in many cases when debugging client side code. | |||
[[migration-steps-quick-and-dirty]] | |||
Migration steps (quick and dirty) | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
* Create a connector class for the add-on | |||
* Extend *LegacyConnector*, override the *getWidget()* method, change its | |||
signature to return *VMyWidget* and implement it as return *(VMyWidget) | |||
super.getWidget();* | |||
* Replace the *@ClientWidget(VMyWidget.class)* annotation (on the | |||
server-side component) with *@Connect(MyServerSideComponent.class)* on the | |||
connector class | |||
* Remove the call to *super.updateFromUIDL(...)* in | |||
*VMyWidget.updateFromUIDL(...)* if no such method exists in the | |||
superclass. | |||
* If the widget has implemented *setHeight* and *setWidth*, make the | |||
connector implement *SimpleManagedLayout* and move the layout logic to the | |||
*layout()* method. | |||
* The actual sizes of the widget is available through | |||
*getLayoutManager().getOuterHeight(getWidget().getElement())* and similar | |||
for the width. | |||
* If the widget implements *ContainerResizedListener*, make the connector | |||
implement *SimpleManagedLayout* and call *getWidget().iLayout()* from the | |||
*layout()* method. | |||
* Be prepared for problems if you are doing layouting in *updateFromUIDL* | |||
as the actual size of a relatively sized widget will most likely change | |||
during the layout phase, i.e. after *updateFromUIDL* | |||
The connector class should look like | |||
[source,java] | |||
.... | |||
@Connect(MyComponent.class) | |||
public class MyConnector extends LegacyConnector { | |||
@Override | |||
public VMyWidget getWidget() { | |||
return (VMyWidget) super.getWidget(); | |||
} | |||
} | |||
.... | |||
* Implement the interface *LegacyComponent* in the server side class | |||
* If your widget has not delegated caption handling to the framework | |||
(i.e. used *ApplicationConnection.updateComponent(..., ..., false)* you | |||
should override *delegateCaptionHandling()* in your connector and return | |||
false. Please note, however, that this is not recommended for most | |||
widgets. | |||
[[basic-widget-add-on-using-vaadin-7-apis]] | |||
Basic widget add-on using Vaadin 7 APIs | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Note: migration to new communication mechanisms etc. should be performed | |||
step by step.These instructions continue from where the quick and dirty | |||
migration ended. | |||
* Intermediate step: move *updateFromUIDL(...)* implementation from the | |||
widget to the connector | |||
* Change the visibility of any methods and fields it accesses in the | |||
widget to "package" | |||
* Intermediate step: design an API for the widget that does not access | |||
Vaadin communication mechanisms directly | |||
* Use listeners for events from the widget to the server | |||
* Use setters and action methods for server to client modifications | |||
* Convert state variables and their transmission in | |||
*paintContent()*/*updateFromUIDL()* to use shared state | |||
* Convert one-time actions (events etc.) to use RPC | |||
* Remove "implements LegacyComponent" from the server-side class and the | |||
methods *paintContent()* and *changeVariables()* | |||
* Remove "implements Paintable" or "extends LegacyConnector" and | |||
*updateFromUIDL()* from the client-side connector class (extend | |||
*AbstractComponentConnector* instead of *LegacyConnector*) | |||
[[layouts-and-component-containers]] | |||
Layouts and Component Containers | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
While the server side API of various layouts has not changed much, the | |||
implementations on the client side have. With the currently supported | |||
browsers, much more can now be calculated by the browser, so Vaadin | |||
layouts often do not need to measure and calculate sizes. | |||
Most of the differences are only relevant to those who develop client | |||
side component containers, but a few can also affect other developers. | |||
Among the changes affecting others than layout developers, *CssLayout* | |||
now consists of a single DIV instead of three nested elements, and | |||
link:WidgetStylingUsingOnlyCSS.asciidoc[CSS | |||
can be used to do more customization] than in previous Vaadin versions. | |||
Also other layouts have changed in terms of their *DOM structure* on the | |||
client, which might require changes to themes. The interface | |||
*MarginHandler* is now only implemented by layouts that actually support | |||
it, not in *AbstractLayout*, and *margins* should be set in CSS for | |||
*CssLayout*. | |||
When implementing components that are not full-blown layouts (with | |||
*addComponent()*, *removeComponent()* etc.) but should contain other | |||
components, the simpler interface *HasComponents* should be used instead | |||
of *ComponentContainer*. | |||
For those implementing new component containers or layouts, see the | |||
related tutorials | |||
link:CreatingASimpleComponentContainer.asciidoc[Creating | |||
a simple component container] and | |||
link:WidgetStylingUsingOnlyCSS.asciidoc[Widget | |||
styling using only CSS]. | |||
[[migration-steps-for-componentcontainers]] | |||
Migration steps for ComponentContainers | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
These continue from where the add-on migration steps above left off | |||
* Component containers (e.g. layouts) require more changes as the | |||
underlying layout mechanisms and updates have changed | |||
* Client-side child connectors are now created by the framework | |||
* Hierarchy change events. Guaranteed to run before child calls | |||
*updateCaption*. Create any child slots here and attach the widget. | |||
* Don't paint children | |||
* Don't call *child.updateFromUidl* | |||
* Update caption management (called before *updateFromUidl*, from state | |||
change event listener) | |||
[[miscellaneous-changes]] | |||
Miscellaneous Changes | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
Many overloaded *addListener()* methods have been deprecated. Use | |||
*addClickListener()*, *addValueChangeListener()* etc. instead of them, | |||
reducing ambiguity and the need for explicit casts. | |||
Many *constants* have been replaced with enums, although in most cases | |||
the old names refer to enum values to ease migration. | |||
If using *background threads, locking* has changed: there is no longer | |||
an *Application* class to synchronize to, but *getSession().lock()* etc. | |||
should be used - see the javadoc for details on its correct use, using a | |||
correct try-finally is crucial for building reliable multi-threaded | |||
Vaadin applications. | |||
*ApplicationResource* has been replaced with *ConnectorResource*, taking | |||
different parameters. | |||
*URIHandler* has been replaced with *RequestHandler*. See also the related | |||
class *DownloadStream*. | |||
*JavaScript* can now be executed using *JavaScript.execute()*. | |||
Various methods that were *deprecated* until 6.8 etc. have been removed, | |||
and some classes and methods have been deprecated. In most of those | |||
cases, the deprecation comment or javadoc indicates what to use as a | |||
replacement. | |||
AbstractComponent.*isEnabled()* and *isVisible()* do not take the state | |||
of the parent component into account, but only inquire the state set for | |||
the component itself. A component inside a disabled component still is | |||
disabled, and one inside an invisible component is not rendered on the | |||
browser. | |||
No information is sent to the browser about components marked as | |||
*invisible* - they simply do not exist from the point of view of the | |||
client. | |||
[[components]] | |||
Components | |||
~~~~~~~~~~ | |||
*Button* is no longer a Field and does not have a constructor that takes | |||
a method name to call, use anonymous inner class instead. Because of | |||
this, *CheckBox* is no longer a Button and uses a *ValueChangeListener* | |||
instead of a *ClickListener*. | |||
*DateField* no longer supports milliseconds and its default resolution | |||
is day. | |||
*Label* now supports converters. | |||
*RichTextArea* custom formatting methods removed, use a | |||
*PropertyFormatter* or a *Converter* instead of overriding formatting | |||
methods. | |||
[[need-help]] | |||
Need help? | |||
---------- | |||
If you need any advice, training or hands on help in migrating your app | |||
to Vaadin 7, please be in touch with sales@vaadin.com. Vaadin team would | |||
be happy to be at your service. |
@@ -0,0 +1,169 @@ | |||
[[migrating-from-vaadin-7.0-to-vaadin-7.1]] | |||
Migrating from Vaadin 7.0 to Vaadin 7.1 | |||
--------------------------------------- | |||
This guide describes how to migrate from earlier versions to Vaadin 7.1. | |||
[[migrating-from-vaadin-6]] | |||
Migrating from Vaadin 6 | |||
~~~~~~~~~~~~~~~~~~~~~~~ | |||
When migrating from Vaadin 6, first review | |||
link:MigratingFromVaadin6ToVaadin7.asciidoc[Migrating | |||
from Vaadin 6 to Vaadin 7], then continue with the rest of this guide. | |||
[[migrating-from-vaadin-7.0]] | |||
Migrating from Vaadin 7.0 | |||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
As always with minor releases, we have tried hard to minimize the number | |||
and extent of changes that could affect existing applications you want | |||
to upgrade. However, there are a few points that must be considered, and | |||
some other changes and improvements that might be beneficial to know. | |||
[[property-legacypropertytostring]] | |||
Property legacyPropertyToString | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
The old convention where `Property` `toString()` was used to get the value | |||
of the `Property` continues to cause problems. Changing this behaviour | |||
could potentially cause severe bugs that are hard to find, so instead we | |||
continue our quest to phase out this behaviour. | |||
The behaviour can now be configured via the `legacyPropertyToString` | |||
(either as an init-parameter or using `@VaadinServletConfiguration`). The | |||
settings are: | |||
* “warning” = as 7.0, `toString()` logs warning, default when using | |||
web.xml | |||
* “disabled” = `toString()` is just `toString()`, does not log, default when | |||
using `@VaadinServletConfiguration` | |||
* “enabled” = legacy `toString()` behaviour, does not log, compatible with | |||
Vaadin 6 | |||
By default, if you are not using `@VaadinServletConfiguration` to | |||
configure your servlet, the functionality is the same as in 7.0, and | |||
compatible with 6; a warning is logged. | |||
If you are using the new `@VaadinServletConfiguration` to configure your | |||
servlet, it is assumed that you’re creating a new project, and using | |||
`getValue()` instead of `toString()`, and no warning of `toString()` usage is | |||
logged. | |||
This change will not break your application, but you should consider the | |||
options. | |||
1. Consider switching `legacyPropertyToString` mode to | |||
1. “enabled” if you are using `toString()` improperly, and do not want | |||
warnings | |||
2. “disabled” if you are absolutely sure you are not using `toString()` | |||
improperly, and do not want warnings | |||
[[converter-targettype]] | |||
Converter targetType | |||
^^^^^^^^^^^^^^^^^^^^ | |||
The conversion methods in `Converter` now have an additional `targetType` | |||
parameter, used by the caller to indicate what return type is expected. | |||
This enables `Converter`{empty}s to support multiple types, which can be handy in | |||
some cases. | |||
This change will cause compile errors if you implement or call | |||
`Converter.convertToModel()` and/or `Converter.convertToPresentation()`. | |||
1. Add the `targetType` parameter if needed | |||
[[ui-access-outside-its-requestresponse]] | |||
UI access() outside it’s request/response | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
If you have background threads/processes that update the ui (e.g long | |||
running process updating a `ProgressBar`), or if you otherwise update a ui | |||
from outside its request/response (e.g updating one UI from another), | |||
you should use the new `UI.access()` method. This ensures proper locking | |||
is done, and failing to do so might result in hard to debug concurrency | |||
problems. | |||
To debug possible concurrency problems, it is recommended to enable | |||
assertions with the "-ea" parameter to the JVM. | |||
This change will not break your application, but your application might | |||
already be broken; you should ensure that all ui access dome outside the | |||
request handling thread uses this new API. | |||
[[calendar-included]] | |||
Calendar included | |||
^^^^^^^^^^^^^^^^^ | |||
The `Calendar` component, which was previously an add-on, is now included | |||
in the core framework. However, the package is new, and there are minor | |||
API changes. | |||
This change will not break your application, but you might want to | |||
switch to the core framework version of the component. | |||
1. Remove the Calendar add-on | |||
2. Update imports to the new package | |||
3. Adjust for API changes | |||
[[progressbar-is-the-new-progressindicator]] | |||
ProgressBar is the new ProgressIndicator | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
The `ProgressIndicator` component had integrated support for polling - a | |||
feature that was a bit strange, especially now with built-in polling and | |||
push support. `ProgressBar` is a pure visual component that is intended to | |||
replace `ProgressIndicator`. If you have been relying on the polling | |||
capability of `ProgressIndicator`, you should look at `UI.setPollInterval()` | |||
or enable server push. | |||
This change does not break your application, but is deprecated, and | |||
should particularly not be used if push or `UI.setPollInterval()` is used. | |||
1. Replace `ProgressIndicator` with `ProgressBar` | |||
2. If you are using the polling feature use `UI.setPollInterval()` or enable push | |||
[[isattached-replaces-sessionnull]] | |||
isAttached() replaces session!=null | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Previously you had to do an awkward `getSession() != null` to figure out | |||
whether or not the component (or `ClientConnector` to be precise) actually | |||
was attached to the UI hierarchy (attached to a session, to be precise). | |||
There is now a `isAttached()` method that does that. Note that the old way | |||
still works, the new way is just more explicit, clean and findable. | |||
This change will not break your application, but if you want to clean up | |||
your code, you can look for `getSession()` null-checking and replace as | |||
appropriate with `isAttached()`. | |||
[[vconsole-is-now-java.util.logging]] | |||
VConsole is now java.util.logging | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
For client-side logging and debug messages, the proprietary `VConsole` has | |||
been deprecated and replaced with the standard `java.util.logging` | |||
framework, and the messages are (by default) displayed in the completely | |||
renewed debug window. | |||
This change will not break your application, but the old API is | |||
deprecated, and the new one has additional features (e.g log levels). To | |||
update, look for references to `VConsole` and replace with standard | |||
`java.util.logging` calls, e.g | |||
`Logger.getLogger(getClass().getName()).log(“A message”)`. | |||
[[call-init-for-custom-vaadinservice-instances]] | |||
Call init() for custom VaadinService instances | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
If overriding `VaadinServlet.createServletService()` or | |||
`VaadinPortlet.createPortletService()`, the new `init` method must be | |||
invoked for the newly-created `VaadinService` instance. | |||
[[new-features]] | |||
New features | |||
~~~~~~~~~~~~ | |||
In addition to the changes, there are a number of new features that you | |||
probably want to familiarize yourself with, such as `Push` and the | |||
redesigned `DebugWindow`. |
@@ -0,0 +1,568 @@ | |||
[[offline-mode-for-touchkit-4-mobile-apps]] | |||
Offline mode for TouchKit 4 mobile apps | |||
--------------------------------------- | |||
[.underline]#*_Note:_* _Vaadin Touchkit has been discontinued. A community-supported version is | |||
available https://github.com/parttio/touchkit[on GitHub]._# | |||
[[background]] | |||
Background | |||
~~~~~~~~~~ | |||
Vaadin is primarily a server-side framework. What happens with the | |||
application when the server is not available? Although this is possible | |||
on desktop computers, more often it happens when using a mobile device. | |||
This is why Vaadin TouchKit allows | |||
you to define offline behavior. In this article I will tell you all the | |||
details you need to know about offline mode and how to use it. It is | |||
written based on Vaadin 7.3 and TouchKit 4.0.0. | |||
Touchkit is a Vaadin | |||
addon that helps in developing mobile applications. I assume that you | |||
have some knowledge in Vaadin and how to develop client-side Vaadin | |||
(GWT) code. I will mention the http://demo.vaadin.com/parking/[Parking | |||
demo] here a few times and you can find its sources | |||
https://github.com/vaadin/parking-demo[here]. I suggest that you read | |||
this article before you try to understand the Parking demo source code, | |||
it will help you grasp the concepts demonstrated in the demo. | |||
[[demystifying-offline-mode]] | |||
Demystifying offline mode | |||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
As said before, Vaadin is a server-side framework and that implies that | |||
when an application is running, there is a lot of communication going on | |||
between the server and the client. Thus server-side views are not | |||
accessible when there is no connection. On the other hand, offline | |||
enabled applications run pure client-side Vaadin (GWT) code without | |||
connecting the server. | |||
There are a couple of approaches you might take to specify offline | |||
behavior on the client-side. | |||
1. Write a fully client-side application for the user to interact with | |||
when the server is offline. | |||
2. Write some views as client-side widgets and, in case the connection | |||
is lost, disable all the components that might need a server connection. | |||
Let’s take a look at the technical details you need to know. | |||
[[client-side-offline-mode-handling---method-1-checking-the-status]] | |||
Client-side offline mode handling - method 1: checking the status | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
The simplest way to know if the application is online or offline is to | |||
use this code: | |||
[source,java] | |||
.... | |||
OfflineModeEntrypoint.get().getNetworkStatus().isAppOnline() | |||
.... | |||
You might use it before sending something to the server or calling an | |||
RPC, for example. However, the network status might change at any time. | |||
Method 2 helps you react to those changes. | |||
[[client-side-offline-mode-handling---method-2-handling-events]] | |||
Client-side offline mode handling - method 2: handling events | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
In order to use this method you need an `ApplicationConnection` instance. | |||
We are going to use its event bus to handle online/offline events. | |||
Usually you get an `ApplicationConnection` instance from a component | |||
connector. Here is an example: | |||
[source,java] | |||
.... | |||
@Connect(MyComponent.class) | |||
public class MyConnector extends AbstractComponentConnector { | |||
@Override | |||
protected void init() { | |||
super.init(); | |||
getConnection().addHandler(OnlineEvent.TYPE, new OnlineEvent.OnlineHandler() { | |||
@Override | |||
public void onOnline(final OnlineEvent event) { | |||
// do some stuff | |||
} | |||
}); | |||
getConnection().addHandler(OfflineEvent.TYPE, new OfflineEvent.OfflineHandler() { | |||
@Override | |||
public void onOffline(final OfflineEvent event) { | |||
// do some stuff | |||
} | |||
}); | |||
} | |||
} | |||
.... | |||
Note that this connector will only be created if an instance of | |||
`MyComponent` is created on the server side and attached to the UI. As an | |||
option, it might be a `UI` or `Component` extension connector. Otherwise | |||
your connector will never be instantiated and you will never receive | |||
these events, so you can rely on them only if you want to show some | |||
changes in the view or disable some functionality of a view when | |||
offline. In order to get true offline capabilities, use method 3. | |||
[[client-side-offline-mode-handling---method-3-implementing-offlinemode-interface]] | |||
Client-side offline mode handling - method 3: implementing OfflineMode interface | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Implementing client-side OfflineMode interface allows you to specify | |||
true offline-mode behavior: you will receive events also in case the | |||
page is loaded from cache without network connection at all. | |||
Fortunately, there is a default implementation and you don’t need to | |||
worry about the implementation details. `DefaultOfflineMode` provides an | |||
OfflineMode implementation for any TouchKit application. It shows a | |||
loading indicator and a sad face when the network is down. In most cases | |||
all you want to do is replace this sad face with something more useful | |||
(for example Minesweeper or Sudoku), here’s a sample: | |||
[source,java] | |||
.... | |||
public class MyOfflineMode extends DefaultOfflineMode { | |||
@Override | |||
protected void buildDefaultContent() { | |||
getPanel().clear(); | |||
getPanel().add(createOfflineApplication()); // might be a full blown GWT UI | |||
} | |||
} | |||
.... | |||
Then you need to specify the implementation in your widgetset definition | |||
file (*.gwt.xml): | |||
[source,xml] | |||
.... | |||
<replace-with class="com.mybestapp.widgetset.client.MyOfflineMode"> | |||
<when-type-is class="com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineMode" /> | |||
</replace-with> | |||
.... | |||
This is enough for showing an offline UI, it will be shown and hidden | |||
automatically, `DefaultOfflineMode` will take care of this. If you need a | |||
more complex functionality, like doing something when going | |||
offline/online, you might want to override additional methods from | |||
`DefaultOfflineMode` or implement OfflineMode from scratch. I briefly | |||
sketch what you need to know about it. | |||
The OfflineMode interface has three methods: | |||
[source,java] | |||
.... | |||
void activate(ActivationReason); | |||
boolean deactivate(); | |||
boolean isActive(); | |||
.... | |||
Pretty clear, but there are some pitfalls. | |||
Counterintuitively, not all `ActivationReason`{empty}(s) actually require | |||
activating the offline application view. On | |||
`ActivationReason.APP_STARTING` you can just show a loading indicator and | |||
on `ActivationReason.ONLINE_APP_NOT_STARTED` you might want to display a | |||
reload button or actually hide the offline view. Take a look at the | |||
`DefaultOfflineMode` implementation and the `TicketViewWidget` in the | |||
Parking demo. | |||
Second thing to note: `deactivate()` will never be called if i`sActive()` | |||
returns `false`. So you must track whether the offline mode is active or | |||
just take a shortcut like this: | |||
[source,java] | |||
.... | |||
boolean isActive() { | |||
return true; | |||
} | |||
.... | |||
And the last one: regardless of what JavaDoc says, the return value of | |||
the `deactivate()` method is ignored. You might want to check if this | |||
changes in future versions. | |||
Note that this client-side | |||
http://demo.vaadin.com/javadoc/com.vaadin.addon/vaadin-touchkit-agpl/4.0.0/com/vaadin/addon/touchkit/gwt/client/offlinemode/OfflineMode.html[com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineMode] | |||
interface has nothing to do with server-side extension | |||
http://demo.vaadin.com/javadoc/com.vaadin.addon/vaadin-touchkit-agpl/4.0.0/com/vaadin/addon/touchkit/extensions/OfflineMode.html[com.vaadin.addon.touchkit.extensions.OfflineMode] | |||
class (unfortunate naming). | |||
[[setting-up-the-offline-mode]] | |||
Setting up the offline mode | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
You can turn a Vaadin application into an offline-enabled TouchKit | |||
application by using an extension of `TouchKitServlet` as your servlet | |||
class. For example, the following might be your servlet declaration in | |||
your UI class: | |||
[source,java] | |||
.... | |||
@WebServlet(value = "/*") | |||
public static class Servlet extends TouchKitServlet /* instead of VaadinServlet */ {} | |||
.... | |||
Below are some details that you might need at some point (or have read | |||
about in other places and are wondering what they are). You may skip to | |||
the “Synchronizing data between server and client” section if you just | |||
want a quick start. | |||
You can check network status (method 1) in any TouchKit application | |||
(i.e. any application using `TouchKitServlet`), nothing special is | |||
required. | |||
In order to use the application connection event bus (method 2), offline | |||
mode must be enabled or no events will be sent. As of TouchKit 4, it is | |||
enabled by default whenever you use TouchKit. If for some reason you | |||
want offline mode disabled, annotate your UI class with | |||
`@OfflineModeEnabled(false)`. Although this is not recommended in TouchKit | |||
applications, because no message will be shown if the app goes offline, | |||
not even the standard Vaadin message. | |||
For method 3 (implementing the OfflineMode interface), besides enabling | |||
offline mode, the | |||
http://en.wikipedia.org/wiki/Cache_manifest_in_HTML5[HTML5 cache | |||
manifest] should be enabled. The cache manifest tells the browser to | |||
cache some files, so that they can be used without a network connection. | |||
As with the offline mode, it is enabled by default. If you want it | |||
disabled, annotate your UI class with `@CacheManifestEnabled(false)`. | |||
That way your application might be fully functional once starting online | |||
and then going offline (if it does not need any additional files when | |||
offline), but will not be able to start when there is no connection. | |||
[[caching-additional-files-for-example-a-custom-theme]] | |||
Caching additional files, for example a custom theme | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
If you need some additional files to be cached for offline loading (most | |||
likely your custom theme), you can add this property to your *.gwt.xml | |||
file: | |||
[source,xml] | |||
.... | |||
<set-configuration-property | |||
name='touchkit.manifestlinker.additionalCacheRoot' | |||
value='path/relative/to/project/root:path/on/the/server' /> | |||
.... | |||
Only files having these extensions will be added to the cache manifest: | |||
.html, .js, .css, .png, .jpg, .gif, .ico, .woff); | |||
If this is a directory, it will be scanned recursively and all the files | |||
with these extensions will be added to the manifest. | |||
[[offlinemode-extension]] | |||
OfflineMode extension | |||
^^^^^^^^^^^^^^^^^^^^^ | |||
In addition, you can slightly tweak the offline mode through the | |||
OfflineMode UI extension. | |||
You can set offline mode timeout (if there’s no response from the server | |||
during this time, offline mode will be activated), or manually set | |||
application mode to offline/online (useful for development). There’s | |||
also a less useful parameter: enable/disable persistent session cookie | |||
(enabled by default if you use `@PreserveOnRefresh`, which you should do | |||
for offline mode anyways). That’s all there is in this extension. Usage: | |||
[source,java] | |||
.... | |||
// somewhere among UI initializaion | |||
OfflineMode offline = new OfflineMode(); | |||
offline.extend(this); | |||
offlineModeSettings.setOfflineModeTimeout(5); | |||
.... | |||
Note: it is not compulsory to use this extension, but it helps the | |||
client side of the Touchkit add-on to find the application connection. | |||
Without it, it tries to get an application connection for 5 seconds. If | |||
you suspect that your connection is too slow or the server is very slow | |||
to respond, you might add a new `OfflineMode().extend(this);` to your UI | |||
just in case. That should be very rarely needed. | |||
This extension is usually used for synchronizing data between the server | |||
and the client (covered in the next section), but it can be done through | |||
any other extension/component -- there is no special support for it in | |||
OfflineMode extension. | |||
[[synchronizing-data-between-server-and-client]] | |||
Synchronizing data between server and client | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
In a sense, the client is always in “offline mode” between requests from | |||
the server point of view. Therefore the regular Vaadin way of | |||
synchronizing data between the client-side widget and the server-side | |||
(https://vaadin.com/book/-/page/gwt.rpc.html[Vaadin RPC mechanism] and | |||
https://vaadin.com/book/-/page/gwt.shared-state.html[shared state]) is | |||
still valid, the difference being that the offline widget is probably | |||
more complex and the amount of data is greater than that of an average | |||
component. | |||
As mentioned, the server is not necessarily aware that the client went | |||
offline for some time, therefore the synchronization should be initiated | |||
from the client side. So using method 2 or 3, the client side gets an | |||
event that the connection is online and it sends an RPC call to the | |||
server. New data might be sent with the notification or asked | |||
separately, e.g. using | |||
http://demo.vaadin.com/javadoc/com.vaadin.addon/vaadin-touchkit-agpl/4.0.0/index.html?com/vaadin/addon/touchkit/extensions/LocalStorage.html[LocalStorage] | |||
(TouchKit provides easy access to | |||
http://www.w3schools.com/html/html5_webstorage.asp[HTML5 LocalStorage] | |||
from the server side). The server might send new data through shared | |||
state. | |||
If we reuse OfflineMode (mentioned in the end of the last section), the | |||
code might look like this: | |||
[source,java] | |||
.... | |||
public class MyOfflineModeExtension extends OfflineMode { | |||
public MyOfflineModeExtension() { | |||
registerRpc(serverRpc); | |||
} | |||
private final SyncDataServerRpc serverRpc = new SyncDataServerRpc() { | |||
@Override | |||
public void syncData(final Object newData) { | |||
doSmth(newData); // update data | |||
getState().someProperty = newServerData; // new data from the server to the client | |||
} | |||
}; | |||
} | |||
@Connect(MyOfflineModeExtension.class) | |||
public class MyOfflineConnector extends OfflineModeConnector { | |||
private final SyncDataServerRpc rpc = RpcProxy.create(SyncDataServerRpc.class, this); | |||
@Override | |||
protected void init() { | |||
super.init(); | |||
getConnection().addHandler(OnlineEvent.TYPE, new OnlineEvent.OnlineHandler() { | |||
@Override | |||
public void onOnline(final OnlineEvent event) { | |||
Object new Data = … // get updated data | |||
rpc.syncData(newData); | |||
} | |||
}); | |||
} | |||
} | |||
.... | |||
As already said, this does not necessarily have to be done through the | |||
OfflineMode extension, it can be done using any component connector, | |||
there is nothing special about OfflineMode. | |||
Another option, a less wordy and more decoupled one, could be done by | |||
using JavaScript function call. | |||
On the server side: | |||
[source,java] | |||
.... | |||
JavaScript.getCurrent().addFunction("myapp.syncData", | |||
(args) -> { /*sync data, e.g. get it from LocalStorage */}); | |||
.... | |||
On the client side: | |||
[source,java] | |||
.... | |||
// in any connector | |||
getConnection().addHandler(OnlineEvent.TYPE, new OnlineEvent.OnlineHandler() { | |||
@Override | |||
public native void onOnline(final OnlineEvent event) /*-{ | |||
myapp.syncData(); | |||
}-*/; | |||
}); | |||
.... | |||
Or similar code in client-side OfflineMode implementation: | |||
[source,java] | |||
.... | |||
MyOfflineMode extends DefaultOfflineMode { | |||
@Override | |||
public native boolean deactivate() /*-{ | |||
myapp.syncData(); | |||
}-*/; | |||
} | |||
.... | |||
This option is less “the Vaadin way”, but in some cases might be useful. | |||
[[creating-efficient-offline-views]] | |||
Creating efficient offline views | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
There are two main concerns with offline-enabled applications: | |||
1. Maximizing code sharing between online and offline mode. | |||
2. Seamlessly switching between offline and online mode. | |||
To share the code for a view that is used both in online and offline, | |||
you will probably need to create the view as a custom widget, including | |||
connector and a server-side component class. If you know how to do this | |||
and understand why it is needed, you can skip to the “Switching between | |||
online and offline” subsection . | |||
As Vaadin is a server-side framework, the views and the logic are | |||
usually implemented using server-side Java code. During application | |||
lifetime, a lot of traffic is sent between the server and the client | |||
even in a single view. Thus server-side implemented views are not usable | |||
when there is no connection between server and client. | |||
For very simple views (e.g. providing a list, no data input) it might be | |||
appropriate to have two separate implementations, one client-side and | |||
one server-side, as it is quick and easy to build these and you avoid | |||
the development and code overhead of using client-side views online, | |||
keeping the server-side advantages for the online version. | |||
For more complex functionality you will need to implement a fully | |||
client-side view for both online and offline operation and then | |||
synchronize the data as described in the previous section. Using it | |||
during a completely offline operation is straightforward: just show the | |||
view on the screen by an OfflineMode interface implementation in an | |||
overlay. For server-side usage you will probably need to create a | |||
https://vaadin.com/book/-/page/gwt.html[server-side component and a | |||
connector]. | |||
[[switching-between-online-and-offline]] | |||
Switching between online and offline | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
What we want to achieve is that the user doesn’t feel that the | |||
application went offline or online if he doesn’t need to know that. We | |||
might show an indicator so that the user is aware, but he should be able | |||
to do what he did before the switch happened, if this is possible. Also, | |||
no data should be lost during switching. | |||
[[a-navigatormanager-issue-and-workaround]] | |||
A NavigatorManager issue and workaround | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Before we go to some deeper details, note that there is an annoying | |||
`NavigatorManager` behavior related to offline mode: when you click a | |||
`NagivationButton` while the connection is down (but before offline mode | |||
was activated) and the target view is not in the DOM yet, the server | |||
does not respond the system switches to offline mode and then when | |||
coming back from offline mode, we’re stuck in an empty view. | |||
A workaround for this is to call `NavigatorManagerConnector` to redraw on | |||
an online event, so this might be put in some connector (you might use | |||
deferred binding to put this in `NavigatorManagerConnector` itself): | |||
[source,java] | |||
.... | |||
getConnection().addHandler(OnlineEvent.TYPE, new OnlineEvent.OnlineHandler() { | |||
@Override | |||
public void onOnline(final OnlineEvent event) { | |||
final JsArrayObject<ComponentConnector> jsArray = | |||
ConnectorMap.get(getConnection()).getComponentConnectorsAsJsArray(); | |||
for (int i = 0; jsArray.size() > i; i++) { | |||
if (jsArray.get(i) instanceof NavigationManagerConnector) { | |||
final NavigationManagerConnector connector = | |||
(NavigationManagerConnector) jsArray.get(i); | |||
connector.forceStateChange(); | |||
} | |||
} | |||
} | |||
}); | |||
.... | |||
[[user-experience-considerations-related-to-switching]] | |||
User experience considerations related to switching | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Here’s an example of what we want to achieve: if the user is filling a | |||
form, which by design can be filled offline or online, and the network | |||
suddenly goes down, he should be able to continue filling the form | |||
without much interference. That means, if we’re using method 3 by | |||
implementing OfflineMode and showing an overlay on the screen (which is | |||
done in the Parking demo), the offline overlay will be hiding the real | |||
online form. At that point the data from the online form is copied to | |||
the offline form and the user barely notices that something happened. | |||
That means there are two instances of the form, online one and offline | |||
one. Another option would be that you have only one instance of the form | |||
and instead of copying the data, you attach the whole form to a | |||
different view (thanks to Tomi Virkku for the tip). | |||
In the Parking demo, the ticket view jumps, because the scroll position | |||
changes and an indicator is added. If the user was in the middle of | |||
something, he is suddenly interrupted, although no data is lost. | |||
If we want to improve user experience, we could implement it in a better | |||
way. In case the network goes offline when the user is filling a form, | |||
we disable all the elements that might fire a request to the server and | |||
let the user continue filling the form. Of course, the form should be | |||
implemented completely client-side, and all the suspicious elements | |||
would be around it, probably navigation/toolbar buttons. Another option | |||
would be to have all the elements client-side and on click they would be | |||
checking if there is a connection, before sending anything to the | |||
server. After the user submits or cancels the form, we can show the | |||
“true” offline view. Alternatively, it will be the only offline view in | |||
the application, depending on the specific case. | |||
For example, if you are using a navigator manager, the trick would be to | |||
keep or find the `VNavigatorManager` and disable its widgets (left and | |||
right widgets, the ones that are used to navigate): | |||
[source,java] | |||
.... | |||
getConnection().addHandler(OfflineEvent.TYPE, new OfflineEvent.OfflineHandler() { | |||
@Override | |||
public void onOffline(final OfflineEvent event) { | |||
setWidgetEnabled(getWidget().getNavigationBar().getWidget(0), false); | |||
} | |||
}); | |||
void setWidgetEnabled(final Widget widget, final boolean enabled) { | |||
widget.setStyleName(ApplicationConnection.DISABLED_CLASSNAME, !enabled); | |||
if (widget instanceof HasEnabled) | |||
((HasEnabled) widget).setEnabled(enabled); | |||
// this is just because for some reason VNavigatorButton does not implement HasEnabled, although it has such methods... | |||
if (widget instanceof VNavigationButton) | |||
((VNavigationButton) widget).setEnabled(enabled); | |||
} | |||
.... | |||
Known issues: `HasEnabled` declaration should be fixed soon, but I should | |||
warn you that for some reason a disabled `NavigationButton` still responds | |||
to mouse click events, although correctly ignoring touch events. | |||
Same works in the other direction as well, so when an offline form is | |||
shown and the connection goes up, you just keep the offline form until | |||
the user submits/cancels, then show the online view again. | |||
This is how you can give the user experience the best experience. | |||
[[phonegap-integration]] | |||
PhoneGap integration | |||
~~~~~~~~~~~~~~~~~~~~ | |||
As this is not directly related to the topic I will not explain the | |||
basics here, just a couple of pitfalls that someone familiar with | |||
PhoneGap might encounter. | |||
http://dev.vaadin.com/ticket/13250[An issue with offline mode on | |||
PhoneGap] was reported recently and because of that, a new solution was | |||
found that puts the Vaadin application into an iframe. You can get the | |||
files for PhoneGap from TouchKit maven archetype (_link no longer available_). However, this solution has its | |||
drawbacks and you might want | |||
to disable the iframe. If you do that, you need to copy some files (like | |||
widgetset) to your PhoneGap project. There is still ongoing discussion | |||
of how to improve this. No more details here, this was just to warn you. | |||
Another pitfall is that when you specify the URL in archetype’s | |||
index.html do put the final slash: | |||
[source,java] | |||
.... | |||
window.vaadinAppUrl = 'http://youraddress.com/path/'; // <--- slash is compulsory! | |||
.... | |||
Without it the application will not load from cache when there’s no | |||
connection. |
@@ -0,0 +1,187 @@ | |||
[[scala-and-vaadin-how-to]] | |||
Scala and Vaadin how-to | |||
----------------------- | |||
[[introduction]] | |||
Introduction | |||
~~~~~~~~~~~~ | |||
Since Vaadin is a server-side library it works very well with all JVM | |||
languages, including Scala. This article provides instructions on how to | |||
get started with Vaadin using Scala. First, we'll go through setting up | |||
a new project. After that we'll introduce the Scaladin add-on and see | |||
how it enhances Vaadin components by adding features that leverage the | |||
power of Scala. | |||
[[creating-a-new-eclipse-vaadin-project-with-scala]] | |||
Creating a new Eclipse Vaadin project with Scala | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
[[installing-the-required-software-components]] | |||
Installing the required software components | |||
+++++++++++++++++++++++++++++++++++++++++++ | |||
* Download and install http://eclipse.org/[Eclipse] Helios or Indigo by | |||
unpacking it to a location of your choice. Please note that only Eclipse | |||
Helios is officially supported by Scala IDE but also Indigo can be used. | |||
* Start Eclipse, and install the http://vaadin.com/eclipse[Vaadin | |||
Eclipse Plug-in] and http://www.scala-ide.org[Scala IDE Eclipse Plug-in] | |||
using the plug-in installation feature of Eclipse (available under | |||
`Help -> Install New Software...`). | |||
You also need a servlet container to run your application. In this | |||
example we use Tomcat, but any standard container (Jetty, JBoss, | |||
Glassfish, Oracle WebLogic, IBM WebSphere etc.) should be fine. | |||
* Download and install http://tomcat.apache.org/[Tomcat] by unpacking it | |||
to a location of your choice. | |||
* Add the server to Eclipse | |||
* Open the Servers view | |||
* Right click in the Servers view and choose `New -> Server` | |||
* Choose the type of your server, in this case `Apache -> Tomcat` | |||
* Choose the server runtime environment in the dialog by selecting the | |||
folder you unpacked Tomcat to. | |||
[[creating-a-new-project]] | |||
Creating a new project | |||
++++++++++++++++++++++ | |||
* Create a new Vaadin project in Eclipse: | |||
* Choose `File -> New...` | |||
* Choose `Other...` | |||
* Choose `Vaadin -> Vaadin Project` from the list. You can use the | |||
filter to narrow down the list. | |||
* Choose a name for your project, eg. "ScalaTest" | |||
The New Vaadin Project Wizard allows you to configure different aspects | |||
your project, but the defaults are fine. | |||
At this point you have a ready-to-go Vaadin Java project. To start doing | |||
Scala we need to do a few more things: | |||
* Add the Scala nature to your project: right click your project root, | |||
and choose `Configure -> Add Scala Nature` from the menu. | |||
* Navigate to the `src` folder, and delete the generated Java file under | |||
the default package (eg. `com.example.scalatest`) | |||
Next up, some Scala! | |||
* Add a new Scala class in your project: right click the default | |||
package, and choose `New -> Scala Class` | |||
* Choose a name for the class, eg. "ScalaApp" | |||
* Our new class should extend the `com.vaadin.Application`, so in the | |||
wizard, click the `Browse...` button next to the "Superclass" field, and | |||
choose that from the list. | |||
* Click "Finish" to let Eclipse generate the class. | |||
Now we need to write some code in the method of our new Vaadin | |||
application. | |||
* Open the `ScalaApp.scala` | |||
* Add the following lines in the `init()` | |||
method: `setMainWindow(new Window("Scala Rocks!"))` `getMainWindow.addComponent(new Label("Hello World!"))` | |||
You can let Eclipse add the imports as you go, or just import the Vaadin | |||
components `(import com.vaadin.ui._)` yourself. The resulting file | |||
should look like this: | |||
[source,javascript] | |||
.... | |||
import com.vaadin.Application | |||
import com.vaadin.ui._ | |||
class ScalaApp extends Application { | |||
def init(): Unit = { | |||
setMainWindow(new Window("Scala Rocks!")) | |||
getMainWindow.addComponent(new Label("Hello World!")) | |||
} | |||
} | |||
.... | |||
Next we make sure the servlet container knows which class it should | |||
load. | |||
* Open `WebContent/WEB-INF/web.xml` | |||
* Under the `<web-app><servlet>` branch change the `param-value` of the | |||
`application` init-param to contain to your application class, including | |||
the package name. Eg. "com.example.scalatest.ScalaApp" | |||
[[additional-configuration]] | |||
Additional configuration | |||
++++++++++++++++++++++++ | |||
We're almost done. The last thing we need to do is make sure that the | |||
`scala-library.jar` is available at runtime. We do this by adding the | |||
JAR into the classpath of our servlet container. | |||
First, we need the JAR file itself. You already have this in the Scala | |||
IDE installation folder under Eclipse, or you can download the Scala | |||
distribution from http://www.scala-lang.org/downloads. | |||
We have a few options how to make sure the JAR is available at runtime. | |||
* Put the file in the `WEB-INF/lib` folder under your project. | |||
* Put the file directly in the lib folder of your servlet container. | |||
* Add the Scala library to the deployment assembly: | |||
`project properties -> Deployment assembly -> Add... -> Java build path entries` | |||
After you have done this we can fire up our application! | |||
[[running-the-application]] | |||
Running the application | |||
+++++++++++++++++++++++ | |||
Running the application is simple | |||
* Right click your project, and choose `Run As -> Run On Server` | |||
* Choose the previously created Tomcat instance as the target. You might | |||
also want to check the "Always use this server when running this | |||
project" checkbox. | |||
Eclipse should then start the server and open the UI in a internal | |||
browser window. | |||
[[creating-a-new-project-using-a-giter8-template]] | |||
Creating a new project using a Giter8 template | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
https://github.com/n8han/giter8[Giter8] is a command-line tool that | |||
generates project skeletons from templates that are published on GitHub. | |||
The Vaadin-Scala template creates the basic structure for a | |||
http://www.scala-sbt.org/[SBT]-project that has Vaadin, Scala | |||
and Scaladin included. | |||
First, install Giter8 following the instructions | |||
https://github.com/n8han/giter8#readme[on their readme]. Then just | |||
.... | |||
g8 ripla/vaadin-scala | |||
.... | |||
And answer the questions, or press enter for defaults. After that launch | |||
the server (jetty): | |||
.... | |||
cd <project dir> | |||
sbt | |||
container:start | |||
.... | |||
You can then browse to | |||
__[[http://localhost:8080__|http://localhost:8080_]] for the app. The | |||
created project is a standard SBT-project that uses the normal maven | |||
style layout, so you'll find the application source from_ | |||
src/main/scala__.__ | |||
To create Eclipse project files, type _eclipse_ in the sbt prompt. After | |||
this, the project can be imported as an Eclipse project. | |||
[[scaladin]] | |||
Scaladin | |||
~~~~~~~~ | |||
Scaladin is a library that extends Vaadin and adds Scala-like features | |||
to Vaadin classes. It's just a single add-on (one JAR) and is highly | |||
recommended for any Scala Vaadin development. See the | |||
http://github.com/henrikerola/scaladin/wiki[GitHub wiki] and the | |||
https://vaadin.com/directory/component/scaladin[Directory page] for more information. |
@@ -0,0 +1,106 @@ | |||
[[showing-data-in-grid]] | |||
Showing data in Grid | |||
-------------------- | |||
Grid lazy-loads data from a `Container` instance. There are different | |||
container implementations that e.g. fetch data from a database or use a | |||
list of Java objects. Assuming you already have code that initializes a | |||
`Container`, this is all that is needed for showing a Grid with the data | |||
from your container. | |||
[source,java] | |||
.... | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.UI; | |||
public class UsingGridWithAContainer extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
Grid grid = new Grid(); | |||
grid.setContainerDataSource(GridExampleHelper.createContainer()); | |||
setContent(grid); | |||
} | |||
} | |||
.... | |||
The container in this example contains three properties; name, count and | |||
amount. You can configure the columns in Grid using the property ids to | |||
do things like setting the column caption, removing a column or changing | |||
the order of the visible columns. | |||
[source,java] | |||
.... | |||
protected void init(VaadinRequest request) { | |||
Grid grid = new Grid(); | |||
grid.setContainerDataSource(GridExampleHelper.createContainer()); | |||
grid.getColumn("name").setHeaderCaption("Bean name"); | |||
grid.removeColumn("count"); | |||
grid.setColumnOrder("name", "amount"); | |||
setContent(grid); | |||
} | |||
.... | |||
This is really all that is needed to get started with Grid. | |||
For reference, this is how the example container is implemented. | |||
[source,java] | |||
.... | |||
public class GridExampleBean { | |||
private String name; | |||
private int count; | |||
private double amount; | |||
public GridExampleBean() { | |||
} | |||
public GridExampleBean(String name, int count, double amount) { | |||
this.name = name; | |||
this.count = count; | |||
this.amount = amount; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public int getCount() { | |||
return count; | |||
} | |||
public double getAmount() { | |||
return amount; | |||
} | |||
public void setName(String name) { | |||
this.name = name; | |||
} | |||
public void setCount(int count) { | |||
this.count = count; | |||
} | |||
public void setAmount(double amount) { | |||
this.amount = amount; | |||
} | |||
} | |||
.... | |||
[source,java] | |||
.... | |||
import com.vaadin.data.util.BeanItemContainer; | |||
public class GridExampleHelper { | |||
public static BeanItemContainer<GridExampleBean> createContainer() { | |||
BeanItemContainer<GridExampleBean> container = new BeanItemContainer<GridExampleBean>(GridExampleBean.class); | |||
for (int i = 0; i < 1000; i++) { | |||
container.addItem(new GridExampleBean("Bean " + i, i * i, i / 10d)); | |||
} | |||
return container; | |||
} | |||
} | |||
.... |
@@ -0,0 +1,163 @@ | |||
[[showing-extra-data-for-grid-rows]] | |||
Showing extra data for Grid rows | |||
-------------------------------- | |||
Some data might not be suitable to be shown as part of a regular Grid, | |||
e.g. because it's too large to fit into a Grid cell or because it's | |||
secondary information that should only be shown on demand. This kind of | |||
situation is covered with the row details functionality that shows a | |||
Vaadin Component in an area expanded below a specific row. Using this | |||
functionality is a two step process: first you need to implement a | |||
generator that lazily creates the `Component` for a row if it has been | |||
expanded, and then you need to hook up the events for actually expanding | |||
a row. | |||
This example uses the same data as in the | |||
link:UsingGridWithAContainer.asciidoc[Using Grid with a Container] | |||
example. | |||
[[detailsgenerator]] | |||
DetailsGenerator | |||
^^^^^^^^^^^^^^^^ | |||
A details generator is a callback interface that Grid calls to create | |||
the Vaadin `Component` that is used for showing the details for a specific | |||
row. In this example, we create a layout that contains a label, an image | |||
and a button that all use data from the row. | |||
[source,java] | |||
.... | |||
grid.setDetailsGenerator(new DetailsGenerator() { | |||
@Override | |||
public Component getDetails(RowReference rowReference) { | |||
// Find the bean to generate details for | |||
final GridExampleBean bean = (GridExampleBean) rowReference.getItemId(); | |||
// A basic label with bean data | |||
Label label = new Label("Extra data for " + bean.getName()); | |||
// An image with extra details about the bean | |||
Image image = new Image(); | |||
image.setWidth("300px"); | |||
image.setHeight("150px"); | |||
image.setSource(new ExternalResource("http://dummyimage.com/300x150/000/fff&text=" + bean.getCount())); | |||
// A button just for the sake of the example | |||
Button button = new Button("Click me", new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
Notification.show("Button clicked for " + bean.getName()); | |||
} | |||
}); | |||
// Wrap up all the parts into a vertical layout | |||
VerticalLayout layout = new VerticalLayout(label, image, button); | |||
layout.setSpacing(true); | |||
layout.setMargin(true); | |||
return layout; | |||
} | |||
}); | |||
.... | |||
[[opening-the-details-for-a-row]] | |||
Opening the details for a row | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Since there are multiple different UI patterns for how details should be | |||
opened (e.g. clicking a button in a cell or double clicking anywhere on | |||
the row), Grid does not have any action enabled by default. You can | |||
instead implement your own listener that takes care of showing and | |||
hiding the details for the rows. One easy way of doing this is to add an | |||
item click listener that toggles the status whenever a row is double | |||
clicked. | |||
[source,java] | |||
.... | |||
grid.addItemClickListener(new ItemClickListener() { | |||
@Override | |||
public void itemClick(ItemClickEvent event) { | |||
if (event.isDoubleClick()) { | |||
Object itemId = event.getItemId(); | |||
grid.setDetailsVisible(itemId, !grid.isDetailsVisible(itemId)); | |||
} | |||
} | |||
}); | |||
.... | |||
[[full-example]] | |||
Full example | |||
^^^^^^^^^^^^ | |||
Putting all these pieces together, we end up with this class that uses | |||
the same data as in the link:UsingGridWithAContainer.asciidoc[Using | |||
Grid with a Container] example. | |||
[source,java] | |||
.... | |||
import com.vaadin.event.ItemClickEvent; | |||
import com.vaadin.event.ItemClickEvent.ItemClickListener; | |||
import com.vaadin.server.ExternalResource; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.Button; | |||
import com.vaadin.ui.Button.ClickEvent; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.Grid.DetailsGenerator; | |||
import com.vaadin.ui.Grid.RowReference; | |||
import com.vaadin.ui.Image; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.Notification; | |||
import com.vaadin.ui.UI; | |||
import com.vaadin.ui.VerticalLayout; | |||
public class ShowingExtraDataForRows extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
final Grid grid = new Grid(); | |||
grid.setContainerDataSource(GridExampleHelper.createContainer()); | |||
grid.setDetailsGenerator(new DetailsGenerator() { | |||
@Override | |||
public Component getDetails(RowReference rowReference) { | |||
// Find the bean to generate details for | |||
final GridExampleBean bean = (GridExampleBean) rowReference.getItemId(); | |||
// A basic label with bean data | |||
Label label = new Label("Extra data for " + bean.getName()); | |||
// An image with extra details about the bean | |||
Image image = new Image(); | |||
image.setWidth("300px"); | |||
image.setHeight("150px"); | |||
image.setSource(new ExternalResource("http://dummyimage.com/300x150/000/fff&text=" + bean.getCount())); | |||
// A button just for the sake of the example | |||
Button button = new Button("Click me", new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
Notification.show("Button clicked for " + bean.getName()); | |||
} | |||
}); | |||
// Wrap up all the parts into a vertical layout | |||
VerticalLayout layout = new VerticalLayout(label, image, button); | |||
layout.setSpacing(true); | |||
layout.setMargin(true); | |||
return layout; | |||
} | |||
}); | |||
grid.addItemClickListener(new ItemClickListener() { | |||
@Override | |||
public void itemClick(ItemClickEvent event) { | |||
if (event.isDoubleClick()) { | |||
Object itemId = event.getItemId(); | |||
grid.setDetailsVisible(itemId, !grid.isDetailsVisible(itemId)); | |||
} | |||
} | |||
}); | |||
setContent(grid); | |||
} | |||
} | |||
.... |
@@ -0,0 +1,99 @@ | |||
[[simplified-rpc-using-javascript]] | |||
Simplified RPC using JavaScript | |||
------------------------------- | |||
This tutorial continues where | |||
link:IntegratingAJavaScriptComponent.asciidoc[Integrating a JavaScript | |||
component] ended. We will now add RPC functionality to the JavaScript | |||
Flot component. RPC can be used in the same way as with ordinary GWT | |||
components as described in link:UsingRPCFromJavaScript.asciidoc[Using | |||
RPC from JavaScript]. This tutorial describes a simplified way that is | |||
based on the same concepts as in | |||
link:ExposingServerSideAPIToJavaScript.asciidoc[Exposing server | |||
side API to JavaScript]. This way of doing RPC is less rigorous and is | |||
intended for simple cases and for developers appreciating the dynamic | |||
nature of JavaScript. | |||
The simplified way is based on single callback functions instead of | |||
interfaces containing multiple methods. We will invoke a server-side | |||
callback when the user clicks a data point in the graph and a | |||
client-side callback for highlighting a data point in the graph. Each | |||
callback takes a data series index and the index of a point in that | |||
series. | |||
In the constructor, we register the callback that will be called from | |||
the client-side when a data point is clicked. | |||
[source,java] | |||
.... | |||
public Flot() { | |||
addFunction("onPlotClick", new JavaScriptFunction() { | |||
public void call(JsonArray arguments) throws JSONException { | |||
int seriesIndex = arguments.getInt(0); | |||
int dataIndex = arguments.getInt(1); | |||
Notification.show("Clicked on [" + seriesIndex + ", " | |||
+ dataIndex + "]"); | |||
} | |||
}); | |||
} | |||
.... | |||
Highlighting is implemented by invoking the client-side callback | |||
function by name and passing the appropriate arguments. | |||
[source,java] | |||
.... | |||
public void highlight(int seriesIndex, int dataIndex) { | |||
callFunction("highlight", seriesIndex, dataIndex); | |||
} | |||
.... | |||
The simplified RPC mechanism is based on JavaScript functions attached | |||
directly to the connector wrapper object. Callbacks registered using the | |||
server-side `registerCallback` method will be made available as a | |||
similarly named function on the connector wrapper and functions in the | |||
connector wrapper object matching the name used in a server-side | |||
`callFunction` will be called. Because of the dynamic nature of | |||
JavaScript, it's the developer's responsibility to avoid naming | |||
conflicts. | |||
We need to make some small adjustments to the connector JavaScript to | |||
make it work with the way Flot processes events. Because a new Flot | |||
object is created each time the onStateChange function is called, we | |||
need to store a reference to the current object that we can use for | |||
applying the highlight. We also need to pass a third parameter to | |||
`$.plot` to make the graph area clickable. We are finally storing a | |||
reference to `this` in the `self` variable because `this` will point to | |||
a different object inside the click event handler. Aside from those | |||
changes, we just call the callback in a click listener and add our own | |||
callback function for highlighting a point. | |||
[source,javascript] | |||
.... | |||
window.com_example_Flot = function() { | |||
var element = $(this.getElement()); | |||
var self = this; | |||
var flot; | |||
this.onStateChange = function() { | |||
flot = $.plot(element, this.getState().series, {grid: {clickable: true}}); | |||
} | |||
element.bind('plotclick', function(event, point, item) { | |||
if (item) { | |||
self.onPlotClick(item.seriesIndex, item.dataIndex); | |||
} | |||
}); | |||
this.highlight = function(seriesIndex, dataIndex) { | |||
if (flot) { | |||
flot.highlight(seriesIndex, dataIndex); | |||
} | |||
}; | |||
} | |||
.... | |||
When the simplified RPC functionality designed for JavaScript | |||
connectors, there's no need to define RPC interfaces for communication. | |||
This fits the JavaScript world nicely and makes your server-side code | |||
more dynamic - for better or for worse. |
@@ -0,0 +1,107 @@ | |||
[[using-grid-with-a-container]] | |||
Using Grid with a Container | |||
--------------------------- | |||
Grid lazy-loads data from a `Container` instance. There are different | |||
container implementations that e.g. fetch data from a database or use a | |||
list of Java objects. Assuming you already have code that initializes a | |||
`Container`, this is all that is needed for showing a Grid with the data | |||
from your container. | |||
[source,java] | |||
.... | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.UI; | |||
public class UsingGridWithAContainer extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
Grid grid = new Grid(); | |||
grid.setContainerDataSource(GridExampleHelper.createContainer()); | |||
setContent(grid); | |||
} | |||
} | |||
.... | |||
The container in this example contains three properties; name, count and | |||
amount. You can configure the columns in Grid using the property ids to | |||
do things like setting the column caption, removing a column or changing | |||
the order of the visible columns. | |||
[source,java] | |||
.... | |||
protected void init(VaadinRequest request) { | |||
Grid grid = new Grid(); | |||
grid.setContainerDataSource(GridExampleHelper.createContainer()); | |||
grid.getColumn("name").setHeaderCaption("Bean name"); | |||
grid.removeColumn("count"); | |||
grid.setColumnOrder("name", "amount"); | |||
setContent(grid); | |||
} | |||
.... | |||
This is really all that is needed to get started with Grid. | |||
For reference, this is how the example container is implemented. | |||
[source,java] | |||
.... | |||
public class GridExampleBean { | |||
private String name; | |||
private int count; | |||
private double amount; | |||
public GridExampleBean() { | |||
} | |||
public GridExampleBean(String name, int count, double amount) { | |||
this.name = name; | |||
this.count = count; | |||
this.amount = amount; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public int getCount() { | |||
return count; | |||
} | |||
public double getAmount() { | |||
return amount; | |||
} | |||
public void setName(String name) { | |||
this.name = name; | |||
} | |||
public void setCount(int count) { | |||
this.count = count; | |||
} | |||
public void setAmount(double amount) { | |||
this.amount = amount; | |||
} | |||
} | |||
.... | |||
[source,java] | |||
.... | |||
import com.vaadin.data.util.BeanItemContainer; | |||
public class GridExampleHelper { | |||
public static BeanItemContainer<GridExampleBean> createContainer() { | |||
BeanItemContainer<GridExampleBean> container = new BeanItemContainer<GridExampleBean>( | |||
GridExampleBean.class); | |||
for (int i = 0; i < 1000; i++) { | |||
container.addItem(new GridExampleBean("Bean " + i, i * i, i / 10d)); | |||
} | |||
return container; | |||
} | |||
} | |||
.... |
@@ -0,0 +1,91 @@ | |||
[[using-grid-with-inline-data]] | |||
Using Grid with inline data | |||
--------------------------- | |||
Instead of using a Vaadin Container as explained in | |||
link:UsingGridWithAContainer.asciidoc[Using Grid with a Container], | |||
you can also directly add simple inline data to Grid without directly | |||
using a Container. | |||
After creating a Grid instance, the first thing you need to do is to | |||
define the columns that should be shown. You an also define the types of | |||
the data in each column - Grid will expect String data in each column | |||
unless you do this. | |||
[source,java] | |||
.... | |||
grid.addColumn("Name").setSortable(true); | |||
grid.addColumn("Score", Integer.class); | |||
.... | |||
The columns will be shown in the order they are added. The `addColumn` | |||
method does also return the created `Column` instance, so you can go ahead | |||
and configure the column right away if you want to. | |||
When you have added all columns, you can add data using the | |||
`addRow(Object...)` method. | |||
[source,java] | |||
.... | |||
grid.addRow("Alice", 15); | |||
grid.addRow("Bob", -7); | |||
grid.addRow("Carol", 8); | |||
grid.addRow("Dan", 0); | |||
grid.addRow("Eve", 20); | |||
.... | |||
The order of the arguments to `addRow` should match the order in which the | |||
columns are shown. It is recommended to only use `addRow` when | |||
initializing Grid, since later on e.g. `setColumnOrder(Object...)` might | |||
have been used to change the order, causing unintended behavior. | |||
Grid will still manage a `Container` instance for you behind the scenes, | |||
so you can still use Grid API that is based on `Property` or `Item` from the | |||
`Container` API. One particularly useful feature is that each added row | |||
will get an `Integer` item id, counting up starting from 1. This means | |||
that you can e.g. select the second row in this way: | |||
[source,java] | |||
.... | |||
grid.select(2); | |||
.... | |||
[[full-example]] | |||
Full example | |||
^^^^^^^^^^^^ | |||
Putting all these pieces together, we end up with this class. | |||
[source,java] | |||
.... | |||
import com.vaadin.annotations.Theme; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.shared.ui.grid.HeightMode; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.UI; | |||
@Theme("valo") | |||
public class ShowingInlineDataInGrid extends UI { | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
final Grid grid = new Grid(); | |||
grid.addColumn("Name").setSortable(true); | |||
grid.addColumn("Score", Integer.class); | |||
grid.addRow("Alice", 15); | |||
grid.addRow("Bob", -7); | |||
grid.addRow("Carol", 8); | |||
grid.addRow("Dan", 0); | |||
grid.addRow("Eve", 20); | |||
grid.select(2); | |||
grid.setHeightByRows(grid.getContainerDataSource().size()); | |||
grid.setHeightMode(HeightMode.ROW); | |||
setContent(grid); | |||
} | |||
} | |||
.... |
@@ -0,0 +1,433 @@ | |||
[[using-hibernate-with-vaadin]] | |||
Using Hibernate with Vaadin | |||
--------------------------- | |||
Using Hibernate in Toolkit application, Basic | |||
http://en.wikipedia.org/wiki/Create,_read,_update_and_delete[CRUD] | |||
actions for persistent POJO | |||
image:img/screenshot.png[Example CRUD application] | |||
Check out related source code with subversion (svn co | |||
http://dev.vaadin.com/svn/incubator/hbncontainer/) or view it with trac | |||
http://dev.vaadin.com/browser/incubator/hbncontainer/. Download the | |||
latest version as a Vaadin add-on from the Vaadin Directory (https://vaadin.com/directory/component/hbncontainer) | |||
_The project in incubator currently has a prototype of using | |||
associations. The article is outdated on that part_. | |||
Hibernate is the de facto standard when it comes to Java and Object | |||
Relational Mapping. Since version 3 onwards one can actually drop the de | |||
facto part as Hibernate 3 implements Java Persistency API with some | |||
optional packages. Hibernate is backed by a strong support from both | |||
commercial players and open source community. It is an important part of | |||
popular JBoss Application Server. | |||
As an open source project with an industry proven maturity, Hibernate | |||
makes a perfect combo with IT Mill Toolkit. Hibernate is in a key role | |||
in many projects built or supported by IT Mill. The way Hibernate is | |||
used varies a lot due different kinds of architectures and requirements. | |||
Largest questions are usually how to work with Hibernate session, | |||
transactions and how to tie entity beans into toolkit components. | |||
In this article and example application I'll show you how to implement | |||
session-per-request pattern for Hibernate session handling and present | |||
some patterns to do | |||
http://en.wikipedia.org/wiki/Create,_read,_update_and_delete[CRUD] | |||
actions of a simple entity bean. As I'm a sport fanatic, instead of | |||
storing cats and other mammals to DB we'll build a simple *WorkoutLog* | |||
application to store the details of our jogging sessions. Download the | |||
source package to see full source code. | |||
Note that this is not trying to be a yet another Hibernate tutorial. | |||
Although we'll stay in rather basic tricks, I expect the reader to have | |||
some experience on ORM and IT Mill Toolkit. The purpose of this tutorial | |||
is to show an example how to do simple Hibernate session handling in | |||
Toolkit application and explain some patterns how to entity objects can | |||
be tied into GUI. | |||
[[preparing-the-project]] | |||
Preparing the project | |||
~~~~~~~~~~~~~~~~~~~~~ | |||
If you want want to learn by doing, it is time to put your hands on | |||
dirt. Create a new web application project in your favorite IDE, throw | |||
in latest `toolkit.jar` and all needed Hibernate related libraries. | |||
Prepare your database and configure Hibernate. Combo I chose when | |||
writing this article was Eclipse, WTP and MySQL 5, but any option should | |||
be fine. | |||
If you want to get started really easily, check out the Eclipse project | |||
from svn repository. This is done simply with subclipse plugin or via | |||
command line svn co http://dev.vaadin.com/svn/incubator/hbncontainer/. | |||
The project containtains embedded database( http://hsqldb.org/[HSQLDB] | |||
), all needed required libraries and the source code for the example | |||
project itself. That is an easy way to start experimenting with Toolkit | |||
and Hibernate. You will also need a servlet container, Tomcat is a good | |||
option. | |||
As I hate all xml configuration I created DB mappings with annotations. | |||
Below is the one and only entity class we'll be using in this example. | |||
Create it in and possibly test your Hibernate configuration with a | |||
simple test application. | |||
[source,java] | |||
.... | |||
@Entity | |||
public class Workout { | |||
@Id | |||
@GeneratedValue(strategy=GenerationType.AUTO) | |||
private Long id; | |||
private Date date = new Date(); | |||
private String title = " -- new workout -- "; | |||
private float kilometers; | |||
public Workout() {} | |||
public Long getId() { | |||
return id; | |||
} | |||
private void setId(Long id) { | |||
this.id = id; | |||
} | |||
public Date getDate() { | |||
return date; | |||
} | |||
public void setDate(Date date) { | |||
this.date = date; | |||
} | |||
public String getTitle() { | |||
return title; | |||
} | |||
public void setTitle(String title) { | |||
this.title = title; | |||
} | |||
public float getKilometers() { | |||
return kilometers; | |||
} | |||
public void setKilometers(float kilometers) { | |||
this.kilometers = kilometers; | |||
} | |||
} | |||
.... | |||
Also create a new Tookit application, configure it in web.xml. | |||
[[using-session-per-request-pattern]] | |||
Using session-per-request pattern | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Proper session handling in Hibernate backed applications is often the | |||
most difficult problem. Use cases vary from by architecture and load. | |||
Hibernate is known to be quite strict on session and transaction | |||
handling, so to save yourself from a headache, I'd suggest you to make | |||
it right. There is a lot's of good documentation about different session | |||
handling patterns in hibernate.org. | |||
Using session-per-request pattern is often a safe bet for Toolkit | |||
application. It is maybe the most common pattern among all Servlet based | |||
applications. When doing data manipulation we'll use the same session | |||
during the whole request and in the end of the request make sure that | |||
session and transaction is properly finalized. When implemented | |||
properly, session-per-request pattern guarantees that number of | |||
Hibernate sessions is in control, sessions are properly closed and | |||
sessions are flushed regularly. A good combo of characteristics for a | |||
multi-user web application. | |||
By Toolkits nature, session-per-request pattern is actually kind of | |||
wrong. Toolkit is a general purpose GUI framework and programmer does | |||
not need to think about requests and responses at all. Actually Toolkit | |||
applications and components don't know nothing about requests. It its | |||
the web terminal that does all the web magic. Another option is to use | |||
session-per-application or even session-per-transaction like one would | |||
do with SWING or other destop application. Always evaluate your | |||
requirements, use cases and available computing resources to have the | |||
optimal session handling pattern. | |||
To ensure that we are using only one Hibernate session per http request | |||
is the easy part. We can use Hibernates `getCurrentSession()` to retrieve | |||
thread local session instance. As we always want to actually use the | |||
session I build a helper method that will also begin a database | |||
transaction. In our *WorkoutLog* we will always be using this method to | |||
get session reference. | |||
[source,java] | |||
.... | |||
/** | |||
* Used to get current Hibernate session. Also ensures an open Hibernate | |||
* transaction. | |||
*/ | |||
public Session getSession() { | |||
Session currentSession = HibernateUtil.getSessionFactory() | |||
.getCurrentSession(); | |||
if(!currentSession.getTransaction().isActive()) { | |||
currentSession.beginTransaction(); | |||
} | |||
return currentSession; | |||
} | |||
.... | |||
Closing is bit more tricky. One way around would be to use a servlet | |||
filter. You can find examples of this from hibernate.org. But we'll keep | |||
toolkits terminal independence in mind and don't pollute our program | |||
with servlet specific code. To properly implement session-per-request | |||
pattern we'll need to familiarize ourselves to a feature in Toolkits | |||
terminal. Ideally toolkit programmer don't need to care about terminal | |||
at all, but now we need to hook some logic into the end of (http) | |||
request that don't exist for the application. For the pattern it is | |||
essential that session finalization is done always and and after all | |||
hibernate related stuff is done. With event based programming model | |||
there is no way we can detect the last database action in the actual | |||
program code. | |||
The feature we need is `TransactionListeners`. `TransactionListeners` are | |||
attached to `ApplicationContext` which corresponds to http session in our | |||
current web terminal. `TransactionListeners` are notified right before | |||
and right after the clients state is synchronized with server. The | |||
transaction end is what we need here. I'll attach the transaction | |||
listener in the applications `init()` like this: | |||
[source,java] | |||
.... | |||
getContext().addTransactionListener(new TransactionListener() { | |||
public void transactionEnd(Application application, | |||
Object transactionData) { | |||
// Transaction listener gets fired for all contexts | |||
// (HttpSessions) toolkit applications, checking to be this one. | |||
if (application == WorkoutLog.this) { | |||
closeSession(); | |||
} | |||
} | |||
public void transactionStart(Application application, Object transactionData) { | |||
} | |||
}); | |||
.... | |||
In `closeSession()` the usual Hibernate sessions finalization is done. | |||
[source,java] | |||
.... | |||
private void closeSession() { | |||
Session sess = HibernateUtil.getSessionFactory().getCurrentSession(); | |||
if(sess.getTransaction().isActive()) { | |||
sess.getTransaction().commit(); | |||
} | |||
sess.flush(); | |||
sess.close(); | |||
} | |||
.... | |||
The sequence diagram below shows how Session handling works with this | |||
pattern during one (http) request. It is an imaginary server visit that | |||
fires to event listeners. The first one does some listing and the latter | |||
re-attaches detached pojo. Note that the second database/Hibernate | |||
action uses the same Session object as the first one. Note that function | |||
names are not real ones, but trying to describe the process better. | |||
image:img/sd_s_per_r.gif[Session handling sequence diagram] | |||
Due Toolkit applications do have state, pattern can be defined more | |||
strictly as a session-per-request-with-detached-objects pattern. As the | |||
session closes quite often, our entity objects are most likely detached | |||
by the time we are updating them. So when we have our changes to entity | |||
object done, it is time to re-attach it to current session to persist | |||
changes into database. An example of that is below: | |||
[source,java] | |||
.... | |||
run.setDate((Date) date.getValue()); | |||
run.setKilometers(Float.parseFloat(kilomiters.getValue().toString())); | |||
run.setTitle((String) title.getValue()); | |||
getSession().merge(run); | |||
.... | |||
[[attaching-pojos-ui]] | |||
Attaching POJO's UI | |||
~~~~~~~~~~~~~~~~~~~ | |||
In this chapter I'll discuss briefly some options to implement basic | |||
CRUD (Create, Read, Update, Delete) actions for our DB backed Workout | |||
objects. | |||
[[listing-objects]] | |||
Listing Objects | |||
^^^^^^^^^^^^^^^ | |||
If you are learning by doing, I'd suggest that you manually insert some | |||
rows to your db at this point. Listing an empty database will be quite | |||
boring. | |||
The most natural way to list our simple Workout object is to put them | |||
into Table component. To do this there is an easy way and an the right | |||
way. We'll start with the easy one, but I suggest to use the latter in | |||
real applications. The code below (the "easy" way) is not in the | |||
*WorkoutLog* app at all, but you can try it if you want. | |||
[source,java] | |||
.... | |||
// prepare tables container | |||
table.addContainerProperty("date", Date.class, null); | |||
table.addContainerProperty("kilometers", Float.class, null); | |||
table.addContainerProperty("title", String.class, null); | |||
// list all Workouts | |||
List workouts = getSession().createCriteria(Workout.class).list(); | |||
for (Iterator iterator = workouts.iterator(); iterator.hasNext();) { | |||
Workout wo = (Workout) iterator.next(); | |||
// add item to table and set properties from POJO | |||
Item woItem = table.addItem(wo.getId()); | |||
woItem.getItemProperty("date").setValue(wo.getDate()); | |||
woItem.getItemProperty("kilometers").setValue(wo.getKilometers()); | |||
woItem.getItemProperty("title").setValue(wo.getTitle()); | |||
} | |||
.... | |||
In the above example we are using Table's default container, | |||
`IndexedContainer`. It is a good general purpose container, but using it | |||
always is not a good option. You have to load the data into it by | |||
yourself and configure properties etc. It also stores everything in | |||
memory. In our example it may start to be a problem if you | |||
do three workouts everyday, live 100 years old and memory chips don't | |||
get cheaper in the future. But in real application we might really have | |||
millions of records in DB. I really wouldn't suggest to load that table | |||
into memory anymore. | |||
As you may guess the way is to build our own container for Workouts. | |||
Building good containers is one of the most difficult tasks in Toolkit | |||
programming. There are number of different sub interfaces one might want | |||
to implement and a whole bunch of methods code. Luckily one can't safely | |||
throw `UnsupportedOperationExeception` for many of those. It is a boring | |||
tasks, but it often pays it back later. When you have your container | |||
ready, it hides lots of DB access from program logic and can be used for | |||
many components (Selects, Trees, Tables etc). With your own customized | |||
container you can also tune it to work as you want (memory-consumption | |||
versus speed etc). | |||
As building a full-featured is not in the scope of this article, it is | |||
time to throw in a nice helper class called `HbnContainer`. It takes a | |||
Hibernate entity class and a strategy to get Hibernate session in its | |||
constructor. It is indexed, ordered, sortable, had a limited supports | |||
adding/removing items and even ought to be fairly well scalable (by | |||
number of rows in DB). It is not part of Toolkit as we don't consider it | |||
ready for framework yet, but we hope to have something similar in the | |||
core Toolkit in later releases. But feel free to use it in you own | |||
projects. | |||
With `HbnContainer` loading table with Workouts simplifies quite a bit. | |||
We need to implement `HbnContainer`.`SessionManager` interface, but it is | |||
rather easy task as we already have getSession named function in our | |||
*WorkoutLog*. Create and add table to your application, load its content | |||
with following code snippet and you should have a Workout listing on | |||
your screen. | |||
[source,java] | |||
.... | |||
table.setContainerDataSource(new HbnContainer(Workout.class, this)); | |||
.... | |||
[[creating-workouts]] | |||
Creating workouts | |||
^^^^^^^^^^^^^^^^^ | |||
Now that we have listing we might want to add some rows via our web | |||
interface. To create a new Workout instance and store it in to DB we | |||
have to do the usual Hibernate stuff: instantiate POJO and attach it to | |||
session. But as I hinted earlier, having a good container will help us | |||
to do it even simpler. `HbnContainer` supports adding items with the most | |||
simplest method `addItem()`. | |||
If you look into the implementation, it does all the usual Hibernates | |||
stuff and returns items generated identifier. In addition this it also | |||
notifies appropriate listeners that the content of table has changed. So | |||
by using containers `addItem()` method instead of doing DB persist | |||
ourselves we don't need to worry about UI updates. Table listens to its | |||
container changes and changes gets sent to web browsers. | |||
[[updates-and-deletes]] | |||
Updates and deletes | |||
^^^^^^^^^^^^^^^^^^^ | |||
Building an editor for our Workout object is a straight forwarded coding | |||
task. You may organize your code just like you want. `WorkoutEditor` | |||
class is a simple example implementation that shows and editor in | |||
floating window. It has fields for workouts properties and it can be | |||
loaded with Workout instance or with an identifier. In `WorkoutLog` I | |||
attached a `ValueChangeListener` into table to open editor when user | |||
clicks a row in table. Save and delete buttons in `WorkoutEditor` | |||
delegates work back to methods in main application. Delete uses | |||
containers method and behind the scenes a normal Hibernate object | |||
deletion. When saving we just reattach detached object using `merge()`. | |||
To avoid "monkey-coding" I'll show one can to use toolkits advanced | |||
features to automatically create editable fields for items. The | |||
`WorkoutEditor` class could have created its fields automatically by | |||
using appropriate Item and a Form component. Also Table supports | |||
automatic field generation, so why not edit workouts directly in our | |||
main object listing? | |||
All we need to do is to use `setEditable()` method. In `WorkoutLog` there | |||
is a button that toggles this feature. Clicking it make table editable, | |||
clicking it again shows data only. Can't imagine any simpler way to do | |||
the 'U' part of CRUD. | |||
Both Form and Table components use `FieldFactory` interface to | |||
automatically create fields for Items properties. There is a simple | |||
default factory that you almost certainly want to modify for your needs. | |||
As an example I extended it to set proper resolution for date field and | |||
also did some other fine tuning. | |||
If you investigate the code a bit you might wonder how the database is | |||
updated now as we don't seem to call `merge()` or any other method to | |||
re-attached POJO. When field is updated it knows only about its | |||
underlaying Property. In this case it is `EntityItemProperty` built by | |||
`HbnContainer`. Field calls its `setValue()` method and that is where the | |||
underlaying POJO is re-attached into Hibernate session. | |||
[[adding-custom-columns-to-hbncontainer]] | |||
Adding custom columns to HbnContainer | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
This last bonus chapter is bit out of scope of the article. But as | |||
updating is so easy in Table we could ditch our `WorkoutEditor`. But then | |||
arises a question how to implement deletion. An option is to use Tables | |||
selection feature and "Delete selected" button. Another one is to use | |||
context menu option. This is also done in `WorkoutLog`. Both are good | |||
options, but someday someone will be asking how to add delete button on | |||
each row. So lets discuss that right away. | |||
Ideologically this is adding a new property to our items. We definitely | |||
don't want to pollute our entity object by adding `public Button | |||
getDelete()` to our Workout object. The right place to implement this is | |||
in custom Container and Item. I implemented an example of this by | |||
extending `HbnContainer` to `WorkoutListingWithSteroids`. It adds a column | |||
"actions" (or container property if we are talking "Toolkit") which is a | |||
layout containing two buttons. | |||
Another possibly little bit easier method is to use recently introduced | |||
feature in Table component called `ColumnGenerator`. *WorkoutLog* (in svn) | |||
has an example of this method too. | |||
Check out the example code if you want this kind of behavior. | |||
[[summary]] | |||
Summary | |||
~~~~~~~ | |||
Popular open source ORM tool Hibernate is a perfect companion for IT | |||
Mill Toolkit. Finding the right way to handle session in your | |||
application is a often the most critical task. Session-per-request | |||
pattern is a safe choice for Toolkit application, but not the only | |||
option. DB backed entity objects are used in a usual manner. To use more | |||
advanced features of toolkit, you'll want to use a custom built | |||
container-item-property set. ORM is never easy, but it is not a rocket | |||
science if you use tested industry proven patterns. And if your | |||
application is going to be a big or old, I can guarantee that you will | |||
have a nice ROI for hours you spend on it (ORM). |
@@ -0,0 +1,413 @@ | |||
[[using-jdbc-with-lazy-query-container-and-filteringtable]] | |||
Using JDBC with Lazy Query Container and FilteringTable | |||
------------------------------------------------------- | |||
Introduction | |||
Populating display tables from a database is a deceptively complicated | |||
operation, especially when mixing multiple techniques together. This | |||
page provides an example of one way to efficiently load data from a SQL | |||
database table into a filterable UI, using the _Lazy Query Container_ and | |||
_FilteringTable_ add-ons. | |||
Note: Do not use the SQLContainer package. This is buggy and will have | |||
your database and garbage collector crunching in loops. | |||
`Query` and `QueryFactory` implementation | |||
The place to start is the Lazy Query Container's (LQC) Query interface. | |||
This is where the interface with your database happens. This example | |||
access a database table with computer statistics. It's read-only. How to | |||
log and access your JDBC connection differs in each environment; they | |||
are treated generically here. Only select imports are included. | |||
[source,java] | |||
.... | |||
import org.vaadin.addons.lazyquerycontainer.Query; | |||
import org.vaadin.addons.lazyquerycontainer.QueryDefinition; | |||
import org.vaadin.addons.lazyquerycontainer.QueryFactory; | |||
import com.vaadin.data.Container.Filter; | |||
import com.vaadin.data.Item; | |||
import com.vaadin.data.util.ObjectProperty; | |||
import com.vaadin.data.util.PropertysetItem; | |||
import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; | |||
import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; | |||
/** | |||
* Query for using the database's device-status table as a data source | |||
* for a Vaadin container (table). | |||
*/ | |||
public class DeviceStatusQuery implements Query { | |||
private static final Logger log = LoggerFactory.getLogger(DeviceStatusQuery.class); | |||
/** | |||
* The table column names. Use these instead of typo-prone magic strings. | |||
*/ | |||
public static enum Column { | |||
hostname, loc_id, update_when, net_ip, lan_ip, lan_mac, hardware, | |||
opsys, image, sw_ver, cpu_load, proc_count, mem_usage, disk_usage; | |||
public boolean is(Object other) { | |||
if (other instanceof String) | |||
return this.toString().equals(other); | |||
else | |||
return (this == other); | |||
} | |||
}; | |||
public static class Factory implements QueryFactory { | |||
private int locId; | |||
/** | |||
* Constructor | |||
* @param locId - location ID | |||
*/ | |||
public Factory(int locId) { | |||
this.locId = locId; | |||
} | |||
@Override | |||
public Query constructQuery(QueryDefinition def) { | |||
return new DeviceStatusQuery(def, locId); | |||
} | |||
}//class Factory | |||
/////// INSTANCE /////// | |||
private String countQuery; | |||
private String fetchQuery; | |||
/** Borrow from SQLContainer to build filter queries */ | |||
private StatementHelper stmtHelper = new StatementHelper(); | |||
/** | |||
* Constructor | |||
* @param locId - location ID | |||
* @param userId - ID of user viewing the data | |||
*/ | |||
private DeviceStatusQuery(QueryDefinition def, int locId) { | |||
Build filters block List<Filter> filters = def.getFilters(); | |||
String filterStr = null; | |||
if (filters != null && !filters.isEmpty()) | |||
filterStr = QueryBuilder.getJoinedFilterString(filters, "AND", stmtHelper); | |||
// Count query | |||
StringBuilder query = new StringBuilder( "SELECT COUNT(*) FROM device_status"); | |||
query.append(" WHERE loc_id=").append(locId); | |||
if (filterStr != null) | |||
query.append(" AND ").append(filterStr); | |||
this.countQuery = query.toString(); | |||
// Fetch query | |||
query = new StringBuilder( | |||
"SELECT hostname, loc_id, update_when, net_ip, lan_ip, " + | |||
"lan_mac, hardware, opsys, image, sw_ver, cpu_load, " + | |||
"proc_count, mem_usage, disk_usage FROM device_status"); | |||
query.append(" WHERE loc_id=").append(locId); | |||
if (filterStr != null) | |||
query.append(" AND ").append(filterStr); | |||
// Build Order by | |||
Object[] sortIds = def.getSortPropertyIds(); | |||
if (sortIds != null && sortIds.length > 0) { | |||
query.append(" ORDER BY "); | |||
boolean[] sortAsc = def.getSortPropertyAscendingStates(); | |||
assert sortIds.length == sortAsc.length; | |||
for (int si = 0; si < sortIds.length; ++si) { | |||
if (si > 0) query.append(','); | |||
query.append(sortIds[si]); | |||
if (sortAsc[si]) query.append(" ASC"); | |||
else query.append(" DESC"); | |||
} | |||
} | |||
else query.append(" ORDER BY hostname"); | |||
this.fetchQuery = query.toString(); | |||
log.trace("DeviceStatusQuery count: {}", this.countQuery); | |||
log.trace("DeviceStatusQuery fetch: {}", this.fetchQuery); | |||
}//constructor | |||
@Override | |||
public int size() { | |||
int result = 0; | |||
try (Connection conn = Database.getConnection()) { | |||
PreparedStatement stmt = conn.prepareStatement(this.countQuery); | |||
stmtHelper.setParameterValuesToStatement(stmt); | |||
ResultSet rs = stmt.executeQuery(); | |||
if (rs.next()) result = rs.getInt(1); | |||
stmt.close(); | |||
} | |||
catch (SQLException ex) { | |||
log.error("DB access failure", ex); | |||
} | |||
log.trace("DeviceStatusQuery size=\{}", result); | |||
return result; | |||
} | |||
@Override | |||
public List<Item> loadItems(int startIndex, int count) { | |||
List<Item> items = new ArrayList<Item>(); | |||
try (Connection conn = Database.getConnection()) { | |||
String q = this.fetchQuery + " LIMIT " + count + " OFFSET " + startIndex; | |||
PreparedStatement stmt = conn.prepareStatement(q); | |||
stmtHelper.setParameterValuesToStatement(stmt); | |||
ResultSet rs = stmt.executeQuery(); | |||
while (rs.next()) { | |||
PropertysetItem item = new PropertysetItem(); | |||
// Include the data type parameter on ObjectProperty any time the value could be null | |||
item.addItemProperty(Column.hostname, | |||
new ObjectProperty<String>(rs.getString(1), String.class)); | |||
item.addItemProperty(Column.loc_id, | |||
new ObjectProperty<Integer>(rs.getInt(2), Integer.class)); | |||
item.addItemProperty(Column.update_when, | |||
new ObjectProperty<Timestamp>(rs.getTimestamp(3), Timestamp.class)); | |||
item.addItemProperty(Column.net_ip, | |||
new ObjectProperty<String>(rs.getString(4))); | |||
item.addItemProperty(Column.lan_ip, | |||
new ObjectProperty<String>(rs.getString(5))); | |||
item.addItemProperty(Column.lan_mac, | |||
new ObjectProperty<String>(rs.getString(6))); | |||
item.addItemProperty(Column.hardware, | |||
new ObjectProperty<String>(rs.getString(7))); | |||
item.addItemProperty(Column.opsys, | |||
new ObjectProperty<String>(rs.getString(8))); | |||
item.addItemProperty(Column.image, | |||
new ObjectProperty<String>(rs.getString(9))); | |||
item.addItemProperty(Column.sw_ver, | |||
new ObjectProperty<String>(rs.getString(10))); | |||
item.addItemProperty(Column.cpu_load, | |||
new ObjectProperty<String>(rs.getString(11))); | |||
item.addItemProperty(Column.proc_count, | |||
new ObjectProperty<Integer>(rs.getInt(12))); | |||
item.addItemProperty(Column.mem_usage, | |||
new ObjectProperty<Integer>(rs.getInt(13))); | |||
item.addItemProperty(Column.disk_usage, | |||
new ObjectProperty<Integer>(rs.getInt(14))); | |||
items.add(item); | |||
} | |||
rs.close(); | |||
stmt.close(); | |||
} | |||
catch (SQLException ex) { | |||
log.error("DB access failure", ex); | |||
} | |||
log.trace("DeviceStatusQuery load {} items from {}={} found", count, | |||
startIndex, items.size()); | |||
return items; | |||
} //loadItems() | |||
/** | |||
* Only gets here if loadItems() fails, so return an empty state. | |||
* Throwing from here causes an infinite loop. | |||
*/ | |||
@Override | |||
public Item constructItem() { | |||
PropertysetItem item = new PropertysetItem(); | |||
item.addItemProperty(Column.hostname, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.loc_id, new ObjectProperty<Integer>(-1)); | |||
item.addItemProperty(Column.update_when, | |||
new ObjectProperty<Timestamp>(new Timestamp(System.currentTimeMillis()))); | |||
item.addItemProperty(Column.net_ip, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.lan_ip, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.lan_mac, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.hardware, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.opsys, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.image, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.sw_ver, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.cpu_load, new ObjectProperty<String>("")); | |||
item.addItemProperty(Column.proc_count, new ObjectProperty<Integer>(0)); | |||
item.addItemProperty(Column.mem_usage, new ObjectProperty<Integer>(0)); | |||
item.addItemProperty(Column.disk_usage, new ObjectProperty<Integer>(0)); | |||
log.warn("Shouldn't be calling DeviceStatusQuery.constructItem()"); | |||
return item; | |||
} | |||
@Override | |||
public boolean deleteAllItems() { | |||
throw new UnsupportedOperationException(); | |||
} | |||
@Override | |||
public void saveItems(List<Item> arg0, List<Item> arg1, List<Item> arg2) { | |||
throw new UnsupportedOperationException(); | |||
} | |||
} | |||
.... | |||
Using the Query with FilteringTable | |||
Now that we have our Query, we need to create a table to hold it. Here's | |||
one of many ways to do it with FilteringTable. | |||
[source,java] | |||
.... | |||
import org.tepi.filtertable.FilterDecorator; | |||
import org.tepi.filtertable.numberfilter.NumberFilterPopupConfig; | |||
import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; | |||
import com.vaadin.data.Property; | |||
import com.vaadin.server.Resource; | |||
import com.vaadin.shared.ui.datefield.Resolution; | |||
import com.vaadin.ui.DateField; | |||
import com.vaadin.ui.AbstractTextField.TextChangeEventMode; | |||
/** | |||
* Filterable table of device statuses. | |||
*/ | |||
public class DeviceStatusTable extends FilterTable { | |||
private final | |||
String[] columnHeaders = {"Device", "Site", "Last Report", "Report IP", | |||
"LAN IP", "MAC Adrs", "Hardware", "O/S", "Image", "Software", "CPU" | |||
"Load", "Processes", "Memory Use", "Disk Use"}; | |||
/** | |||
* Configuration this table for displaying of DeviceStatusQuery data. | |||
*/ | |||
public void configure(LazyQueryContainer dataSource) { | |||
super.setFilterGenerator(new LQCFilterGenerator(dataSource)); | |||
super.setFilterBarVisible(true); | |||
super.setSelectable(true); | |||
super.setImmediate(true); | |||
super.setColumnReorderingAllowed(true); | |||
super.setColumnCollapsingAllowed(true); | |||
super.setSortEnabled(true); | |||
dataSource.addContainerProperty(Column.hostname, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.loc_id, Integer.class, null, true, false); | |||
dataSource.addContainerProperty(Column.update_when, Timestamp.class, null, true, true); | |||
dataSource.addContainerProperty(Column.net_ip, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.lan_ip, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.lan_mac, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.hardware, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.opsys, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.image, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.sw_ver, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.cpu_load, String.class, null, true, true); | |||
dataSource.addContainerProperty(Column.proc_count, Integer.class, null, true, true); | |||
dataSource.addContainerProperty(Column.mem_usage, Integer.class, null, true, true); | |||
dataSource.addContainerProperty(Column.disk_usage, Integer.class, null, true, true); | |||
super.setContainerDataSource(dataSource); | |||
super.setColumnHeaders(columnHeaders); | |||
super.setColumnCollapsed(Column.lan_mac, true); | |||
super.setColumnCollapsed(Column.opsys, true); | |||
super.setColumnCollapsed(Column.image, true); | |||
super.setFilterFieldVisible(Column.loc_id, false); | |||
} | |||
@Override | |||
protected String formatPropertyValue(Object rowId, Object colId, Property<?> property) { | |||
if (Column.loc_id.is(colId)) { | |||
// Example of how to translate a column value | |||
return Hierarchy.getLocation(((Integer) property.getValue())).getShortName(); | |||
} else if (Column.update_when.is(colId)) { | |||
// Example of how to format a value. | |||
return ((java.sql.Timestamp) property.getValue()).toString().substring(0, 19); | |||
} | |||
return super.formatPropertyValue(rowId, colId, property); | |||
} | |||
/** | |||
* Filter generator that triggers a refresh of a LazyQueryContainer | |||
* whenever the filters change. | |||
*/ | |||
public class LQCFilterGenerator implements FilterGenerator { | |||
private final LazyQueryContainer lqc; | |||
public LQCFilterGenerator(LazyQueryContainer lqc) { | |||
this.lqc = lqc; | |||
} | |||
@Override | |||
public Filter generateFilter(Object propertyId, Object value) { | |||
return null; | |||
} | |||
@Override | |||
public Filter generateFilter(Object propertyId, Field<?> originatingField) { | |||
return null; | |||
} | |||
@Override | |||
public AbstractField<?> getCustomFilterComponent(Object propertyId) { | |||
return null; | |||
} | |||
@Override | |||
public void filterRemoved(Object propertyId) { | |||
this.lqc.refresh(); | |||
} | |||
@Override | |||
public void filterAdded(Object propertyId, Class<? extends Filter> filterType, Object value) { | |||
this.lqc.refresh(); | |||
} | |||
@Override | |||
public Filter filterGeneratorFailed(Exception reason, Object propertyId, Object value) { | |||
return null; | |||
} | |||
} | |||
} | |||
.... | |||
Put them together on the UI | |||
Now we have our Container that reads from the database, and a Table for | |||
displaying them, lets put the final pieces together somewhere in some UI | |||
code: | |||
[source,java] | |||
.... | |||
final DeviceStatusTable table = new DeviceStatusTable(); | |||
table.setSizeFull(); | |||
DeviceStatusQuery.Factory factory = new DeviceStatusQuery.Factory(locationID); | |||
final LazyQueryContainer statusDataContainer = new LazyQueryContainer(factory, | |||
/*index*/ null, /*batchSize*/ 50, false); | |||
statusDataContainer.getQueryView().setMaxCacheSize(300); | |||
table.configure(statusDataContainer); | |||
layout.addComponent(table); | |||
layout.setHeight(100f, Unit.PERCENTAGE); // no scrollbar | |||
// Respond to row click | |||
table.addValueChangeListener(new Property.ValueChangeListener() { | |||
@Override | |||
public void valueChange(ValueChangeEvent event) { | |||
Object index = event.getProperty().getValue(); | |||
if (index != nulll) { | |||
int locId = (Integer) statusDataContainer.getItem(index) | |||
.getItemProperty(DeviceStatusQuery.Column.loc_id).getValue(); | |||
doSomething(locId); | |||
table.setValue(null); //visually deselect | |||
} | |||
} | |||
}); | |||
.... | |||
And finally, since we're using `SQLContainer`{empty}'s `QueryBuilder`, depending on | |||
your database you may need to include something like this once during | |||
your application startup: | |||
[source,java] | |||
.... | |||
import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; | |||
import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator; | |||
// Configure Vaadin SQLContainer to work with MySQL | |||
QueryBuilder.setStringDecorator(new StringDecorator("`","`")); | |||
.... |
@@ -0,0 +1,272 @@ | |||
[[using-phonegap-build-with-vaadin-touchkit]] | |||
Using PhoneGap Build with Vaadin TouchKit | |||
----------------------------------------- | |||
[.underline]#*_Note:_* _Vaadin Touchkit has been discontinued. A community-supported version is | |||
available https://github.com/parttio/touchkit[on GitHub]._# | |||
At first, using https://build.phonegap.com/[PhoneGap Build] to point to | |||
your Vaadin TouchKit apps seems like a breeze. Just create a simple | |||
`config.xml` and an `index.html` that redirects to your web site, and you | |||
have an app! Unfortunately, simply doing this is not robust. Mobile | |||
devices lose connectivity, and when they do your app not only stops | |||
working, it may appear to freeze up and have to be killed and restarted | |||
to get working again. | |||
With the release of TouchKit v3.0.2 though, there is a solution! This | |||
article summarizes this solution, which was worked out over months of | |||
trial and error on http://dev.vaadin.com/ticket/13250[Vaadin ticket | |||
13250]. | |||
''''' | |||
First, server side you need TouchKit v3.0.2. (The needed enhancements | |||
and fixes should roll into _v4.0_ at some point, but as of _beta1_ it isn't | |||
there.) You also need to ensure that your VAADIN directory resources are | |||
being served up by a servlet extending `TouchKitServlet`. If you have a | |||
main application extending `VaadinServlet`, this needs to be changed to | |||
`TouchKitServlet`. | |||
''''' | |||
When your PhoneGap app runs, it loads your provided `index.html` file into | |||
an embedded WebKit browser. Only this file has access to the PhoneGap | |||
Javascript library, so it handles things like offline-mode detection, | |||
and passes this via messages to the iframe containing your | |||
server-provided application. | |||
[source,html] | |||
.... | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<meta name="format-detection" content="telephone=no" /> | |||
<meta name="viewport" content="user-scalable=no,initial-scale=1.0" /> | |||
<meta name="apple-mobile-web-app-capable" content="yes" /> | |||
<meta name="apple-mobile-web-app-status-bar-style" content="black"> | |||
<title>My Application Name</title> | |||
<style type="text/css"> | |||
html, body {height:100%;margin:0;} | |||
.spinner {-webkit-animation: spin 6s infinite linear;} | |||
@-webkit-keyframes spin { | |||
0% {-webkit-transform: rotate(0deg);} | |||
100% {-webkit-transform: rotate(360deg);} | |||
} | |||
</style> | |||
</head> | |||
<body style='margin: 0px'> | |||
<script type="text/javascript" src="cordova.js"></script> | |||
<script> | |||
function failedIframe() { | |||
document.getElementById('offline').style.display = 'none'; | |||
document.getElementById('spinner').className = ''; | |||
document.getElementById('retry').style.display = 'block'; | |||
} | |||
function retryIframe() { | |||
document.getElementById('offline').style.display = 'block'; | |||
document.getElementById('spinner').className = 'spinner'; | |||
document.getElementById('retry').style.display = 'none'; | |||
setTimeout(failedIframe, 20000); | |||
document.getElementById('app').src = document.getElementById('app').src; | |||
} | |||
// Use cordova network plugin to inform the iframe about the connection | |||
document.addEventListener('deviceready', function() { | |||
if (!navigator.network || !navigator.network.connection || !Connection) { | |||
console.log(">>> ERROR, it seems cordova network connection plugin has not been loaded."); | |||
return; | |||
} | |||
var iframe = document.getElementById('app'); | |||
var loading = document.getElementById('loading'); | |||
var offline = document.getElementById('offline'); | |||
function sendMessage(msg) { | |||
iframe.contentWindow.postMessage("cordova-" + msg, "*"); | |||
} | |||
function check() { | |||
var sts = navigator.network.connection.type == Connection.NONE ? 'offline' : 'online'; | |||
sendMessage(sts); | |||
} | |||
function showIframe(ev) { | |||
if (loading.parentNode) { | |||
loading.parentNode.removeChild(loading); | |||
document.getElementById('app').style.width = iframe.style.height = "100%"; | |||
sendMessage('resume'); | |||
} | |||
navigator.splashscreen.hide(); | |||
} | |||
function showOffline() { | |||
document.getElementById('offline').style.display = 'block'; | |||
navigator.splashscreen.hide(); | |||
// if after a while we have not received any notification we show the retry link | |||
setTimeout(failedIframe, 20000); | |||
} | |||
// Listen for offline/online events | |||
document.addEventListener('offline', check, false); | |||
document.addEventListener('online', check, false); | |||
document.addEventListener('resume', function(){sendMessage('resume')}, false); | |||
document.addEventListener('pause', function(){sendMessage('pause')}, false); | |||
// check the connection periodically | |||
setInterval(check, 30000); | |||
// when vaadin app is loaded, it sends to the parent window a ready message | |||
window.addEventListener('message', showIframe, false); | |||
// If the app takes more than 3 secs to start, proly .manifest stuff is being loaded. | |||
setTimeout(showOffline, 3000); | |||
// Ignore back button in android | |||
// document.addEventListener('backbutton', function() {}, false); | |||
}, false); | |||
</script> | |||
<!-- A div to show in the meanwhile the app is loaded --> | |||
<div id='loading' style='font-size: 120%; font-weight: bold; font-family: helvetica; width: 100%; height: 100%; position: absolute; text-align: center;'> | |||
<div id='spinner' class='spinner'><img src="spinner.png"></div> | |||
<div id='offline' style='display: block; padding: 15px;'>Downloading application files,<br/>Please be patient...</div> | |||
<div id="retry" style="display: none;"> | |||
<p>Failed to contact the server.</p> | |||
<p> | |||
Please ensure you have a stable Internet connection, and then | |||
<a href="javascript:void(0)" onclick="retryIframe();">touch here</a> to retry. | |||
</p> | |||
</div> | |||
</div> | |||
<!-- Load the app in an iframe so as we can pass messages, instead of using redirect --> | |||
<iframe id='app' style='width: 0px; height: 0px; position: absolute; border: none' src='http://www.example.com/touch/'></iframe> | |||
</body> | |||
</html> | |||
.... | |||
Change the `<title>` and URL in the iframe at the end to match your app. | |||
This also expects a file named `spinner.png` along side `index.html`, which | |||
will be displayed and spin while loading application files from the | |||
server. | |||
This Javascript handles detecting when the app goes offline and back | |||
online (and passes that to TouchKit), provides user feedback during a | |||
long initial load, and provides a friendly retry mechanism if the app is | |||
initially run without network access. It also hides the initial | |||
splashscreen. | |||
''''' | |||
PhoneGap Build requires a config.xml file to tell it how to behave. | |||
Below is a working example that works to create Android 4.0+ and iOS 6 & | |||
7 apps. | |||
[source,xml] | |||
.... | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE widget> | |||
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:gap="http://phonegap.com/ns/1.0" | |||
id="com.example.myapp" version="{VERSION}" versionCode="{RELEASE}"> | |||
<name>My App Name</name> | |||
<description xml:lang="en"><![CDATA[ | |||
Describe your app. This only shows on PhoneGap - each app store has you enter descriptions on their systems. | |||
]]> | |||
</description> | |||
<author href="http://www.example.com"> | |||
Example Corp, LLC | |||
</author> | |||
<license> | |||
Copyright 2014, Example Corp, LLC | |||
</license> | |||
<gap:platform name="android"/> | |||
<gap:platform name="ios"/> | |||
<gap:plugin name="com.phonegap.plugin.statusbar" /> | |||
<gap:plugin name="org.apache.cordova.network-information" /> | |||
<gap:plugin name="org.apache.cordova.splashscreen" /> | |||
<feature name="org.apache.cordova.network-information" /> | |||
<icon src="res/ios/icon-57.png" gap:platform="ios" width="57" height="57" /> | |||
<icon src="res/ios/icon-57_at_2x.png" gap:platform="ios" width="114" height="114" /> | |||
<icon src="res/ios/icon-72.png" gap:platform="ios" width="72" height="72" /> | |||
<icon src="res/ios/icon-72_at_2x.png" gap:platform="ios" width="144" height="144" /> | |||
<icon src="res/ios/icon-76.png" gap:platform="ios" width="76" height="76" /> | |||
<icon src="res/ios/icon-76_at_2x.png" gap:platform="ios" width="152" height="152" /> | |||
<icon src="res/ios/icon-120.png" gap:platform="ios" width="120" height="120" /> | |||
<icon src="res/android/icon-36-ldpi.png" gap:platform="android" width="36" height="36" gap:density="ldpi"/> | |||
<icon src="res/android/icon-48-mdpi.png" gap:platform="android" width="48" height="48" gap:density="mdpi"/> | |||
<icon src="res/android/icon-72-hdpi.png" gap:platform="android" width="72" height="72" gap:density="hdpi"/> | |||
<icon src="res/android/icon-96-xhdpi.png" gap:platform="android" width="96" height="96" gap:density="xhdpi"/> | |||
<icon src="res/android/icon-96-xxhdpi.png" gap:platform="android" width="96" height="96" gap:density="xxhdpi"/> | |||
<gap:splash src="res/ios/Default.png" gap:platform="ios" width="320" height="480" /> | |||
<gap:splash src="res/ios/Default@2x.png" gap:platform="ios" width="640" height="960" /> | |||
<gap:splash src="res/ios/Default_iphone5.png" gap:platform="ios" width="640" height="1136"/> | |||
<gap:splash src="res/ios/Default-Landscape.png" gap:platform="ios" width="1024" height="768" /> | |||
<gap:splash src="res/ios/Default-Portrait.png" gap:platform="ios" width="768" height="1004"/> | |||
<gap:splash src="res/ios/Default-568h.png" gap:platform="ios" width="320" height="568" /> | |||
<gap:splash src="res/ios/Default-568@2x.png" gap:platform="ios" width="640" height="1136"/> | |||
<gap:splash src="res/ios/Default-Landscape@2x.png" gap:platform="ios" width="2048" height="1496"/> | |||
<gap:splash src="res/ios/Default-Portrait@2x.png" gap:platform="ios" width="1536" height="2008"/> | |||
<gap:splash src="res/android/splash-ldpi.9.png" gap:platform="android" gap:density="ldpi" /> | |||
<gap:splash src="res/android/splash-mdpi.9.png" gap:platform="android" gap:density="mdpi" /> | |||
<gap:splash src="res/android/splash-hdpi.9.png" gap:platform="android" gap:density="hdpi" /> | |||
<gap:splash src="res/android/splash-xhdpi.9.png" gap:platform="android" gap:density="xhdpi"/> | |||
<!-- PhoneGap version to use --> | |||
<preference name="phonegap-version" value="3.4.0" /> | |||
<!-- Allow landscape and portrait orientations --> | |||
<preference name="Orientation" value="default" /> | |||
<!-- Don't allow overscroll effects (bounce-back on iOS, glow on Android. | |||
Not useful since app doesn't scroll. --> | |||
<preference name="DisallowOverscroll" value="true"/> | |||
<!-- Don't hide the O/S's status bar --> | |||
<preference name="fullscreen" value="false" /> | |||
<!-- iOS: Obey the app's viewport meta tag --> | |||
<preference name="EnableViewportScale" value="true"/> | |||
<!-- iOS: if set to true, app will terminate when home button is pressed --> | |||
<preference name="exit-on-suspend" value="false" /> | |||
<!-- iOS: If icon is prerendered, iOS will not apply it's gloss to the app's icon on the user's home screen --> | |||
<preference name="prerendered-icon" value="false" /> | |||
<!-- iOS: if set to false, the splash screen must be hidden using a JavaScript API --> | |||
<preference name="AutoHideSplashScreen" value="false" /> | |||
<!-- iOS: MinimumOSVersion --> | |||
<preference name="deployment-target" value="6.0" /> | |||
<!-- Android: Keep running in the background --> | |||
<preference name="KeepRunning" value="true"/> | |||
<!-- Android: Web resource load timeout, ms --> | |||
<preference name="LoadUrlTimeoutValue" value="30000"/> | |||
<!-- Android: The amount of time the splash screen image displays (if not hidden by app) --> | |||
<preference name="SplashScreenDelay" value="3000"/> | |||
<!-- Android: Minimum (4.0) and target (4.4) API versions --> | |||
<preference name="android-minSdkVersion" value="14"/> | |||
<preference name="android-targetSdkVersion" value="19"/> | |||
</widget> | |||
.... | |||
The listed plugins are all required to make the splash screen and | |||
offline-mode work properly. The slew of icons and splash screen .png | |||
file are required by the app stores, so be sure to include all of them | |||
in the source .zip that you upload to PhoneGap Build. Placing these | |||
files in a subdirectory allows you to also put an empty file named | |||
".pgbomit" in that folder, which ensures that *extra* copies of each of | |||
these file are not included in the file app package produced by PhoneGap | |||
Build. | |||
''''' | |||
Special thanks to "manolo" from Vaadin for working with me for over a | |||
month to make all of this work by creating enhancements to TouchKit and | |||
the index.html file that the above one is based on. |
@@ -0,0 +1,437 @@ | |||
[[developing-vaadin-apps-with-python]] | |||
Developing Vaadin apps with Python | |||
---------------------------------- | |||
[[to-accomplish-exactly-what]] | |||
To accomplish exactly what? | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
This article describes how to start developing Vaadin apps with Python | |||
programming language. Goal is that programmer could use Python instead | |||
of Java with smallest amount of boilerplate code necessary to get the | |||
environment working. | |||
Luckily Python can make use of Java classes and vice versa. For detailed | |||
tutorial how to accomplish this in general please see | |||
http://www.jython.org/jythonbook/en/1.0/JythonAndJavaIntegration.html | |||
and http://wiki.python.org/jython/UserGuide. | |||
[[requirements]] | |||
Requirements | |||
^^^^^^^^^^^^ | |||
For setup used in this article you will need to install PyDev plugin to | |||
your Eclipse and Jython. See http://pydev.org/ and | |||
http://www.jython.org/ for more details. | |||
[[lets-get-started]] | |||
Let's get started | |||
^^^^^^^^^^^^^^^^^ | |||
To get started create a new Vaadin project or open existing as you would | |||
normally do. As you have PyDev installed as Eclipse plugin you can start | |||
developing after few steps. | |||
* Add Python nature to your project by right clicking the project and | |||
selecting PyDev -> Set as PyDev Project. After this the project | |||
properties has PyDev specific sections. | |||
* Go to PyDev - Interpreter/Grammar and select Jython as your Python | |||
interpreter. | |||
* Add a source folder where your Python source code will reside. Go to | |||
section PyDev - PYTHONPATH and add source folder. Also add | |||
vaadin-x.x.x.jar to PYTHONPATH in external libraries tab. | |||
* Add jython.jar to your project's classpath and into deployment | |||
artifact. | |||
* Map your python source folder into WEB-INF/classes in deployment | |||
artifact. Go to Deployment Assembly -> Add -> Folder. | |||
image:img/deployartifact.png[Deploy artifact] | |||
[[modify-web.xml-and-applicationservlet]] | |||
Modify web.xml and ApplicationServlet | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
First of all to build a basic Vaadin app you need to define your app in | |||
web.xml. You have something like this in your web.xml: | |||
[source,xml] | |||
.... | |||
<servlet> | |||
<servlet-name>Vaadin Application</servlet-name> | |||
<servlet-class>com.vaadin.terminal.gwt.server.ApplicationServlet</servlet-class> | |||
<init-param> | |||
<description>Vaadin application class to start</description> | |||
<param-name>application</param-name> | |||
<param-value>com.vaadin.example.ExampleApplication</param-value> | |||
</init-param> | |||
</servlet> | |||
.... | |||
This will have to be modified a bit. Servlet init parameter application | |||
is a Java class name which will be instantiated for each user session. | |||
Default implementation of | |||
`com.vaadin.terminal.gwt.server.ApplicationServlet` can only instantiate | |||
Java classes so therefore you must override that class so that it is | |||
able to instantiate Python objects. Of course if you want the main | |||
Application object to be a Java class there is no need to modify the | |||
web.xml. | |||
Here's the modified section of web.xml. Implementation of PythonServlet | |||
is explained later. Init parameter application is now actually Python | |||
class. | |||
[source,xml] | |||
.... | |||
<servlet> | |||
<servlet-name>Python Application</servlet-name> | |||
<servlet-class>com.vaadin.example.pythonapp.PythonServlet</servlet-class> | |||
<init-param> | |||
<description>Vaadin application class to start</description> | |||
<param-name>application</param-name> | |||
<param-value>python.vaadin.pythonapp.PyApplication</param-value> | |||
</init-param> | |||
</servlet> | |||
.... | |||
And here's the PythonServlet. This is altered version of original Vaadin | |||
ApplicationServlet. | |||
[source,java] | |||
.... | |||
package com.vaadin.example.pythonapp; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
import org.python.core.PyObject; | |||
import org.python.util.PythonInterpreter; | |||
import com.vaadin.Application; | |||
import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; | |||
public class PythonServlet extends AbstractApplicationServlet { | |||
// Private fields | |||
private Class<? extends Application> applicationClass; | |||
/** | |||
* Called by the servlet container to indicate to a servlet that the servlet | |||
* is being placed into service. | |||
* | |||
* @param servletConfig | |||
* the object containing the servlet's configuration and | |||
* initialization parameters | |||
* @throws javax.servlet.ServletException | |||
* if an exception has occurred that interferes with the | |||
* servlet's normal operation. | |||
*/ | |||
@Override | |||
public void init(javax.servlet.ServletConfig servletConfig) | |||
throws javax.servlet.ServletException { | |||
super.init(servletConfig); | |||
final String applicationModuleName = servletConfig | |||
.getInitParameter("application"); | |||
if (applicationModuleName == null) { | |||
throw new ServletException( | |||
"Application not specified in servlet parameters"); | |||
} | |||
String[] appModuleSplitted = applicationModuleName.split("\\."); | |||
if(appModuleSplitted.length < 1) { | |||
throw new ServletException("Cannot parse class name"); | |||
} | |||
final String applicationClassName = appModuleSplitted[appModuleSplitted.length-1]; | |||
try { | |||
PythonInterpreter interpreter = new PythonInterpreter(); | |||
interpreter.exec("from "+applicationModuleName+" import "+applicationClassName); | |||
PyObject pyObj = interpreter.get(applicationClassName).__call__(); | |||
Application pyApp = (Application)pyObj.__tojava__(Application.class); | |||
applicationClass = pyApp.getClass(); | |||
} catch (Exception e) { | |||
e.printStackTrace(); | |||
throw new ServletException("Failed to load application class: " | |||
+ applicationModuleName, e); | |||
} | |||
} | |||
@Override | |||
protected Application getNewApplication(HttpServletRequest request) | |||
throws ServletException { | |||
// Creates a new application instance | |||
try { | |||
final Application application = getApplicationClass().newInstance(); | |||
return application; | |||
} catch (final IllegalAccessException e) { | |||
throw new ServletException("getNewApplication failed", e); | |||
} catch (final InstantiationException e) { | |||
throw new ServletException("getNewApplication failed", e); | |||
} catch (ClassNotFoundException e) { | |||
throw new ServletException("getNewApplication failed", e); | |||
} | |||
} | |||
@Override | |||
protected Class<? extends Application> getApplicationClass() | |||
throws ClassNotFoundException { | |||
return applicationClass; | |||
} | |||
} | |||
.... | |||
The most important part is the following. It uses Jython's | |||
PythonInterpreter to instantiate and convert Python classes into Java | |||
classes. Then Class object is stored for later use of creating new | |||
instances of it on demand. | |||
[source,java] | |||
.... | |||
PythonInterpreter interpreter = new PythonInterpreter(); | |||
interpreter.exec("from "+applicationModuleName+" import "+applicationClassName); | |||
PyObject pyObj = interpreter.get(applicationClassName).__call__(); | |||
Application pyApp = (Application)pyObj.__tojava__(Application.class); | |||
.... | |||
Now the Python application for Vaadin is good to go. No more effort is | |||
needed to get it running. So next we see how the application itself can | |||
be written in Python. | |||
[[python-style-application-object]] | |||
Python style Application object | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Creating an Application is pretty straightforward. You would write class | |||
that is identical to the Java counterpart except it's syntax is Python. | |||
Basic hello world application would look like this | |||
[source,python] | |||
.... | |||
from com.vaadin import Application | |||
from com.vaadin.ui import Label | |||
from com.vaadin.ui import Window | |||
class PyApplication(Application): | |||
def __init__(self): | |||
pass | |||
def init(self): | |||
mainWindow = Window("Vaadin with Python") | |||
label = Label("Vaadin with Python") | |||
mainWindow.addComponent(label) | |||
self.setMainWindow(mainWindow) | |||
.... | |||
[[event-listeners]] | |||
Event listeners | |||
^^^^^^^^^^^^^^^ | |||
Python does not have anonymous classes like Java and Vaadin's event | |||
listeners rely heavily on implementing listener interfaces which are | |||
very often done as anonymous classes. So therefore the closest | |||
equivalent of | |||
[source,java] | |||
.... | |||
Button button = new Button("java button"); | |||
button.addListener(new Button.ClickListener() { | |||
public void buttonClick(ClickEvent event) { | |||
//Do something for the click | |||
} | |||
}); | |||
.... | |||
is | |||
[source,python] | |||
.... | |||
button = Button("python button") | |||
class listener(Button.ClickListener): | |||
def buttonClick(self, event): | |||
#do something for the click | |||
button.addListener(listener()) | |||
.... | |||
Jython supports for some extend AWT/Swing-style event listeners but | |||
however that mechanism is not compatible with Vaadin. Same problem | |||
applies to just about anything else event listening interface in Java | |||
libraries like Runnable or Callable. To reduce the resulted verbosity | |||
some decorator code can be introduced like here | |||
https://gist.github.com/sunng87/947926. | |||
[[creating-custom-components]] | |||
Creating custom components | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Creating custom Vaadin components is pretty much as straightforward as | |||
the creation of Vaadin main application. Override the CustomComponent | |||
class in similar manner as would be done with Java. | |||
[source,python] | |||
.... | |||
from com.vaadin.ui import CustomComponent | |||
from com.vaadin.ui import VerticalLayout | |||
from com.vaadin.ui import Label | |||
from com.vaadin.ui import Button | |||
from com.vaadin.terminal import ThemeResource | |||
class PyComponent(CustomComponent, Button.ClickListener): | |||
def __init__(self): | |||
mainLayout = VerticalLayout() | |||
button = Button("click me to toggle the icon") | |||
self.label = Label() | |||
button.addListener(self) | |||
mainLayout.addComponent(self.label) | |||
mainLayout.addComponent(button) | |||
self.super__setCompositionRoot(mainLayout) | |||
def buttonClick(self, event): | |||
if self.label.getIcon() == None: | |||
self.label.setIcon(ThemeResource("../runo/icons/16/lock.png")); | |||
else: | |||
self.label.setIcon(None) | |||
.... | |||
[[containers-and-pythonbeans]] | |||
Containers and PythonBeans | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
Although not Python style of doing things there are some occasions that | |||
require use of beans. | |||
Let's say that you would like to have a table which has it's content | |||
retrieved from a set of beans. Content would be one row with two columns | |||
where cells would contain strings "first" and "second" respectively. You | |||
would write this code to create and fill the table. | |||
[source,python] | |||
.... | |||
table = Table() | |||
container = BeanItemContainer(Bean().getClass()) | |||
bean = Bean() | |||
bean.setFirst("first") | |||
bean.setSecond("second") | |||
container.addItem(bean) | |||
table.setContainerDataSource(container) | |||
.... | |||
and the Bean object would look like this | |||
[source,python] | |||
.... | |||
class Bean(JavaBean): | |||
def __init__(self): | |||
self.__first = None | |||
self.__second = None | |||
def getFirst(self): | |||
return self.__first | |||
def getSecond(self): | |||
return self.__second | |||
def setFirst(self, val): | |||
self.__first = val | |||
def setSecond(self, val): | |||
self.__second = val | |||
.... | |||
and JavaBean | |||
[source,java] | |||
.... | |||
public interface JavaBean { | |||
String getFirst(); | |||
void setFirst(String first); | |||
String getSecond(); | |||
void setSecond(String second); | |||
} | |||
.... | |||
Note that in this example there is Java interface mixed into Python | |||
code. That is because Jython in it's current (2.5.2) version does not | |||
fully implement reflection API for python objects. Result without would | |||
be a table that has no columns. | |||
Implementing a Java interface adds necessary piece of information of | |||
accessor methods so that bean item container can handle it. | |||
[[filtering-container]] | |||
Filtering container | |||
^^^^^^^^^^^^^^^^^^^ | |||
Let's add filtering to previous example. Implement custom filter that | |||
allows only bean that 'first' property is set to 'first' | |||
[source,python] | |||
.... | |||
container.addContainerFilter(PyFilter()) | |||
class PyFilter(Container.Filter): | |||
def appliesToProperty(self, propertyId): | |||
return True | |||
def passesFilter(self, itemId, item): | |||
prop = item.getItemProperty("first") | |||
if prop.getValue() == "first": | |||
return True | |||
else: | |||
return False | |||
.... | |||
Again pretty straightforward. | |||
[[debugging]] | |||
Debugging | |||
^^^^^^^^^ | |||
Debugging works as you would debug any Jython app remotely in a servlet | |||
engine. See PyDev's manual for remote debugging at | |||
http://pydev.org/manual_adv_remote_debugger.html. | |||
Setting breakpoints directly via Eclipse IDE however does not work. | |||
Application is started as a Java application and the debugger therefore | |||
does not understand Python code. | |||
[[final-thoughts]] | |||
Final thoughts | |||
^^^^^^^^^^^^^^ | |||
By using Jython it allows easy access from Python code to Java code | |||
which makes it really straightforward to develop Vaadin apps with | |||
Python. | |||
Some corners are bit rough as they require mixing Java code or are not | |||
possible to implement with Python as easily or efficiently than with | |||
Java. | |||
[[how-this-differs-from-muntjac]] | |||
How this differs from Muntjac? | |||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
https://pypi.python.org/pypi/Muntjac[Muntjac project] | |||
is a python translation of Vaadin and it's goal is pretty much same as | |||
this article's: To enable development of Vaadin apps with Python. | |||
Muntjac's approach was to take Vaadin's Java source code and translate | |||
it to Python while keeping the API intact or at least similar as | |||
possible. While in this article the Vaadin itself is left as is. | |||
Simple Python applications like shown above can be executed with Vaadin | |||
or Muntjac. Application code should be compatible with both with small | |||
package/namespace differences. | |||
Muntjac requires no Jython but it also lacks the possibility to use Java | |||
classes directly. | |||
The problems we encountered above with requiring the use of mixed Java | |||
code are currently present in Muntjac (v1.0.4) as well. For example the | |||
BeanItemContainer is missing from the Muntjac at the moment. |
@@ -0,0 +1,129 @@ | |||
[[using-vaadin-in-an-existing-gwt-project]] | |||
Using Vaadin in an existing GWT project | |||
--------------------------------------- | |||
[[using-vaadin-jar-with-google-eclipse-plugin-in-a-gwt-project]] | |||
Using Vaadin JAR with Google Eclipse plugin in a GWT project | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
With GWT development and run-time classes now included in Vaadin, it is | |||
easy to move from Google's build of GWT to Vaadin. | |||
By switching to the GWT integrated in Vaadin 7, you immediately get | |||
easier integration of SuperDevMode in your application. Many future GWT | |||
bugfixes will be available in Vaadin before they get integrated to the | |||
official version and more and more Vaadin widgets ready to use in your | |||
application. You risk nothing and can easily switch back to stand-alone | |||
GWT if you don't use features from `com.vaadin` packages. | |||
You also have the option to easily move to a hybrid application | |||
development model integrating business logic on the server with custom | |||
components and other parts of your UI implemented using GWT. You can | |||
easily combine the productivity and security benefits of a server side | |||
framework with the flexibility of client side development where needed. | |||
[[using-google-eclipse-plugin]] | |||
Using Google Eclipse Plugin | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
Google Plugin for Eclipse assumes the use of GWT SDK. Nevertheless, the | |||
plugin can easily be used to develop client side applications with | |||
Vaadin, by following the steps described below. | |||
For lighter deployment, a minimal run-time version of Vaadin JAR will be | |||
available in the future. | |||
1. You need to have the IvyDE plugin for Eclipse installed | |||
2. Disable some error messages by setting *Preferences... → Google → | |||
Errors/Warnings → Missing SDK → Ignore*. Note that you may still get an | |||
error message about missing `gwt-servlet.jar` when modifying project | |||
build path. | |||
3. If you don't already have a client side application project, you can | |||
create one with "New Web Application Project...", selecting any recent | |||
version of the GWT SDK. If you don't have any version of GWT installed, | |||
download one | |||
https://code.google.com/p/google-web-toolkit/downloads/list[here] - the | |||
next steps will switch to using Vaadin JAR. | |||
4. Open project properties, select *Java Build Path → Libraries* and | |||
remove the GWT SDK from the project class path | |||
5. In the project properties, make sure the project JRE version in | |||
*Project Facets* is 1.6 or later | |||
6. Copy the `ivy.xml` and `ivy-settings.xml` from an existing Vaadin | |||
project created with the Vaadin Plugin for Eclipse | |||
7. Set the Vaadin version in `ivy.xml` to your preferred version | |||
8. Add the following dependency in the `ivy.xml`: | |||
`<dependency org="javax.servlet" name="jsp-api" rev="2.0" />` | |||
9. Right-click the `ivy.xml` and select *Add Ivy library...* and click | |||
*Finish* | |||
10. Right-click project, select *Ivy → Resolve* | |||
That's it - you are now ready to debug the application using GWT | |||
development mode server: | |||
* *Debug as... → Web Application* | |||
To avoid the need to install and update browser plug-ins, use SuperDevMode. | |||
[[using-maven]] | |||
Using Maven | |||
~~~~~~~~~~~ | |||
Also the Maven plug-in for GWT makes some assumptions but it is easy to | |||
switch to the combined Vaadin JAR. | |||
As the Vaadin JAR now includes GWT, Maven projects should not depend | |||
directly on GWT JARs (gwt-user, gwt-dev, gwt-servlet). | |||
To convert an existing Maven project, perform the following | |||
modifications in your pom.xml | |||
* update compiler source and target Java version to 1.6 | |||
* remove dependencies to GWT (`com.google.gwt:gwt-user`, | |||
`com.google.gwt:gwt-servlet`, `com.google.gwt:gwt-dev`) | |||
* add dependencies to | |||
Vaadin | |||
[source,xml] | |||
.... | |||
<!-- this replaces gwt-user.jar --> | |||
<dependency> | |||
<groupId>com.vaadin</groupId> | |||
<artifactId>vaadin-client</artifactId> | |||
<version>7.0.0.beta9</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<!-- this replaces gwt-dev.jar --> | |||
<dependency> | |||
<groupId>com.vaadin</groupId> | |||
<artifactId>vaadin-client-compiler</artifactId> | |||
<version>7.0.0.beta9</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<!-- optional - this replaces gwt-servlet.jar etc. and is deployed on the server --> | |||
<dependency> | |||
<groupId>com.vaadin</groupId> | |||
<artifactId>vaadin-server</artifactId> | |||
<version>7.0.0.beta9</version> | |||
</dependency> | |||
.... | |||
* if not included e.g. via Jetty/Tomcat/other, add a "provided" | |||
dependency to the servlet | |||
API | |||
[source,xml] | |||
.... | |||
<dependency> | |||
<groupId>javax.servlet</groupId> | |||
<artifactId>servlet-api</artifactId> | |||
<version>2.5</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
.... | |||
* replace the `gwt-maven-plugin` with `com.vaadin:vaadin-maven-plugin`, | |||
comment out `<dependencies>` in its configuration (if exists) and use | |||
plug-in version that matches the Vaadin version | |||
* use goal `vaadin:compile` instead of `gwt:compile` etc. | |||
The vaadin-client, vaadin-client-compiler and their dependencies only | |||
need to be deployed on the server for debugging with | |||
SuperDevMode. |
@@ -0,0 +1,396 @@ | |||
[[v-access-control]] | |||
V - Access control | |||
------------------ | |||
In this tutorial we will look into access control. | |||
[[basic-access-control]] | |||
Basic access control | |||
~~~~~~~~~~~~~~~~~~~~ | |||
The application we've been building will inevitably need some | |||
administrative tools. Creation and deletion of users, for example, is | |||
generally something that we'd like to do during runtime. Let's create a | |||
simple View for creating a new user: | |||
[source,java] | |||
.... | |||
package com.vaadin.cdi.tutorial; | |||
import java.util.concurrent.atomic.AtomicLong; | |||
import javax.inject.Inject; | |||
import com.vaadin.cdi.CDIView; | |||
import com.vaadin.data.Validator; | |||
import com.vaadin.data.fieldgroup.BeanFieldGroup; | |||
import com.vaadin.data.fieldgroup.FieldGroup.CommitEvent; | |||
import com.vaadin.data.fieldgroup.FieldGroup.CommitException; | |||
import com.vaadin.data.fieldgroup.FieldGroup.CommitHandler; | |||
import com.vaadin.navigator.View; | |||
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; | |||
import com.vaadin.ui.Button; | |||
import com.vaadin.ui.Button.ClickEvent; | |||
import com.vaadin.ui.Button.ClickListener; | |||
import com.vaadin.ui.CustomComponent; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.VerticalLayout; | |||
@CDIView | |||
public class CreateUserView extends CustomComponent implements View { | |||
@Inject | |||
UserDAO userDAO; | |||
private static final AtomicLong ID_FACTORY = new AtomicLong(3); | |||
@Override | |||
public void enter(ViewChangeEvent event) { | |||
final VerticalLayout layout = new VerticalLayout(); | |||
layout.setMargin(true); | |||
layout.setSpacing(true); | |||
layout.addComponent(new Label("Create new user")); | |||
final BeanFieldGroup<User> fieldGroup = new BeanFieldGroup<User>( | |||
User.class); | |||
layout.addComponent(fieldGroup.buildAndBind("firstName")); | |||
layout.addComponent(fieldGroup.buildAndBind("lastName")); | |||
layout.addComponent(fieldGroup.buildAndBind("username")); | |||
layout.addComponent(fieldGroup.buildAndBind("password")); | |||
layout.addComponent(fieldGroup.buildAndBind("email")); | |||
fieldGroup.getField("username").addValidator(new Validator() { | |||
@Override | |||
public void validate(Object value) throws InvalidValueException { | |||
String username = (String) value; | |||
if (username.isEmpty()) { | |||
throw new InvalidValueException("Username cannot be empty"); | |||
} | |||
if (userDAO.getUserBy(username) != null) { | |||
throw new InvalidValueException("Username is taken"); | |||
} | |||
} | |||
}); | |||
fieldGroup.setItemDataSource(new User(ID_FACTORY.incrementAndGet(), "", | |||
"", "", "", "", false)); | |||
final Label messageLabel = new Label(); | |||
layout.addComponent(messageLabel); | |||
fieldGroup.addCommitHandler(new CommitHandler() { | |||
@Override | |||
public void preCommit(CommitEvent commitEvent) throws CommitException { | |||
} | |||
@Override | |||
public void postCommit(CommitEvent commitEvent) throws CommitException { | |||
userDAO.saveUser(fieldGroup.getItemDataSource().getBean()); | |||
fieldGroup.setItemDataSource(new User(ID_FACTORY | |||
.incrementAndGet(), "", "", "", "", "", false)); | |||
} | |||
}); | |||
Button commitButton = new Button("Create"); | |||
commitButton.addClickListener(new ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
try { | |||
fieldGroup.commit(); | |||
messageLabel.setValue("User created"); | |||
} catch (CommitException e) { | |||
messageLabel.setValue(e.getMessage()); | |||
} | |||
} | |||
}); | |||
layout.addComponent(commitButton); | |||
setCompositionRoot(layout); | |||
} | |||
} | |||
.... | |||
`CDIViewProvider` checks the Views for a specific annotation, | |||
`javax.annotation.security.RolesAllowed`. You can get access to it by | |||
adding the following dependency to your pom.xml: | |||
[source,xml] | |||
.... | |||
<dependency> | |||
<groupId>javax.annotation</groupId> | |||
<artifactId>javax.annotation-api</artifactId> | |||
<version>1.2-b01</version> | |||
</dependency> | |||
.... | |||
[source,java] | |||
.... | |||
@CDIView | |||
@RolesAllowed({ "admin" }) | |||
public class CreateUserView extends CustomComponent implements View { | |||
.... | |||
To add access control to our application we'll need to have a concrete | |||
implementation of the AccessControl abstract class. Vaadin CDI comes | |||
bundled with a simple JAAS implementation, but configuring a JAAS | |||
security domain is outside the scope of this tutorial. Instead we'll opt | |||
for a simpler implementation. | |||
We'll go ahead and alter our UserInfo class to include hold roles. | |||
[source,java] | |||
.... | |||
private List<String> roles = new LinkedList<String>(); | |||
public void setUser(User user) { | |||
this.user = user; | |||
roles.clear(); | |||
if (user != null) { | |||
roles.add("user"); | |||
if (user.isAdmin()) { | |||
roles.add("admin"); | |||
} | |||
} | |||
} | |||
public List<String> getRoles() { | |||
return roles; | |||
} | |||
.... | |||
Let's extend `AccessControl` and use our freshly modified `UserInfo` in it. | |||
[source,java] | |||
.... | |||
package com.vaadin.cdi.tutorial; | |||
import javax.enterprise.inject.Alternative; | |||
import javax.inject.Inject; | |||
import com.vaadin.cdi.access.AccessControl; | |||
@Alternative | |||
public class CustomAccessControl extends AccessControl { | |||
@Inject | |||
private UserInfo userInfo; | |||
@Override | |||
public boolean isUserSignedIn() { | |||
return userInfo.getUser() != null; | |||
} | |||
@Override | |||
public boolean isUserInRole(String role) { | |||
if (isUserSignedIn()) { | |||
for (String userRole : userInfo.getRoles()) { | |||
if (role.equals(userRole)) { | |||
return true; | |||
} | |||
} | |||
} | |||
return false; | |||
} | |||
@Override | |||
public String getPrincipalName() { | |||
if (isUserSignedIn()) { | |||
return userInfo.getUser().getUsername(); | |||
} | |||
return null; | |||
} | |||
} | |||
.... | |||
Note the `@Alternative` annotation. The JAAS implementation is set as the | |||
default, and we can't have multiple default implementations. We'll have | |||
to add our custom implementation to the beans.xml: | |||
[source,xml] | |||
.... | |||
<beans> | |||
<alternatives> | |||
<class>com.vaadin.cdi.tutorial.UserGreetingImpl</class> | |||
<class>com.vaadin.cdi.tutorial.CustomAccessControl</class> | |||
</alternatives> | |||
<decorators> | |||
<class>com.vaadin.cdi.tutorial.NavigationLogDecorator</class> | |||
</decorators> | |||
</beans> | |||
.... | |||
Now let's add a button to navigate to this view. | |||
ChatView: | |||
[source,java] | |||
.... | |||
private Layout buildUserSelectionLayout() { | |||
VerticalLayout layout = new VerticalLayout(); | |||
layout.setWidth("100%"); | |||
layout.setMargin(true); | |||
layout.setSpacing(true); | |||
layout.addComponent(new Label("Select user to talk to:")); | |||
for (User user : userDAO.getUsers()) { | |||
if (user.equals(userInfo.getUser())) { | |||
continue; | |||
} | |||
layout.addComponent(generateUserSelectionButton(user)); | |||
} | |||
layout.addComponent(new Label("Admin:")); | |||
Button createUserButton = new Button("Create user"); | |||
createUserButton.addClickListener(new ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
navigationEvent.fire(new NavigationEvent("create-user")); | |||
} | |||
}); | |||
layout.addComponent(createUserButton); | |||
return layout; | |||
} | |||
.... | |||
Everything seems to work fine, the admin is able to use this new feature | |||
to create a new user and the view is inaccessible to non-admins. An | |||
attempt to access the view without the proper authorization will | |||
currently cause an `IllegalArgumentException`. A better approach would be | |||
to create an error view and display that instead. | |||
[source,java] | |||
.... | |||
package com.vaadin.cdi.tutorial; | |||
import javax.inject.Inject; | |||
import com.vaadin.cdi.access.AccessControl; | |||
import com.vaadin.navigator.View; | |||
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; | |||
import com.vaadin.ui.Button; | |||
import com.vaadin.ui.Button.ClickEvent; | |||
import com.vaadin.ui.Button.ClickListener; | |||
import com.vaadin.ui.CustomComponent; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.VerticalLayout; | |||
public class ErrorView extends CustomComponent implements View { | |||
@Inject | |||
private AccessControl accessControl; | |||
@Inject | |||
private javax.enterprise.event.Event<NavigationEvent> navigationEvent; | |||
@Override | |||
public void enter(ViewChangeEvent event) { | |||
VerticalLayout layout = new VerticalLayout(); | |||
layout.setSizeFull(); | |||
layout.setMargin(true); | |||
layout.setSpacing(true); | |||
layout.addComponent(new Label( | |||
"Unfortunately, the page you've requested does not exists.")); | |||
if (accessControl.isUserSignedIn()) { | |||
layout.addComponent(createChatButton()); | |||
} else { | |||
layout.addComponent(createLoginButton()); | |||
} | |||
setCompositionRoot(layout); | |||
} | |||
private Button createLoginButton() { | |||
Button button = new Button("To login page"); | |||
button.addClickListener(new ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
navigationEvent.fire(new NavigationEvent("login")); | |||
} | |||
}); | |||
return button; | |||
} | |||
private Button createChatButton() { | |||
Button button = new Button("Back to the main page"); | |||
button.addClickListener(new ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
navigationEvent.fire(new NavigationEvent("chat")); | |||
} | |||
}); | |||
return button; | |||
} | |||
} | |||
.... | |||
To use this we'll modify our `NavigationService` to add the error view to | |||
the `Navigator`. | |||
NavigationServiceImpl: | |||
[source,java] | |||
.... | |||
@Inject | |||
private ErrorView errorView; | |||
@PostConstruct | |||
public void initialize() { | |||
if (ui.getNavigator() == null) { | |||
Navigator navigator = new Navigator(ui, ui); | |||
navigator.addProvider(viewProvider); | |||
navigator.setErrorView(errorView); | |||
} | |||
} | |||
.... | |||
We don't really want the admin-only buttons to be visible to non-admin | |||
users. To programmatically hide them we can inject `AccessControl` to our | |||
view. | |||
ChatView: | |||
[source,java] | |||
.... | |||
@Inject | |||
private AccessControl accessControl; | |||
private Layout buildUserSelectionLayout() { | |||
VerticalLayout layout = new VerticalLayout(); | |||
layout.setWidth("100%"); | |||
layout.setMargin(true); | |||
layout.setSpacing(true); | |||
layout.addComponent(new Label("Select user to talk to:")); | |||
for (User user : userDAO.getUsers()) { | |||
if (user.equals(userInfo.getUser())) { | |||
continue; | |||
} | |||
layout.addComponent(generateUserSelectionButton(user)); | |||
} | |||
if(accessControl.isUserInRole("admin")) { | |||
layout.addComponent(new Label("Admin:")); | |||
Button createUserButton = new Button("Create user"); | |||
createUserButton.addClickListener(new ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
navigationEvent.fire(new NavigationEvent("create-user")); | |||
} | |||
}); | |||
layout.addComponent(createUserButton); | |||
} | |||
return layout; | |||
} | |||
.... | |||
[[some-further-topics]] | |||
Some further topics | |||
~~~~~~~~~~~~~~~~~~~ | |||
In the previous section we pruned the layout programmatically to prevent | |||
non-admins from even seeing the admin buttons. That was one way to do | |||
it. Another would be to create a custom component representing the | |||
layout, then create a producer for that component which would determine | |||
at runtime which version to create. | |||
Sometimes there's a need for a more complex custom access control | |||
implementations. You may need to use something more than Java Strings to | |||
indicate user roles, you may want to alter access rights during runtime. | |||
For those purposes we could extend the `CDIViewProvider` (with either the | |||
`@Specializes` annotation or `@Alternative` with a beans.xml entry) and | |||
override `isUserHavingAccessToView(Bean<?> viewBean)`. |
@@ -0,0 +1,161 @@ | |||
[[vaadin-7-hierarchical-container-and-treecomponent-example-with-liferay-organizationservice]] | |||
Vaadin 7 hierarchical container and TreeComponent example with Liferay OrganizationService | |||
------------------------------------------------------------------------------------------ | |||
I recently needed a portlet to display the Organizations/Locations a | |||
user belongs to in a Hierarchical Tree. I used Vaadin's tree and | |||
hierarchical container components along with information from Vaadin's | |||
book of examples to create the code below (http://demo.vaadin.com/book-examples-vaadin7/book#component.tree.itemstylegenerator). | |||
See link:img/DmoOrgTreeUI.java[DmoOrgTreeUI.java] for full source code. | |||
[source,java] | |||
.... | |||
private void buildMainLayout() throws SystemException, PortalException { | |||
if (viewContent.getComponentCount() > 0) { | |||
viewContent.removeAllComponents(); | |||
} | |||
viewContent.setMargin(true); | |||
viewContent.addStyleName("view"); | |||
List orgList = new ArrayList(); | |||
orgList = OrganizationLocalServiceUtil.getUserOrganizations(user.getUserId()); | |||
final HierarchicalContainer container = createTreeContent(orgList); | |||
tree = new Tree("My Organizations", container); | |||
tree.addStyleName("checkboxed"); | |||
tree.setSelectable(false); | |||
tree.setItemCaptionMode(ItemCaptionMode.PROPERTY); | |||
tree.setItemCaptionPropertyId("name"); | |||
tree.addItemClickListener(new ItemClickEvent.ItemClickListener() { | |||
public void itemClick(ItemClickEvent event) { | |||
if (event.getItemId().getClass() == Long.class) { | |||
long itemId = (Long) event.getItemId(); | |||
if (checked.contains(itemId)) { | |||
checkboxChildren(container, itemId, false); | |||
} | |||
else { | |||
checkboxChildren(container, itemId, true); | |||
tree.expandItemsRecursively(itemId); | |||
} | |||
} | |||
tree.markAsDirty(); | |||
} | |||
}); | |||
Tree.ItemStyleGenerator itemStyleGenerator = new Tree.ItemStyleGenerator() { | |||
@Override | |||
public String getStyle(Tree source, Object itemId) { | |||
if (checked.contains(itemId)) | |||
return "checked"; | |||
else | |||
return "unchecked"; | |||
} | |||
}; | |||
tree.setItemStyleGenerator(itemStyleGenerator); | |||
viewContent.addComponent(tree); | |||
viewContent.setVisible(true); | |||
setContent(viewContent); | |||
} | |||
public void checkboxChildren(HierarchicalContainer hc, long itemId, boolean bAdd) { | |||
try { | |||
if (bAdd) { | |||
checked.add(itemId); | |||
} | |||
else { | |||
checked.remove(itemId); | |||
Object iParendId = hc.getParent(itemId); | |||
while (iParendId != null) { | |||
checked.remove(iParendId); | |||
iParendId = hc.getParent(iParendId); | |||
} | |||
} | |||
if (hc.hasChildren(itemId)) { | |||
Collection children = hc.getChildren(itemId); | |||
for (Object o : children) { | |||
if (o.getClass() == Long.class) { | |||
itemId = (Long) o; | |||
checkboxChildren(hc, itemId, bAdd); | |||
} | |||
} | |||
} | |||
} | |||
catch (Exception e) { | |||
Notification.show("Unable to build Organization tree. Contact Administrator.", Type.ERROR_MESSAGE); | |||
} | |||
} | |||
public static HierarchicalContainer createTreeContent(List oTrees) | |||
throws SystemException, PortalException { | |||
HierarchicalContainer container = new HierarchicalContainer(); | |||
container.addContainerProperty("name", String.class, ""); | |||
new Object() { | |||
@SuppressWarnings("unchecked") | |||
public void put(List data, HierarchicalContainer container) | |||
throws SystemException, PortalException { | |||
for (Organization o : data) { | |||
long orgId = o.getOrganizationId(); | |||
if (!container.containsId(orgId)) { | |||
container.addItem(orgId); | |||
container.getItem(orgId).getItemProperty("name").setValue(o.getName()); | |||
if (!o.hasSuborganizations()) { | |||
container.setChildrenAllowed(orgId, false); | |||
} | |||
else { | |||
container.setChildrenAllowed(orgId, true); | |||
} | |||
if (o.isRoot()) { | |||
container.setParent(orgId, null); | |||
} | |||
else { | |||
if (!container.containsId(o.getParentOrganizationId())) { | |||
List sub = new ArrayList(); | |||
sub.add(o.getParentOrganization()); | |||
put(sub, container); | |||
} | |||
container.setParent(orgId, (Object) o.getParentOrganizationId()); | |||
} | |||
} | |||
} | |||
} | |||
}.put(oTrees, container); | |||
return container; | |||
} | |||
.... | |||
Below is the css used | |||
[source,scss] | |||
.... | |||
.v-tree-node-caption-disabled { | |||
color: black; | |||
font-style: italic; | |||
//border-style:solid; | |||
//border-width:1px; | |||
} | |||
.v-tree-checkboxed .v-tree-node-caption-unchecked div span { | |||
background: url("images/unchecked.png") no-repeat; | |||
padding-left: 24px; | |||
//border-style:solid; | |||
//border-width:1px; | |||
} | |||
.v-tree-checkboxed .v-tree-node-caption-checked div span { | |||
background: url("images/checked.png") no-repeat; | |||
padding-left: 24px; | |||
//border-style:solid; | |||
//border-width:1px; | |||
} | |||
.... |
@@ -0,0 +1,38 @@ | |||
= Community articles for Vaadin 7 | |||
[discrete] | |||
== Articles | |||
- link:LazyQueryContainer.asciidoc[Lazy query container] | |||
- link:UsingJDBCwithLazyQueryContainerAndFilteringTable.asciidoc[Using JDBC with Lazy Query Container and FilteringTable] | |||
- link:OfflineModeForTouchKit4MobileApps.asciidoc[Offline mode for TouchKit 4 mobile apps] | |||
- link:CreatingYourOwnConverterForString.asciidoc[Creating your own converter for String] | |||
- link:ChangingTheDefaultConvertersForAnApplication.asciidoc[Changing the default converters for an application] | |||
- link:CreatingAnApplicationWithDifferentFeaturesForDifferentClients.asciidoc[Creating an application with different features for different clients] | |||
- link:VAccessControl.asciidoc[V - Access control] | |||
- link:FindingTheCurrentRootAndApplication.asciidoc[Finding the current root and application] | |||
- link:CreatingABasicApplication.asciidoc[Creating a basic application] | |||
- link:JasperReportsOnVaadinSample.asciidoc[Jasper reports on Vaadin sample] | |||
- link:BuildingVaadinApplicationsOnTopOfActiviti.asciidoc[Building Vaadin applications on top of Activiti] | |||
- link:UsingVaadinInAnExistingGWTProject.asciidoc[Using Vaadin in an existing GWT project] | |||
- link:UsingPython.asciidoc[Using Python] | |||
- link:UsingPhoneGapBuildWithVaadinTouchKit.asciidoc[Using PhoneGap Build with Vaadin TouchKit] | |||
- link:ScalaAndVaadinHOWTO.asciidoc[Scala and Vaadin how-to] | |||
- link:UsingHibernateWithVaadin.asciidoc[Using Hibernate with Vaadin] | |||
- link:AddingJPAToTheAddressBookDemo.asciidoc[Adding JPA to the address book demo] | |||
- link:SimplifiedRPCusingJavaScript.asciidoc[Simplified RPC using JavaScript] | |||
- link:JMeterTesting.asciidoc[JMeter testing] | |||
- link:AutoGeneratingAFormBasedOnABeanVaadin6StyleForm.asciidoc[Auto-generating a form based on a bean - Vaadin 6 style Form] | |||
- link:CreatingAReusableVaadinThemeInEclipse.asciidoc[Creating a reusable Vaadin theme in Eclipse] | |||
- link:CreatingATextFieldForIntegerOnlyInputWhenNotUsingADataSource.asciidoc[Creating a TextField for integer only input when not using a data source] | |||
- link:FormattingDataInGrid.asciidoc[Formatting data in grid] | |||
- link:ConfiguringGridColumnWidths.asciidoc[Configuring Grid column widths] | |||
- link:Vaadin7HierarchicalContainerAndTreeComponentExampleWithLiferayOrganizationService.asciidoc[Vaadin 7 hierarchical container and TreeComponent example with Liferay OrganizationService] | |||
- link:CreatingACustomFieldForEditingTheAddressOfAPerson.asciidoc[Creating a CustomField for editing the address of a person] | |||
- link:CreatingAMasterDetailsViewForEditingPersons.asciidoc[Creating a master details view for editing persons] | |||
- link:ShowingExtraDataForGridRows.asciidoc[Showing extra data for Grid rows] | |||
- link:CreatingATextFieldForIntegerOnlyInputUsingADataSource.asciidoc[Creating a TextField for integer only input using a data source] | |||
- link:UsingGridWithAContainer.asciidoc[Using Grid with a Container] | |||
- link:ShowingDataInGrid.asciidoc[Showing data in Grid] | |||
- link:UsingGridWithInlineData.asciidoc[Using Grid with inline data] | |||
- link:MigratingFromVaadin6ToVaadin7.asciidoc[Migrating from Vaadin 6 to Vaadin 7] | |||
- link:MigratingFromVaadin7.0ToVaadin7.1.asciidoc[Migrating from Vaadin 7.0 to Vaadin 7.1] |
@@ -0,0 +1,338 @@ | |||
package com.dmo.util.ui; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import javax.portlet.ActionRequest; | |||
import javax.portlet.ActionResponse; | |||
import javax.portlet.EventRequest; | |||
import javax.portlet.EventResponse; | |||
import javax.portlet.PortletMode; | |||
import javax.portlet.PortletRequest; | |||
import javax.portlet.PortletSession; | |||
import javax.portlet.RenderRequest; | |||
import javax.portlet.RenderResponse; | |||
import javax.portlet.ResourceRequest; | |||
import javax.portlet.ResourceResponse; | |||
import javax.servlet.annotation.WebServlet; | |||
import com.liferay.portal.kernel.exception.PortalException; | |||
import com.liferay.portal.kernel.exception.SystemException; | |||
import com.liferay.portal.kernel.util.WebKeys; | |||
import com.liferay.portal.model.Organization; | |||
import com.liferay.portal.model.User; | |||
import com.liferay.portal.service.OrganizationLocalServiceUtil; | |||
import com.liferay.portal.service.UserLocalServiceUtil; | |||
import com.liferay.portal.theme.ThemeDisplay; | |||
import com.liferay.portal.util.PortalUtil; | |||
import com.vaadin.annotations.Theme; | |||
import com.vaadin.annotations.VaadinServletConfiguration; | |||
import com.vaadin.data.util.HierarchicalContainer; | |||
import com.vaadin.event.ItemClickEvent; | |||
import com.vaadin.server.VaadinPortletSession; | |||
import com.vaadin.server.VaadinPortletSession.PortletListener; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.server.VaadinServlet; | |||
import com.vaadin.server.VaadinSession; | |||
import com.vaadin.ui.AbstractSelect.ItemCaptionMode; | |||
import com.vaadin.ui.Notification; | |||
import com.vaadin.ui.Notification.Type; | |||
import com.vaadin.ui.Tree; | |||
import com.vaadin.ui.UI; | |||
import com.vaadin.ui.VerticalLayout; | |||
@SuppressWarnings({ | |||
"serial", "deprecation" | |||
}) | |||
@Theme("dmoprojectview") | |||
public class DmoOrgTreeUI extends UI implements PortletListener { | |||
private PortletMode previousMode = null; | |||
private PortletRequest portletRequest; | |||
private PortletSession portletSession; | |||
private User user; | |||
private ThemeDisplay themeDisplay; | |||
private VerticalLayout viewContent = new VerticalLayout(); | |||
private Tree tree = new Tree("Organization Tree"); | |||
private HashSet<Long> checked = new HashSet<Long>(); | |||
@WebServlet(value = "/*", asyncSupported = true) | |||
@VaadinServletConfiguration(productionMode = false, ui = DmoOrgTreeUI.class) | |||
public static class Servlet extends VaadinServlet { | |||
} | |||
@Override | |||
protected void init(VaadinRequest request) { | |||
viewContent = new VerticalLayout(); | |||
viewContent.setMargin(true); | |||
setContent(viewContent); | |||
if (VaadinSession.getCurrent() instanceof VaadinPortletSession) { | |||
final VaadinPortletSession portletsession = (VaadinPortletSession) VaadinSession.getCurrent(); | |||
portletsession.addPortletListener(this); | |||
try { | |||
setPortletRequestUI((PortletRequest) request); | |||
setPortletSessionUI(portletsession.getPortletSession()); | |||
user = UserLocalServiceUtil.getUser(PortalUtil.getUser((PortletRequest) request).getUserId()); | |||
setThemeDisplayUI((ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY)); | |||
//System.out.println("DEBUG=>" + this.getClass() + "\n ==>themeDisplay getLayout=" + themeDisplay.getLayout().toString()); | |||
doView(); | |||
} | |||
catch (PortalException e) { | |||
e.printStackTrace(); | |||
} | |||
catch (com.liferay.portal.kernel.exception.SystemException e) { | |||
e.printStackTrace(); | |||
} | |||
} | |||
else { | |||
Notification.show("Not initialized in a Portal!", Notification.Type.ERROR_MESSAGE); | |||
} | |||
} | |||
@Override | |||
public void handleRenderRequest(RenderRequest request, RenderResponse response, UI root) { | |||
PortletMode portletMode = request.getPortletMode(); | |||
try { | |||
setPortletRequestUI((PortletRequest) request); | |||
setPortletSessionUI(request.getPortletSession()); | |||
setThemeDisplayUI((ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY)); | |||
user = UserLocalServiceUtil.getUser(PortalUtil.getUser((PortletRequest) request).getUserId()); | |||
if (request.getPortletMode() == PortletMode.VIEW) { | |||
doView(); | |||
} | |||
} | |||
catch (PortalException e) { | |||
Notification.show(e.getMessage(), Type.ERROR_MESSAGE); | |||
} | |||
catch (com.liferay.portal.kernel.exception.SystemException e) { | |||
Notification.show(e.getMessage(), Type.ERROR_MESSAGE); | |||
} | |||
setPreviousModeUI(portletMode); | |||
} | |||
@Override | |||
public void handleActionRequest(ActionRequest request, ActionResponse response, UI root) { | |||
} | |||
@Override | |||
public void handleEventRequest(EventRequest request, EventResponse response, UI root) { | |||
} | |||
@Override | |||
public void handleResourceRequest(ResourceRequest request, ResourceResponse response, UI root) { | |||
this.setThemeDisplayUI((ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY)); | |||
setPortletRequestUI((PortletRequest) request); | |||
setPortletSessionUI(request.getPortletSession()); | |||
setThemeDisplayUI((ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY)); | |||
try { | |||
user = UserLocalServiceUtil.getUser(PortalUtil.getUser((PortletRequest) request).getUserId()); | |||
} | |||
catch (PortalException e) { | |||
Notification.show(e.getMessage(), Type.ERROR_MESSAGE); | |||
} | |||
catch (com.liferay.portal.kernel.exception.SystemException e) { | |||
Notification.show(e.getMessage(), Type.ERROR_MESSAGE); | |||
} | |||
} | |||
public void doView() { | |||
try { | |||
buildMainLayout(); | |||
} | |||
catch (SystemException e) { | |||
Notification.show("System error occurred. Contact administrator.", Type.WARNING_MESSAGE); | |||
} | |||
catch (PortalException e) { | |||
Notification.show("System error occurred. Contact administrator.", Type.WARNING_MESSAGE); | |||
} | |||
catch (Exception e) { | |||
Notification.show("System error occurred. Contact administrator.", Type.WARNING_MESSAGE); | |||
} | |||
} | |||
private void buildMainLayout() | |||
throws SystemException, PortalException { | |||
if (viewContent.getComponentCount() > 0) { | |||
viewContent.removeAllComponents(); | |||
} | |||
viewContent.setMargin(true); | |||
viewContent.addStyleName("view"); | |||
List<Organization> orgList = new ArrayList<Organization>(); | |||
orgList = OrganizationLocalServiceUtil.getUserOrganizations(user.getUserId()); | |||
final HierarchicalContainer container = createTreeContent(orgList); | |||
tree = new Tree("My Organizations", container); | |||
tree.addStyleName("checkboxed"); | |||
tree.setSelectable(false); | |||
tree.setItemCaptionMode(ItemCaptionMode.PROPERTY); | |||
tree.setItemCaptionPropertyId("name"); | |||
tree.addItemClickListener(new ItemClickEvent.ItemClickListener() { | |||
public void itemClick(ItemClickEvent event) { | |||
if (event.getItemId().getClass() == Long.class) { | |||
long itemId = (Long) event.getItemId(); | |||
if (checked.contains(itemId)) { | |||
checkboxChildren(container, itemId, false); | |||
} | |||
else { | |||
checkboxChildren(container, itemId, true); | |||
tree.expandItemsRecursively(itemId); | |||
} | |||
} | |||
tree.markAsDirty(); | |||
} | |||
}); | |||
Tree.ItemStyleGenerator itemStyleGenerator = new Tree.ItemStyleGenerator() { | |||
@Override | |||
public String getStyle(Tree source, Object itemId) { | |||
if (checked.contains(itemId)) | |||
return "checked"; | |||
else | |||
return "unchecked"; | |||
} | |||
}; | |||
tree.setItemStyleGenerator(itemStyleGenerator); | |||
viewContent.addComponent(tree); | |||
viewContent.setVisible(true); | |||
setContent(viewContent); | |||
} | |||
public void checkboxChildren(HierarchicalContainer hc, long itemId, boolean bAdd) { | |||
try { | |||
if (bAdd) { | |||
checked.add(itemId); | |||
} | |||
else { | |||
checked.remove(itemId); | |||
} | |||
if (hc.hasChildren(itemId)) { | |||
Collection<?> children = hc.getChildren(itemId); | |||
for (Object o : children) { | |||
if (o.getClass() == Long.class) { | |||
itemId = (Long) o; | |||
checkboxChildren(hc, itemId, bAdd); | |||
} | |||
} | |||
} | |||
} | |||
catch (Exception e) { | |||
Notification.show("Unable to build Organization tree. Contact Administrator.", Type.ERROR_MESSAGE); | |||
} | |||
} | |||
public static HierarchicalContainer createTreeContent(List<Organization> oTrees) | |||
throws SystemException, PortalException { | |||
HierarchicalContainer container = new HierarchicalContainer(); | |||
container.addContainerProperty("name", String.class, ""); | |||
new Object() { | |||
@SuppressWarnings("unchecked") | |||
public void put(List<Organization> data, HierarchicalContainer container) | |||
throws SystemException, PortalException { | |||
for (Organization o : data) { | |||
long orgId = o.getOrganizationId(); | |||
if (!container.containsId(orgId)) { | |||
container.addItem(orgId); | |||
container.getItem(orgId).getItemProperty("name").setValue(o.getName()); | |||
if (!o.hasSuborganizations()) { | |||
container.setChildrenAllowed(orgId, false); | |||
} | |||
else { | |||
container.setChildrenAllowed(orgId, true); | |||
} | |||
if (o.isRoot()) { | |||
container.setParent(orgId, null); | |||
} | |||
else { | |||
if (!container.containsId(o.getParentOrganizationId())) { | |||
List<Organization> sub = new ArrayList<Organization>(); | |||
sub.add(o.getParentOrganization()); | |||
put(sub, container); | |||
} | |||
container.setParent(orgId, (Object) o.getParentOrganizationId()); | |||
} | |||
} | |||
} | |||
} | |||
}.put(oTrees, container); | |||
return container; | |||
} | |||
public PortletRequest getPortletRequestUI() { | |||
return portletRequest; | |||
} | |||
public void setPortletRequestUI(PortletRequest portletRequest) { | |||
this.portletRequest = portletRequest; | |||
} | |||
public PortletSession getPortletSessionUI() { | |||
return portletSession; | |||
} | |||
public void setPortletSessionUI(PortletSession portletSession) { | |||
this.portletSession = portletSession; | |||
} | |||
public ThemeDisplay getThemeDisplayUI() { | |||
return themeDisplay; | |||
} | |||
public void setThemeDisplayUI(ThemeDisplay themeDisplay) { | |||
this.themeDisplay = themeDisplay; | |||
} | |||
public PortletMode getPreviousModeUI() { | |||
return previousMode; | |||
} | |||
public void setPreviousModeUI(PortletMode previousMode) { | |||
this.previousMode = previousMode; | |||
} | |||
} |