diff options
author | Henri Sara <henri.sara@gmail.com> | 2017-09-19 09:50:11 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-19 09:50:11 +0300 |
commit | 15a76bbdcaa6cd551e85c9e1e3c681e912885e8a (patch) | |
tree | 6027d86bc9bf9afc7e0838eddabd1e4b29fc59dc | |
parent | 872d489506c1b1108c4e99134f0473c4b4b987fa (diff) | |
parent | 377695d75dee8ddfaef9b994b1c85b50672f2d25 (diff) | |
download | vaadin-framework-15a76bbdcaa6cd551e85c9e1e3c681e912885e8a.tar.gz vaadin-framework-15a76bbdcaa6cd551e85c9e1e3c681e912885e8a.zip |
Migrate Vaadin 7 wiki articles to documentation (#9993)
69 files changed, 8733 insertions, 0 deletions
diff --git a/documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc b/documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc new file mode 100644 index 0000000000..73a2eb72f7 --- /dev/null +++ b/documentation/articles/AddingJPAToTheAddressBookDemo.asciidoc @@ -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. diff --git a/documentation/articles/AutoGeneratingAFormBasedOnABeanVaadin6StyleForm.asciidoc b/documentation/articles/AutoGeneratingAFormBasedOnABeanVaadin6StyleForm.asciidoc new file mode 100644 index 0000000000..8c4851bfe7 --- /dev/null +++ b/documentation/articles/AutoGeneratingAFormBasedOnABeanVaadin6StyleForm.asciidoc @@ -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)); + } + } +} +.... diff --git a/documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc b/documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc new file mode 100644 index 0000000000..d7c8fdba30 --- /dev/null +++ b/documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc @@ -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. diff --git a/documentation/articles/ChangingTheDefaultConvertersForAnApplication.asciidoc b/documentation/articles/ChangingTheDefaultConvertersForAnApplication.asciidoc new file mode 100644 index 0000000000..1110a99eaa --- /dev/null +++ b/documentation/articles/ChangingTheDefaultConvertersForAnApplication.asciidoc @@ -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`. diff --git a/documentation/articles/ConfiguringGridColumnWidths.asciidoc b/documentation/articles/ConfiguringGridColumnWidths.asciidoc new file mode 100644 index 0000000000..322780c422 --- /dev/null +++ b/documentation/articles/ConfiguringGridColumnWidths.asciidoc @@ -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. diff --git a/documentation/articles/CreatingABasicApplication.asciidoc b/documentation/articles/CreatingABasicApplication.asciidoc new file mode 100644 index 0000000000..e5dde4995f --- /dev/null +++ b/documentation/articles/CreatingABasicApplication.asciidoc @@ -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. diff --git a/documentation/articles/CreatingACustomFieldForEditingTheAddressOfAPerson.asciidoc b/documentation/articles/CreatingACustomFieldForEditingTheAddressOfAPerson.asciidoc new file mode 100644 index 0000000000..a28b63150f --- /dev/null +++ b/documentation/articles/CreatingACustomFieldForEditingTheAddressOfAPerson.asciidoc @@ -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] diff --git a/documentation/articles/CreatingAMasterDetailsViewForEditingPersons.asciidoc b/documentation/articles/CreatingAMasterDetailsViewForEditingPersons.asciidoc new file mode 100644 index 0000000000..7ad2e94967 --- /dev/null +++ b/documentation/articles/CreatingAMasterDetailsViewForEditingPersons.asciidoc @@ -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`. diff --git a/documentation/articles/CreatingAReusableVaadinThemeInEclipse.asciidoc b/documentation/articles/CreatingAReusableVaadinThemeInEclipse.asciidoc new file mode 100644 index 0000000000..f4c8eced65 --- /dev/null +++ b/documentation/articles/CreatingAReusableVaadinThemeInEclipse.asciidoc @@ -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]! diff --git a/documentation/articles/CreatingATextFieldForIntegerOnlyInputUsingADataSource.asciidoc b/documentation/articles/CreatingATextFieldForIntegerOnlyInputUsingADataSource.asciidoc new file mode 100644 index 0000000000..980049e9e9 --- /dev/null +++ b/documentation/articles/CreatingATextFieldForIntegerOnlyInputUsingADataSource.asciidoc @@ -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. diff --git a/documentation/articles/CreatingATextFieldForIntegerOnlyInputWhenNotUsingADataSource.asciidoc b/documentation/articles/CreatingATextFieldForIntegerOnlyInputWhenNotUsingADataSource.asciidoc new file mode 100644 index 0000000000..62c8390a07 --- /dev/null +++ b/documentation/articles/CreatingATextFieldForIntegerOnlyInputWhenNotUsingADataSource.asciidoc @@ -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()`. diff --git a/documentation/articles/CreatingAnApplicationWithDifferentFeaturesForDifferentClients.asciidoc b/documentation/articles/CreatingAnApplicationWithDifferentFeaturesForDifferentClients.asciidoc new file mode 100644 index 0000000000..c8b073868f --- /dev/null +++ b/documentation/articles/CreatingAnApplicationWithDifferentFeaturesForDifferentClients.asciidoc @@ -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. diff --git a/documentation/articles/CreatingYourOwnConverterForString.asciidoc b/documentation/articles/CreatingYourOwnConverterForString.asciidoc new file mode 100644 index 0000000000..2c41f5caf5 --- /dev/null +++ b/documentation/articles/CreatingYourOwnConverterForString.asciidoc @@ -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()); + } + } +})); +.... diff --git a/documentation/articles/FindingTheCurrentRootAndApplication.asciidoc b/documentation/articles/FindingTheCurrentRootAndApplication.asciidoc new file mode 100644 index 0000000000..05b90308de --- /dev/null +++ b/documentation/articles/FindingTheCurrentRootAndApplication.asciidoc @@ -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). diff --git a/documentation/articles/FormattingDataInGrid.asciidoc b/documentation/articles/FormattingDataInGrid.asciidoc new file mode 100644 index 0000000000..0562ee9823 --- /dev/null +++ b/documentation/articles/FormattingDataInGrid.asciidoc @@ -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()); + } +} +.... diff --git a/documentation/articles/JMeterTesting.asciidoc b/documentation/articles/JMeterTesting.asciidoc new file mode 100644 index 0000000000..76e02135ab --- /dev/null +++ b/documentation/articles/JMeterTesting.asciidoc @@ -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); + } + } +} +.... diff --git a/documentation/articles/JasperReportsOnVaadinSample.asciidoc b/documentation/articles/JasperReportsOnVaadinSample.asciidoc new file mode 100644 index 0000000000..cac8d261f2 --- /dev/null +++ b/documentation/articles/JasperReportsOnVaadinSample.asciidoc @@ -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. diff --git a/documentation/articles/LazyQueryContainer.asciidoc b/documentation/articles/LazyQueryContainer.asciidoc new file mode 100644 index 0000000000..54e561fa50 --- /dev/null +++ b/documentation/articles/LazyQueryContainer.asciidoc @@ -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(); + } +} +.... diff --git a/documentation/articles/MigratingFromVaadin6ToVaadin7.asciidoc b/documentation/articles/MigratingFromVaadin6ToVaadin7.asciidoc new file mode 100644 index 0000000000..6a10cacbde --- /dev/null +++ b/documentation/articles/MigratingFromVaadin6ToVaadin7.asciidoc @@ -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. diff --git a/documentation/articles/MigratingFromVaadin7.0ToVaadin7.1.asciidoc b/documentation/articles/MigratingFromVaadin7.0ToVaadin7.1.asciidoc new file mode 100644 index 0000000000..2f65f0f74f --- /dev/null +++ b/documentation/articles/MigratingFromVaadin7.0ToVaadin7.1.asciidoc @@ -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`. diff --git a/documentation/articles/OfflineModeForTouchKit4MobileApps.asciidoc b/documentation/articles/OfflineModeForTouchKit4MobileApps.asciidoc new file mode 100644 index 0000000000..e215c0921b --- /dev/null +++ b/documentation/articles/OfflineModeForTouchKit4MobileApps.asciidoc @@ -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. diff --git a/documentation/articles/ScalaAndVaadinHOWTO.asciidoc b/documentation/articles/ScalaAndVaadinHOWTO.asciidoc new file mode 100644 index 0000000000..d0703aa58d --- /dev/null +++ b/documentation/articles/ScalaAndVaadinHOWTO.asciidoc @@ -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. diff --git a/documentation/articles/ShowingDataInGrid.asciidoc b/documentation/articles/ShowingDataInGrid.asciidoc new file mode 100644 index 0000000000..8f84d0ab11 --- /dev/null +++ b/documentation/articles/ShowingDataInGrid.asciidoc @@ -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; + } +} +.... diff --git a/documentation/articles/ShowingExtraDataForGridRows.asciidoc b/documentation/articles/ShowingExtraDataForGridRows.asciidoc new file mode 100644 index 0000000000..71cc69d7f7 --- /dev/null +++ b/documentation/articles/ShowingExtraDataForGridRows.asciidoc @@ -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); + } +} +.... diff --git a/documentation/articles/SimplifiedRPCusingJavaScript.asciidoc b/documentation/articles/SimplifiedRPCusingJavaScript.asciidoc new file mode 100644 index 0000000000..31e69832e0 --- /dev/null +++ b/documentation/articles/SimplifiedRPCusingJavaScript.asciidoc @@ -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. diff --git a/documentation/articles/UsingGridWithAContainer.asciidoc b/documentation/articles/UsingGridWithAContainer.asciidoc new file mode 100644 index 0000000000..5782c698ed --- /dev/null +++ b/documentation/articles/UsingGridWithAContainer.asciidoc @@ -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; + } +} +.... diff --git a/documentation/articles/UsingGridWithInlineData.asciidoc b/documentation/articles/UsingGridWithInlineData.asciidoc new file mode 100644 index 0000000000..6a3640894a --- /dev/null +++ b/documentation/articles/UsingGridWithInlineData.asciidoc @@ -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); + } +} +.... diff --git a/documentation/articles/UsingHibernateWithVaadin.asciidoc b/documentation/articles/UsingHibernateWithVaadin.asciidoc new file mode 100644 index 0000000000..21416ebe98 --- /dev/null +++ b/documentation/articles/UsingHibernateWithVaadin.asciidoc @@ -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). diff --git a/documentation/articles/UsingJDBCwithLazyQueryContainerAndFilteringTable.asciidoc b/documentation/articles/UsingJDBCwithLazyQueryContainerAndFilteringTable.asciidoc new file mode 100644 index 0000000000..2f5e73e6ed --- /dev/null +++ b/documentation/articles/UsingJDBCwithLazyQueryContainerAndFilteringTable.asciidoc @@ -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("`","`")); +.... diff --git a/documentation/articles/UsingPhoneGapBuildWithVaadinTouchKit.asciidoc b/documentation/articles/UsingPhoneGapBuildWithVaadinTouchKit.asciidoc new file mode 100644 index 0000000000..337b3a2905 --- /dev/null +++ b/documentation/articles/UsingPhoneGapBuildWithVaadinTouchKit.asciidoc @@ -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. diff --git a/documentation/articles/UsingPython.asciidoc b/documentation/articles/UsingPython.asciidoc new file mode 100644 index 0000000000..8f1d433759 --- /dev/null +++ b/documentation/articles/UsingPython.asciidoc @@ -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. diff --git a/documentation/articles/UsingVaadinInAnExistingGWTProject.asciidoc b/documentation/articles/UsingVaadinInAnExistingGWTProject.asciidoc new file mode 100644 index 0000000000..00d7e10137 --- /dev/null +++ b/documentation/articles/UsingVaadinInAnExistingGWTProject.asciidoc @@ -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. diff --git a/documentation/articles/VAccessControl.asciidoc b/documentation/articles/VAccessControl.asciidoc new file mode 100644 index 0000000000..dce9c3a6c0 --- /dev/null +++ b/documentation/articles/VAccessControl.asciidoc @@ -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)`. diff --git a/documentation/articles/Vaadin7HierarchicalContainerAndTreeComponentExampleWithLiferayOrganizationService.asciidoc b/documentation/articles/Vaadin7HierarchicalContainerAndTreeComponentExampleWithLiferayOrganizationService.asciidoc new file mode 100644 index 0000000000..988f59f62f --- /dev/null +++ b/documentation/articles/Vaadin7HierarchicalContainerAndTreeComponentExampleWithLiferayOrganizationService.asciidoc @@ -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; +} +.... diff --git a/documentation/articles/contents.asciidoc b/documentation/articles/contents.asciidoc new file mode 100644 index 0000000000..c2e3743cb2 --- /dev/null +++ b/documentation/articles/contents.asciidoc @@ -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] diff --git a/documentation/articles/img/DmoOrgTreeUI.java b/documentation/articles/img/DmoOrgTreeUI.java new file mode 100644 index 0000000000..5e343cc347 --- /dev/null +++ b/documentation/articles/img/DmoOrgTreeUI.java @@ -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; + } + +} diff --git a/documentation/articles/img/JAR Export (1).png b/documentation/articles/img/JAR Export (1).png Binary files differnew file mode 100644 index 0000000000..a67ef892b6 --- /dev/null +++ b/documentation/articles/img/JAR Export (1).png diff --git a/documentation/articles/img/JAR Export (2).png b/documentation/articles/img/JAR Export (2).png Binary files differnew file mode 100644 index 0000000000..7a88f9be92 --- /dev/null +++ b/documentation/articles/img/JAR Export (2).png diff --git a/documentation/articles/img/New Java Project.png b/documentation/articles/img/New Java Project.png Binary files differnew file mode 100644 index 0000000000..b22895d183 --- /dev/null +++ b/documentation/articles/img/New Java Project.png diff --git a/documentation/articles/img/New Vaadin project (1).png b/documentation/articles/img/New Vaadin project (1).png Binary files differnew file mode 100644 index 0000000000..73799e80b6 --- /dev/null +++ b/documentation/articles/img/New Vaadin project (1).png diff --git a/documentation/articles/img/New Vaadin project (2).png b/documentation/articles/img/New Vaadin project (2).png Binary files differnew file mode 100644 index 0000000000..3bde1bef7a --- /dev/null +++ b/documentation/articles/img/New Vaadin project (2).png diff --git a/documentation/articles/img/Theme to build path.png b/documentation/articles/img/Theme to build path.png Binary files differnew file mode 100644 index 0000000000..5641c4b987 --- /dev/null +++ b/documentation/articles/img/Theme to build path.png diff --git a/documentation/articles/img/Theme to deployment assembly.png b/documentation/articles/img/Theme to deployment assembly.png Binary files differnew file mode 100644 index 0000000000..d6b58bf898 --- /dev/null +++ b/documentation/articles/img/Theme to deployment assembly.png diff --git a/documentation/articles/img/VAADIN to deployment assembly.png b/documentation/articles/img/VAADIN to deployment assembly.png Binary files differnew file mode 100644 index 0000000000..44d3463ca5 --- /dev/null +++ b/documentation/articles/img/VAADIN to deployment assembly.png diff --git a/documentation/articles/img/Vaadin to build path.png b/documentation/articles/img/Vaadin to build path.png Binary files differnew file mode 100644 index 0000000000..a346c0ab15 --- /dev/null +++ b/documentation/articles/img/Vaadin to build path.png diff --git a/documentation/articles/img/VaadinJasperReportsSample_small.jpg b/documentation/articles/img/VaadinJasperReportsSample_small.jpg Binary files differnew file mode 100644 index 0000000000..47f14fa7a4 --- /dev/null +++ b/documentation/articles/img/VaadinJasperReportsSample_small.jpg diff --git a/documentation/articles/img/ab-with-vaadin-scrshot.png b/documentation/articles/img/ab-with-vaadin-scrshot.png Binary files differnew file mode 100644 index 0000000000..4da34e1b24 --- /dev/null +++ b/documentation/articles/img/ab-with-vaadin-scrshot.png diff --git a/documentation/articles/img/address editor.png b/documentation/articles/img/address editor.png Binary files differnew file mode 100644 index 0000000000..1dd60c3dc8 --- /dev/null +++ b/documentation/articles/img/address editor.png diff --git a/documentation/articles/img/architecture.png b/documentation/articles/img/architecture.png Binary files differnew file mode 100644 index 0000000000..fa0930179a --- /dev/null +++ b/documentation/articles/img/architecture.png diff --git a/documentation/articles/img/architecture2.png b/documentation/articles/img/architecture2.png Binary files differnew file mode 100644 index 0000000000..c9631cc022 --- /dev/null +++ b/documentation/articles/img/architecture2.png diff --git a/documentation/articles/img/buttons added.png b/documentation/articles/img/buttons added.png Binary files differnew file mode 100644 index 0000000000..26ea5852f9 --- /dev/null +++ b/documentation/articles/img/buttons added.png diff --git a/documentation/articles/img/complexdomain.png b/documentation/articles/img/complexdomain.png Binary files differnew file mode 100644 index 0000000000..050049dff5 --- /dev/null +++ b/documentation/articles/img/complexdomain.png diff --git a/documentation/articles/img/complexdomain_saving.png b/documentation/articles/img/complexdomain_saving.png Binary files differnew file mode 100644 index 0000000000..97cf0cc790 --- /dev/null +++ b/documentation/articles/img/complexdomain_saving.png diff --git a/documentation/articles/img/complexdomain_saving2.png b/documentation/articles/img/complexdomain_saving2.png Binary files differnew file mode 100644 index 0000000000..af4d5d4f25 --- /dev/null +++ b/documentation/articles/img/complexdomain_saving2.png diff --git a/documentation/articles/img/customForms.png b/documentation/articles/img/customForms.png Binary files differnew file mode 100644 index 0000000000..0edb698b71 --- /dev/null +++ b/documentation/articles/img/customForms.png diff --git a/documentation/articles/img/database connected.png b/documentation/articles/img/database connected.png Binary files differnew file mode 100644 index 0000000000..80c3588224 --- /dev/null +++ b/documentation/articles/img/database connected.png diff --git a/documentation/articles/img/deployartifact.png b/documentation/articles/img/deployartifact.png Binary files differnew file mode 100644 index 0000000000..f3e89561db --- /dev/null +++ b/documentation/articles/img/deployartifact.png diff --git a/documentation/articles/img/jm1B.png b/documentation/articles/img/jm1B.png Binary files differnew file mode 100644 index 0000000000..9d4bf9ecf1 --- /dev/null +++ b/documentation/articles/img/jm1B.png diff --git a/documentation/articles/img/jm2B.png b/documentation/articles/img/jm2B.png Binary files differnew file mode 100644 index 0000000000..dfcbd6d30a --- /dev/null +++ b/documentation/articles/img/jm2B.png diff --git a/documentation/articles/img/jm3B.png b/documentation/articles/img/jm3B.png Binary files differnew file mode 100644 index 0000000000..da2cb86b65 --- /dev/null +++ b/documentation/articles/img/jm3B.png diff --git a/documentation/articles/img/jm4.png b/documentation/articles/img/jm4.png Binary files differnew file mode 100644 index 0000000000..4dedd80647 --- /dev/null +++ b/documentation/articles/img/jm4.png diff --git a/documentation/articles/img/jm5.png b/documentation/articles/img/jm5.png Binary files differnew file mode 100644 index 0000000000..058193fe4f --- /dev/null +++ b/documentation/articles/img/jm5.png diff --git a/documentation/articles/img/master detail wireframe.jpg b/documentation/articles/img/master detail wireframe.jpg Binary files differnew file mode 100644 index 0000000000..4745b4831f --- /dev/null +++ b/documentation/articles/img/master detail wireframe.jpg diff --git a/documentation/articles/img/person editor.png b/documentation/articles/img/person editor.png Binary files differnew file mode 100644 index 0000000000..71de0a3ebb --- /dev/null +++ b/documentation/articles/img/person editor.png diff --git a/documentation/articles/img/process.png b/documentation/articles/img/process.png Binary files differnew file mode 100644 index 0000000000..f8b1b3e1ad --- /dev/null +++ b/documentation/articles/img/process.png diff --git a/documentation/articles/img/screenshot.png b/documentation/articles/img/screenshot.png Binary files differnew file mode 100644 index 0000000000..13522d4c66 --- /dev/null +++ b/documentation/articles/img/screenshot.png diff --git a/documentation/articles/img/sd_s_per_r.gif b/documentation/articles/img/sd_s_per_r.gif Binary files differnew file mode 100644 index 0000000000..cccdddc43b --- /dev/null +++ b/documentation/articles/img/sd_s_per_r.gif diff --git a/documentation/articles/img/table and form.png b/documentation/articles/img/table and form.png Binary files differnew file mode 100644 index 0000000000..d454da1fd9 --- /dev/null +++ b/documentation/articles/img/table and form.png diff --git a/documentation/articles/img/views.png b/documentation/articles/img/views.png Binary files differnew file mode 100644 index 0000000000..fd4bb48f64 --- /dev/null +++ b/documentation/articles/img/views.png |