Wednesday, June 24, 2009

Tapestry5 - How to dynamically populate a drop down list

Suppose you have 2 drop down lists - A & B
You want to populate the contents of B based on the selection in A
(For example: A = car manufacturer & B = car dealership)


In yourpage.tml


<t:form t:id="createAssetsForm">
<input t:id="selectManufacturer"/>
<input t:id="selectDealer" model="dealerSelection" />
</t:form>

<script>
function onManufacturerSelectFunction(response)
{
var selectDealer = document.getElementById("selectDealer");

// Unload the existing dealers
for (i = selectDealer.length; i != 0; i--) {
selectDealer.remove(i-1);
}

// Load the new dealers
for (i=0; i != response.dealers.length; i++){
var option = document.createElement('option');
option.text = response.dealers[i];
try {
selectDealer.add(option,null); // standards compliant
}
catch(ex){
selectDealer.add(option); //IE only
}
}
}
</script>


In yourpage.java
@Property
@Persist
private String manufacturer;

@Component(parameters = {"value=manufacturer", "model=literal:Honda, Toyota, Nissan, Subaru", "event=change",
"onCompleteCallback=literal:onManufacturerSelectFunction"})
@Mixins({"ck/OnEvent"})
private Select selectManufacturer;

@Property
@Persist
private String dealer;

@Component(parameters = {"value=dealer"})
private Select selectDealer;

@Property
@Persist
private SelectModel dealerSelection;


@OnEvent(component = "selectManufacturer", value = "change")
public JSONObject onTypeChangeEvent(String selectedManufacturer)
{
List<String> dealers = new ArrayList<String>();
... code to update dealers based on selectedManufacturer ...

// update the dealer selection
dealerSelection = TapestryInternalUtils.toSelectModel(dealers);

// IF you stop here - then the dealers will be updated only on page refresh!
// To update the dealers by AJAX (without page loading) do the following

// Pass the information to javascript as a JSON array
JSONArray JSONDealers = new JSONArray();

... code to enter the dealer names in the JSONArray ...

JSONObject json = new JSONObject();
json.put("dealers", JSONDealers);

return json;
}

Tuesday, June 23, 2009

Tapestry5 - How to handle multiple submit buttons on a form

In yourpage.tml:

<t:form t:id="multiSubmitForm">
<input t:type="submit" t:id="OKButton" value="OK"/>
<input t:type="submit" t:id="CancelButton" value="Cancel"/>
<input t:type="submit" t:id="HelpButton" value="Help"/>
</t:form>


In yourpage.java:

private Class nextPage;

Object onSubmitFromMultiSubmitForm(){
return nextPage;
}

@OnEvent(value="selected", component="OKButton")
void onOKButton(){
.... code ...
nextPage = OKPage.class;
}

@OnEvent(value="selected", component="CancelButton")
void onCancelButton(){
.... code ...
nextPage = CancelPage.class;
}

@OnEvent(value="selected", component="HelpButton")
void onHelpButton(){
.... code ...
nextPage = HelpPage.class;
}

Tapestry - How to create a drop-down selection list

In yourpage.tml:

<t:form>
<input t:id="selectName" model="nameSelection" />
</t:form>


In yourpage.java:


@Property
@Persist
private String selectedName;

@Component(parameters = {"value=selectedName"})
private Select selectName;

@Property
@Persist
private SelectModel nameSelection;

.... in your code .....

List<String> names = new ArrayList<String>();
// populate
for (Iterator<String> iter = .....; iter.hasNext();){
names.add(iter.next());
}

nameSelection = TapestryInternalUtils.toSelectModel(names);

Monday, June 22, 2009

JSONArray example

Here is how to create a JSONArray on the Java side:
public JSONObject onNameChangeEvent(String name)
{
Share share = shareDAO.getShareByName(name);

JSONArray JSONAttributes = new JSONArray();
JSONAttributes.put(name);
JSONAttributes.put("Market Price");
JSONAttributes.put(share.getPrice());
JSONAttributes.put("Market Year");
JSONAttributes.put(share.getPriceYear());
JSONAttributes.put("Average Growth(%)");
JSONAttributes.put(share.getGrowthPercent());

JSONObject json = new JSONObject();
json.put("attributes", JSONAttributes);
return json;
}


Here is how to use it in the javascript side:

function onNameSelectFunction(response)
{
$('details').update(
"<strong>" + response.attributes[0] + "</strong>"
+ "<br/>"
+ response.attributes[1] + " : " + response.attributes[2]
+ "<br/>"
+ response.attributes[3] + " : " + response.attributes[4]
+ "<br/>"
+ response.attributes[5] + " : " + response.attributes[6]
+ "<br/>"
+ "<br/>"
);
}

Tapestry + Hibernate + Jetty + JUnit: How to use a different database for testing?

Assuming your directory structure is based on the standard tapestry-maven project, as created using this method:
http://tapestryjava.blogspot.com/2009/01/using-maven-to-create-new-tapestry-51.html


here is how to tell junit to use a separate database (so it does not interfere with your development database)

1. Make a copy of the hibernate.cfg.xml to your src/test/resources
2. Point it to a different (test) database:

<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/nesteggtest</property>

*** IF you just stop here, your development will use the hibernate.cfg.xml from your test, simply because of the default classpath! ***

** To make your development use the correct hibernate.cfg.xml, do this:

3. Assuming you are using Eclipse and run-jetty-run: go to Run->OPen Run Dialog

4. Select your run-jetty-run configuration

5. Go to classpath and add your target/WEB-INF directory (this will contain the hibernate.cfg.xml for the development)

6. Move this directory (target/WEB-INF) to the top (so that it is the first one to be looked in during development)

Tapestry + Spring + Hibernate setting up project for Eclipse

FOLLOW THE FIVE INSTRUCTIONS BELOW

1. Follow the instructions here to create a base Tapestry project:
http://tapestryjava.blogspot.com/2009/01/using-maven-to-create-new-tapestry-51.html

2. Here is a pom.xml for:
Tapestry + Spring + Hibernate + JUnit + TestNG + Chenillekit + MySQL + Jetty

Modify your own pom.xml accordingly

Then create an eclipse project by typing the command:
cmd> mvn eclipse:eclipse

You can then open the project in eclipse.

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ikangaroo</groupId>
<artifactId>NestEgg</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<name>NestEgg Tapestry 5 Application</name>
<dependencies>
<dependency>
<groupId>org.apache.tapestry</groupId>
<artifactId>tapestry-core</artifactId>
<version>${tapestry-release-version}</version>
</dependency>
<!-- A dependency on either JUnit or TestNG is required, or the surefire plugin (which runs the tests)
will fail, preventing Maven from packaging the WAR. Tapestry includes a large number
of testing facilities designed for use with TestNG (http://testng.org/), so it's recommended. -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>5.8</version>
<classifier>jdk15</classifier>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>2.4</version>
<scope>test</scope>
</dependency>

<!-- tapestry-test will conflict with RunJettyRun inside Eclipse. tapestry-test brings in Selenium, which
is based on Jetty 5.1; RunJettyRun uses Jetty 6.
<dependency>
<groupId>org.apache.tapestry</groupId>
<artifactId>tapestry-test</artifactId>
<version>${tapestry-release-version}</version>
<scope>test</scope>
</dependency>

-->

<!-- Tapestry Spring integration -->
<dependency>
<groupId>org.apache.tapestry</groupId>
<artifactId>tapestry-spring</artifactId>
<version>5.0.18</version>
</dependency>

<!-- Provided by the servlet container, but sometimes referenced in the application
code. -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>

<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>2.5</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5</version>
</dependency>

<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.4.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>3.3.1.ga</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.8.0.1</version>
</dependency>

<dependency>
<groupId>org.chenillekit</groupId>
<artifactId>chenillekit-tapestry</artifactId>
<version>1.0.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>(5.1,)</version>
</dependency>

</dependencies>
<build>
<finalName>NestEgg</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
<optimize>true</optimize>
</configuration>
</plugin>

<!-- Run the application using "mvn jetty:run" -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.9</version>
<configuration>
<!-- Log to the console. -->
<requestLog implementation="org.mortbay.jetty.NCSARequestLog">
<!-- This doesn't do anything for Jetty, but is a workaround for a Maven bug
that prevents the requestLog from being set. -->
<append>true</append>
</requestLog>
</configuration>
</plugin>
</plugins>
</build>

<reporting>

<!-- Adds a report detailing the components, mixins and base classes defined by this module. -->
<plugins>
<plugin>
<groupId>org.apache.tapestry</groupId>
<artifactId>tapestry-component-report</artifactId>
<version>${tapestry-release-version}</version>
<configuration>
<rootPackage>org.ikangaroo.nestegg</rootPackage>
</configuration>
</plugin>
</plugins>
</reporting>

<repositories>

<!-- This repository is only needed if the Tapestry released artifacts haven't made it to the central Maven repository yet. -->
<repository>
<id>tapestry</id>
<url>http://tapestry.formos.com/maven-repository/</url>
</repository>

<!-- This repository is only needed when the tapestry-release-version is a snapshot release. -->
<repository>
<id>tapestry-snapshots</id>
<url>http://tapestry.formos.com/maven-snapshot-repository/</url>
</repository>

<repository>
<id>codehaus.snapshots</id>
<url>http://snapshots.repository.codehaus.org</url>
</repository>
<repository>
<id>OpenQA_Release</id>
<name>OpenQA Release Repository</name>
<url>http://archiva.openqa.org/repository/releases/</url>
</repository>

<repository>
<id>chenillekit</id>
<url>http://www.chenillekit.org/mvnrepo/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>

<!-- As above, this can be commented out when access to the snapshot version
of a Tapestry Maven plugin is not required. -->
<pluginRepository>
<id>tapestry-snapshots</id>
<url>http://tapestry.formos.com/maven-snapshot-repository/</url>
</pluginRepository>


</pluginRepositories>

<properties>
<tapestry-release-version>5.2.0-SNAPSHOT</tapestry-release-version>
</properties>
</project>


3. Here is a web.xml - modify yours accordingly (it is located in src/main/webapp/web-inf)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>NestEgg Tapestry 5 Application</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry
of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>org.ikangaroo.nestegg</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.spring.TapestrySpringFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
</web-app>


4. Here is a hibernate.cfg.xml - modify your accordingly (it is located in /src/main/resources - if it is not there, create one)

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">



<hibernate-configuration>

<session-factory>

<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.username">user</property>
<property name="hibernate.connection.password">passwd</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/nestegg</property>
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.hbm2ddl.auto">create</property>
<property name="hibernate.hbm2ddl.auto">false</property>
<property name="show_sql">true</property> </session-factory>

</hibernate-configuration>


5.Here is an applicationContext.xml- modify yours accordingly (it is located in /src/main/webapp/web-inf - if it is not there, create one)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<!-- the parent application context definition for the springapp application -->



</beans>

Hibernate cascade save-update NonUniqueObject Exception

If you have, say 3 hibernate entitites related as follows:
A --> mapped to --> collection of B : one-to-many
B --> mapped to --> C: many-to-one

If you do a session.update(A) - hibernate will attempt to update all the instances of B in the collection. If two instances of B point to the same instance of C --- you will get a NonUniqueObject Exception!

This is because Hibernate does not allow two instances of the same object in the same session.

The workaround to resolve this issue is to do a session.merge(A) instead of session.update(A)

This is another reason I feel it is better to avoid using save-update.

Hibernate cascade save-update issues

Using "save-update" has some potential performance issues. For example, consider:

<hibernate-mapping>
<class name="org.ikangaroo.nestegg.entities.AssetTracker" table="ASSET_TRACKER">
<id name="id" column="ASSET_TRACKER_ID">
<generator class="increment" />
</id>

<map name="trackedAssets" table="TRACKED_ASSETS" lazy="false" cascade="save-update">
<key column="ASSET_TRACKER_ID" />
<map-key type="integer" column="ASSETS_YEAR"/>
<one-to-many class="org.ikangaroo.nestegg.entities.Assets"/>
</map>

</class>

</hibernate-mapping>

Here TrackedAssets contains a collection (Map) of Assets.

Suppose, TrackedAssets contains 10 Assets. Now I add a 11th one. And I do session.update(trackedAssets). This will cause Hibernate to generate 11 database queries to update all the 11 Assets. This is not very efficient, since the 10 have not changed at all.

As you can see, if there were 100 entries and you add 1 new one - that will cause 101 database queries!!!

I personally think it is better to not use save-update through cascade. Instead, do the save-update yourself as required in your code. That way you can do incremental updates, instead of making hibernate do full updates which are expensive.

Note that "delete" option of cascade is quite alright to use.