]> source.dussan.org Git - vaadin-framework.git/commitdiff
Migrate AddingJPAToTheAddressBookDemo
authorErik Lumme <erik@vaadin.com>
Fri, 15 Sep 2017 08:02:28 +0000 (11:02 +0300)
committerErik Lumme <erik@vaadin.com>
Fri, 15 Sep 2017 08:02:28 +0000 (11:02 +0300)
documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc [new file with mode: 0644]
documentation/articles/contents.asciidoc
documentation/articles/img/ab-with-vaadin-scrshot.png [new file with mode: 0644]
documentation/articles/img/architecture2.png [new file with mode: 0644]

diff --git a/documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc b/documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc
new file mode 100644 (file)
index 0000000..73a2eb7
--- /dev/null
@@ -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.
index d4eb3d31649ab00772ac7e5d8b0c28e64459645e..a5f3f3152adcfab618871e7d341276f8e9ec531e 100644 (file)
@@ -18,3 +18,4 @@
 - 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]
diff --git a/documentation/articles/img/ab-with-vaadin-scrshot.png b/documentation/articles/img/ab-with-vaadin-scrshot.png
new file mode 100644 (file)
index 0000000..4da34e1
Binary files /dev/null and b/documentation/articles/img/ab-with-vaadin-scrshot.png differ
diff --git a/documentation/articles/img/architecture2.png b/documentation/articles/img/architecture2.png
new file mode 100644 (file)
index 0000000..c9631cc
Binary files /dev/null and b/documentation/articles/img/architecture2.png differ