Saturday, May 21, 2011

Tapestry 5 on Jetty with MongoDB Part 2



If you haven't already read the previous Tutorial then I would suggest to: http://blog.boehme.me/2011/05/tapestry-51-on-jetty-with-mongodb-part.html

What does the second part cover?

Overview
  • Learn how to use DTO's in order to access mongoDB
  • Write your own ORM for better control
  • get deeper into mongoDB documents handling, i.e.accesing embedded documents etc.
Let's get started

While using native BSON objects in order to save/ read documents in mongoDB we want now to use custom DTO's to have more control.

To continue with the previous example we want to create users in a better way now. Therefore we create a new package "dto". Within this package we create a new Class "User".

User.java
package de.boehme.app.tapestry_mongodb.dto;

import java.io.Serializable;

import com.mongodb.BasicDBObject;

public class User extends BasicDBObject implements Serializable{

/**
*
*/
private static final long serialVersionUID = 8856991851254657487L;

private String username;
private Integer age;
private Address address;

public String getUsername() {
return (String) get("username");
}

public void setUsername(String username) {
put("username", username);
this.username = username;
}

public Integer getAge() {
return (Integer) get("age");
}

public void setAge(Integer age) {
put("age", age);
this.age = age;
}

public void setAddress(Address address) {
put("address", address);
this.address = address;
}

public Address getAddress() {
return (Address) get("address");
}

}


What do theses lines of code mean?
Well the first thing to mention is that we are now a subclass of BasicDBObject, which in fact allows us later on to save this DTO straight to a collection without converting or anything.

Then we need a way to save data to a document and also read data back from it. Therefore we added to the getters and setters "put" and "get" to save and retrieve data.

That's all we need to configure a POJO to be a mongoDB document.

Now we have to change our MongoDBHandler class to use the newly created User.class.
Go to MongoDBHandler.java and add following methods:
Note: I rather replaced the old methods

public void createUser(User user){
DBCollection coll = db.getCollection("userDTOCollection");
coll.setObjectClass(User.class);
coll.insert(user);
}

public List getAllUsers(){
List result = new ArrayList();
DBCollection coll = db.getCollection("userDTOCollection");
DBCursor cur = coll.find();
while(cur.hasNext()) {
User user = (User) cur.next();
result.add(user);
}
return result;
}




What do theses lines of code mean?

We now store directly a User Object into the collection. This gives a main advantage over storing just a String as we did before,
because now we are able to inlcude nearly anything into the object. Note that we have to call setObjectClass on the collection, to let it know which Type gets stored. I think the getAllUsers Method is self-explanaining.

Note: You also have to adjust the interface IMongoDBHandler to apply to the new methods!
Note2: You cannot use the same colllection from the first Example because we want to store now different Objects! Otherwise you get com.mongodb.BasicDBObject cannot be cast to de.boehme.app.tapestry_mongodb.dto.User

Refactoring the UserAdmin Page

The changes I did are marked green:

public class UserAdmin {

@Inject
private IMongoDBHandler mongoDB;

@Property
private String userName;
@Property
private Integer age;

@Property
private List<User> users;
@Property
private User user;

void onActivate(){
users = mongoDB.getAllUsers();
}

Object onSuccess(){
user = new User();
user.setUsername(userName);
user.setAge(age);
mongoDB.createUser(user);
users = mongoDB.getAllUsers();
return this;
}

}


Now modify the UserAdmin.tml


<html t:title="tapestry_mongodb Useradmin"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
xmlns:p="tapestry:parameter">

<h1>Create User</h1>
<form t:type="form">
<t:errors />
Username: <input t:type="textfield" t:id="userName"/>
Age: <input t:type="textfield" t:id="age"/>
<input type="submit" value="Create User" />
</form>

<h1>User List</h1>
<t:loop source="users" value="user">
<h2>User: ${user.userName} Age: ${user.age}</h2>
</t:loop>
</html>



As you can see there are only little changes needed.
Now go navigate your browser to http://localhost:8080/useradmin and you will see following:

With this technique you have a kind of a ORM that helps you in developing new Object structures accessing mongoDB.

Diving deeper into mongoDB documents - embedded documents

What about having embedded documents in one document? Well, this part is not as obvious as one could imagine.

Normally you would just reference to other DTO's by doing this way:

public class User {

private Address address;
//getters setters
}

public class Address{

private String street;
private Integer houseNumber;
private String city;
//getters setters
}



The problem with the mongo-java-driver is that it cannot automatically convert those documents (e.g. a BasicDBObject) into your "Address" object.

When referencing Address in a page you would instantly get a com.mongodb.BasicDBObject cannot be cast to de.boehme.app.tapestry_mongodb.dto.Address Exception.

That's why we have to convert the embedded document first into an usable object.
Put this into your MongoDBHandler.java class:

@SuppressWarnings({ "unchecked" })
private <T extends BasicDBObject> T convertToDTO(BasicDBObject dbObject, Class<T> clazz) {
Object result = null;
try {
Class<T> cl = (Class<T>) Class.forName(clazz.getName());
result = cl.newInstance();
Class<?>[] par=new Class[1];
par[0]=Map.class;
Method m=cl.getMethod("putAll",par);
m.invoke(result, dbObject.toMap());
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return (T) result;
}

What is this method basically doing? It gets the original unconverted document and the target class as input. Then via reflection API we just copy the complete content by calling "putAll" to the newly created object which in turn is now converted and ready to get used.

As this kind of work is tedious when having multiple embedded documents, I created a more advanced method that searches through one document for embedded documents and recursively converts them. That means if one embedded document also has embedded docuements they also get converted and so on.

Add following method to MongoDBHandler.java


@SuppressWarnings("unchecked")
private synchronized <T extends BasicDBObject> T searchForEmbeddedDocuments(BasicDBObject dbObject){
Class<T> documentClass = (Class<T>) dbObject.getClass();
Method[] methods = dbObject.getClass().getMethods();
//check each method if it returns a basicdbobject and thus convert this embedded document
for(Method m : methods){
//now check for return type:
//Embedded document return type class
Class<T> returnTypeClass = (Class<T>) m.getReturnType();
if(returnTypeClass.getSuperclass()!= null && returnTypeClass.getSuperclass().equals(BasicDBObject.class)){
//found embedded document
// System.out.println("Embedded doc at: " m.getName());
//now invoke map get method and get return object
try {
Class<?>[] par=new Class[1];
par[0]=String.class;
Method invokingMethod = returnTypeClass.getMethod("get",par);
//extract the getter methods name // THIS WORKS ONLY IF THE NAMING CONVENTION IS FOLLOWED
//THUS: private String myAttribute; results in the getter method: public String getMyAttribute(){return myAttribute;}
String invokingMethodName = m.getName().substring(3).toLowerCase();
BasicDBObject embeddedDocument = (BasicDBObject) invokingMethod.invoke(dbObject, invokingMethodName);
// System.out.println("Embedded: " embeddedDocument.getClass().getName());
/**STILL TODO:*/ //now search also this embedded document for further embedded documents
searchForEmbeddedDocuments(embeddedDocument);
T converted = convertToDTO(embeddedDocument, returnTypeClass);
// System.out.println(converted.getClass());
//now set/ put the newly converted dto to the existing document
par=new Class[2];
par[0]=String.class;
par[1]=Object.class;
invokingMethod = documentClass.getMethod("put",par);
invokingMethod.invoke(dbObject, new Object[]{invokingMethodName, converted});
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
return (T) dbObject;
}

The code itself is commented so you should understand whats happening there.

Applying the new methods

Changes in getAllUsers() method:
public List<User> getAllUsers(){
List<User> result = new ArrayList<User>();
DBCollection coll = db.getCollection("userEmbeddedCollection");
DBCursor cur = coll.find();
while(cur.hasNext()) {
User user = (User) cur.next();
user.setAddress(convertToDTO((BasicDBObject)user.get("address"), Address.class));
result.add(user);
}
return result;
}


Changes made to the Page UserAdmin.java and UserAdmin.tml

UserAdmin.java
package de.boehme.app.tapestry_mongodb.pages;

import java.util.List;

import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;

import de.boehme.app.tapestry_mongodb.dto.Address;
import de.boehme.app.tapestry_mongodb.dto.User;
import de.boehme.app.tapestry_mongodb.services.IMongoDBHandler;

public class UserAdmin {

@Inject
private IMongoDBHandler mongoDB;

@Property
private String userName;
@Property
private Integer age;
@Property
private String street;
@Property
private Integer houseNumber;
@Property
private String city;

@Property
private List<User> users;
@Property
private User user;

void onActivate(){
users = mongoDB.getAllUsers();
}

Object onSuccess(){
user = new User();
user.setUsername(userName);
user.setAge(age);

Address address = new Address();
address.setCity(city);
address.setHouseNumber(houseNumber);
address.setStreet(street);
user.setAddress(address);

mongoDB.createUser(user);
users = mongoDB.getAllUsers();
return this;
}

}



UserAdmin.tml

<html t:title="tapestry_mongodb Useradmin"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
xmlns:p="tapestry:parameter">

<h1>Create User</h1>
<form t:type="form">
<t:errors />
Username: <input t:type="textfield" t:id="userName"/>
Age: <input t:type="textfield" t:id="age"/>
<br/>
Street: <input t:type="textfield" t:id="street"/>
House Number: <input t:type="textfield" t:id="houseNumber"/>
City: <input t:type="textfield" t:id="city"/>
<input type="submit" value="Create User" />
</form>

<h1>User List</h1>
<t:loop source="users" value="user">
<h2>User: ${user.userName} Age: ${user.age} <br/>
Address: ${user.address.street} ${user.address.houseNumber} ${user.address.city}
</h2>
</t:loop>
</html>


The Result


Again, if you want to have the full source code you can download the maven project from here:
Source: tapestry_mongodb_orm.zip


Remarks: If you still encounter errors as com.mongodb.BasicDBObject cannot be cast to de.boehme.app.tapestry_mongodb.dto.XYZ then you should delete old mongoDB database files from your dbpath. Link

1 comment: