Sunday, September 18, 2011

RESTication of Openxava app and Vaadin client

Article under construction

The purpose of this article is to provide REST interface for Openxava (OX) application.
It shows how-to
  • integrate jersey engine into the OX build
  • extends the JPA2 entities with JAXB and JAXRS annotations
  • write a little UC with methods that returns xml/json message
  • test it
  • Make a sample Vaadin client application for those REST services
To illustrate this, I will take an Openxava application generated by minuteproject (http://minuteproject.blogspot.com/2011/09/sql-select-to-web-application-in-couple.html)

Integrate Jersey engine in OX build

Jersey lib integration
Add the following libraries in your project /web/WEB-INF/lib
  • jsr305-1.3.2.jar
  • jersey-core-1.9-ea03.jar
  • jersey-server-1.9-ea03.jar
  • asm-3.1.jar (although asm.jar is shipped with OX this version is to used)
Here the dependency is on jersey-server-1.9-ea03

In your OX project add the file servlets.xml into /web/WEB-INF

   <servlet>
<servlet-name>petshop Jersey Web Application</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
<param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>my.cool.report</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>petshop Jersey Web Application</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>

Openxava build takes what you define in servlets.xml file and insert it in the web.xml
The snippet above specifies the following:
  • Jersey engine will parse the package for JAXRS annotation in the package "my.cool.report"
  • the context path used for REST will be /rest/*
JAXB & JAXRS integration

Add Jaxb and Jaxrs annotation in OX jpa2 entities

Class MyConferenceCoolReport
package my.cool.report.conference.domain.report;

import java.sql.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.xml.bind.annotation.*;

import my.cool.report.conference.dto.report.*;

import org.openxava.annotations.*;
import org.openxava.jpa.*;


/**
*
*

Title: MyConferenceCoolReport


*
*

Description: Domain Object describing a MyConferenceCoolReport entity


*
*/
@Entity (name="MyConferenceCoolReport")
@Table (name="my_conference_cool_report")
@Views({
@View(
name="base",
members=
""
+ "firstName ; "
+ "lastName ; "
+ "email ; "
+ "status ; "
+ "addressStreet1 ; "
+ "addressStreet2 ; "
+ "countryCode ; "
+ "conferenceName ; "
+ "conferenceBeginDate ; "
+ "conferenceEndDate ; "
),
@View(
name="Create",
extendsView="base"
),
@View(
name="Update",
extendsView="base",
members=
""
),
@View(extendsView="base",
members=
""
),
@View(name="myConferenceCoolReportDEFAULT_VIEW",
members=
" id ;"
+ "firstName ; "
+ "lastName ; "
+ "email ; "
+ "status ; "
+ "addressStreet1 ; "
+ "addressStreet2 ; "
+ "countryCode ; "
+ "conferenceName ; "
+ "conferenceBeginDate ; "
+ "conferenceEndDate ; "
)
})

@Tabs({
@Tab(
properties=
" firstName "
+", lastName "
+", email "
+", status "
+", addressStreet1 "
+", addressStreet2 "
+", countryCode "
+", conferenceName "
+", conferenceBeginDate "
+", conferenceEndDate "
)
,
@Tab(
name = "MyConferenceCoolReportTab",
properties=
" firstName "
+", lastName "
+", email "
+", status "
+", addressStreet1 "
+", addressStreet2 "
+", countryCode "
+", conferenceName "
+", conferenceBeginDate "
+", conferenceEndDate "
)
,
@Tab(
name = "MyConferenceCoolReportTabWithRef",
properties=
" firstName "
+", lastName "
+", email "
+", status "
+", addressStreet1 "
+", addressStreet2 "
+", countryCode "
+", conferenceName "
+", conferenceBeginDate "
+", conferenceEndDate "
)
})
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@XmlRootElement
@Path ("/myreports")
public class MyConferenceCoolReport {

@Hidden @Id @Column(name="id" )
private Long id;

@Column(name="first_name", length=255, nullable=false, unique=false)
@Required
private String firstName;
@Column(name="last_name", length=255, nullable=false, unique=false)
@Required
private String lastName;
@Column(name="email", length=255, nullable=false, unique=false)
@Required
private String email;
@Column(name="status", length=45, nullable=false, unique=false)
@Required
private String status;
@Column(name="address_street1", length=255, nullable=true, unique=false)
private String addressStreet1;
@Column(name="address_street2", length=255, nullable=true, unique=false)
private String addressStreet2;
@Column(name="country_code", length=45, nullable=false, unique=false)
@Required
private String countryCode;
@Column(name="conference_name", length=255, nullable=false, unique=false)
@Required
private String conferenceName;
@Column(name="conference_begin_date", nullable=true, unique=false)
private Date conferenceBeginDate;
@Column(name="conference_end_date", nullable=true, unique=false)
private Date conferenceEndDate;

/**
* Default constructor
*/
public MyConferenceCoolReport() {
}

public Long getId() {
return id;
}

public void setId (Long id) {
this.id = id;
}

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;
}
public String getEmail() {
return email;
}

public void setEmail (String email) {
this.email = email;
}
public String getStatus() {
return status;
}

public void setStatus (String status) {
this.status = status;
}
public String getAddressStreet1() {
return addressStreet1;
}

public void setAddressStreet1 (String addressStreet1) {
this.addressStreet1 = addressStreet1;
}
public String getAddressStreet2() {
return addressStreet2;
}

public void setAddressStreet2 (String addressStreet2) {
this.addressStreet2 = addressStreet2;
}
public String getCountryCode() {
return countryCode;
}

public void setCountryCode (String countryCode) {
this.countryCode = countryCode;
}
public String getConferenceName() {
return conferenceName;
}

public void setConferenceName (String conferenceName) {
this.conferenceName = conferenceName;
}
public Date getConferenceBeginDate() {
return conferenceBeginDate;
}

public void setConferenceBeginDate (Date conferenceBeginDate) {
this.conferenceBeginDate = conferenceBeginDate;
}
public Date getConferenceEndDate() {
return conferenceEndDate;
}

public void setConferenceEndDate (Date conferenceEndDate) {
this.conferenceEndDate = conferenceEndDate;
}

@Transient
@GET
@Path("{reportId}")
@Produces ({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public MyConferenceCoolReport findById (@PathParam ("reportId") Long reportId) {
EntityManager em = XPersistence.getManager();
MyConferenceCoolReport p = em.find(MyConferenceCoolReport.class, reportId);
return p;
}

@Transient
@GET
@QueryParam("{countryCode}")
@Produces ({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public MyConfReports find (@QueryParam ("countryCode") String countryCode) {
String q = null;
EntityManager em = XPersistence.getManager();
if (countryCode!=null) {
q = "select m from MyConferenceCoolReport m where countryCode = '"+countryCode+"'";
} else
q = "select m from MyConferenceCoolReport m ";
Query query = em.createQuery(q);
List<myconferencecoolreport> l = query.getResultList();
MyConfReports m = new MyConfReports();
m.setMyCoolReport(l);
return m;
}

}

Annotation of entities
The previous snippet indicates that entity MyConferenceCoolReport contains a transient method (findById) use for REST GET operation and using the param reportId to load an entity. It can be returned either as Xml or in a Json format.
It contains also a method find that takes a parameter a country code and return a DTO based on the class MyConfReports

package my.cool.report.conference.dto.report;
package my.cool.report.conference.dto.report;

import java.util.*;

import javax.xml.bind.annotation.*;

import my.cool.report.conference.domain.report.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@XmlRootElement
public class MyConfReports {

private List<MyConferenceCoolReport> myCoolReport;

public MyConfReports () {
}

public List<MyConferenceCoolReport> getMyCoolReport() {
return myCoolReport;
}

public void setMyCoolReport(List<MyConferenceCoolReport> myCoolReport) {
this.myCoolReport = myCoolReport;
}

}
Deploy
Use the ant build on your project.
Remark: do not forget when using openxava with eclipse to set "Build Automatically" on the "Project" tab.

Testing
Via the browser type
http://localhost:8080/conference/rest/myreports?countryCode=BE to see all the


http://localhost:8080/conference/rest/myreports/ it returns in an XML format.




Vaadin app client
//todo

Conclusion
//todo


Saturday, September 3, 2011

SQL select to web application in couple of seconds

Maybe you are not aware but if you just know how to write one sql select statement, you're just couple of seconds from having an web application and couple of minutes from enjoying it as a Liferay portlet.
The key ingredients for this recipe are: Openxava, Liferay and ... Minuteproject.

The equation is:
sql select statement (containing one field considered as unique) = Openxava (Webapp) + Liferay (Portlet)
in less than 5 minutes.

Prerequisites
  • Download Openxava (4.2.2), Liferay, Minuteproject (0.5.7.1+)
  • Set environment variables OX_HOME and MP_HOME to Openxava and Minuteproject home directories respectively.
  • Use Java 6
Create your statement
  • Create a sql select statement
  • Ensure that one of the return field can be considered as unique (if not add one)
  • Create a view from your statement
    • Ex: CREATE VIEW abc as
Remark:
Why is it necessary to have a unique field?
It is to ensure that MP can place a @Id JPA2 annotation on one field of the generated entity.
It has to be unique to avoid issue dealing with first level cache.

Start Minuteproject console
  • Point to your model and fill your model name (ex: efg)
  • select your view abc (in customisation tab)
  • add the unique field as 'virtual' primary key. Your unique field is not a PK but it will acts as to uniquely identify a record entry.
  • Choose target openxava and click generate.
Release your application
  • go to the generated code
  • check environment variables (OX_HOME, MP_HOME) are set.
  • execute from a command prompt: build-efg.cmd/sh
  • depending on the OS from couple of seconds to a dozen of seconds and a browser should open to the address http://localhost:8080/egf/xava/homeMenu.jsp
Deploy on Liferay
  • go to OX_HOME/workspace.dist/efg.dist/efg.war
  • add ejb.jar and jdbc driver jar in LIFERAY_HOME/tomcat/lib
  • start Liferay
  • Upload your portlet
  • Install your portlet
  • Enjoy
Remark:
Whether on the standalone tomcat app or on the Liferay deployed portlet, Openxava is only offering the user selection. It is normal since the DB object is a view, Minuteproject 4 Openxava suppress the Create/Update/Delete, just the Select functionality is kept.


Example

As model I take the lazuly model project in version 0.1 it contains 13 tables.
The sql to create the schema on mysql can be downloaded at http://code.google.com/p/lazuly/source/browse/trunk/conference-db/conference-db-mysql-0.1.sql

Create your view from your sql select statement


Start MinuteProject console


Scope your model


Generate
Choose your technology: here Openxava, and click on Generate.
The code will be generated in MP_HOME/output/conference/OpenXava

Make a standalone Openxava app
Go to the generated code
Open a prompt
Check or set OX_HOME and MP_HOME
execute build-conference.cmd/sh
Couple of seconds later a browser should open to the model (conference) menu.
Report tab has one entry 'my conference cool report'.
When clicking on it here is a sample screen with data that you could see:



Deploy on Liferay
  • Add the datasource (the snippet for tomcat is in OX_HOME/workspace/conference/OpenXava/openxava/other/tomcat/snippet/conference_jndi_for_tomcat_snippet.txt is to be inserted into LIFERAY_HOME/tomcat/conf/context.xml)
  • Upload the portlet war (OX_HOME/workspace.dist/conference.dist/conference.war
  • Position it
  • Enjoy.




Other information

You can also find other information regarding Minuteproject 4 Openxava generation at

Conclusion
  • Your model is your technology tutorial
  • Productivity can be greatly improved
  • Data Quality and troubleshooting operations are quickly available
  • Keep on improving send feed-back to minuteproject@gmail.com or tweet @minuteproject