Monday, 12 April 2010

Java XPath

I was pleasantly surprised when I needed to do some XPath in Java recently - it's been a while since I did any and I was expecting some potentially tedious 3rd party integration. Turns out it's now all included in the standard JDK and extracting the data we need from an online resource that looks like this ...








...

Was simply a case of doing this ...
// get connection to items service
URL itemsUrl = new URL(ITEMS_URI);
URLConnection itemsConnection = itemsUrl.openConnection();

// use xpath to extract the item ID's
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse(itemsConnection.getInputStream());
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression xpathExpression = xpath.compile("/guides/r/a[@n=\"ItemId\"]/@v");

// create the list of item ID's
List<Long> itemIds = new ArrayList<Long>();
Object result = xpathExpression.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
String itemId = nodes.item(i).getNodeValue();
try {
itemIds.add(Long.parseLong(itemId));
} catch (NumberFormatException e) {
System.out.println("Could not parse item ID: " + itemId);
}
}

return itemIds;
I'm probably being more specific than I need to be there, but I'm not 100% sure what the finished format will look like.

Friday, 12 February 2010

OneToMany Mapping

Sounds trivial, but it recently came to my attention that the OneToMany mappings in our app looked like this ...
@OneToMany(targetEntity = ChildItem.class, mappedBy = "parentId")
@NotFound(action = NotFoundAction.IGNORE)
public List<ChildItem> getChildItems() {
return childItems;
}
This worked (for a while) but it makes the assumption that the foreign key in the 'many' table is mapped to the primary key in the 'one' table. Of course, this caused us no end of grief when we discovered that the parent ID is non-unique and changed the table to include a real ID.

In fact, a more standard approach looks like this:
@OneToMany
@JoinColumn(name = "PARENT_ID", referencedColumnName = "PARENT_ID")
@NotFound(action = NotFoundAction.IGNORE)
public List<ChildItem> getChildItems() {
return childItems;
}
The JoinColumn annotation is basically saying "join these tables by mapping the PARENT_ID in the child items table to PARENT_ID in the parent items table".

Saturday, 21 November 2009

Ajax in Tapestry (Pt 2)

In my last blog entry, I outlined how to take control of Ajax in Tapestry and pass data into our event handler as a request parameter - useful perhaps, but not very exciting!

In this entry I'm going to build on this knowledge to create an insert zone. By default Tapestry will entirely replace the content of a Zone when we update it - what if we want to add more content to it, eg. add more items to a list? Although this feature is supported by Prototype's insert method, Tapestry doesn't expose it - what we need is an insert zone! Let's start with the following TML:

  • ${item}



more items


Here we've bound a unordered list to a Tapestry zone and inside that we're looping through a collection of items. The component class (not shown) is extremely similar to the class described in the previous blog entry - the only real difference is that we're now dealing with a list of items.

Next we need an event handler that looks like this:
@Inject
private Request request;

Object onMoreItemsEvent() {
int pageNumber = Integer.parseInt(request.getParameter("page"));
setCurrentPage(pageNumber); // this pulls in the next page of items
return listZone.getBody();
}

And now we get to the JavaScript. First of all we need an initMoreItems function:
Tapestry.Initializer.initMoreItems = function(element, zoneId, url) {
element = $(element);
$T(element).zoneId = zoneId;
// add a property to the element - we increment this each time the user clicks 'more items'
element.nextPage = 1;
element.observe("click", function(event) {
Event.stop(event);
var zoneObject = Tapestry.findZoneManager(element);
if (!zoneObject) return;
new Ajax.Request(url, {
method: 'get',
parameters: { "page" : element.nextPage },
onException: Tapestry.ajaxFailureHandler,
onFailure: Tapestry.ajaxFailureHandler,
onSuccess : function (transport) {
zoneObject.processReply(transport.responseJSON, true);
element.nextPage++;
}
});
});
}

Again, extremely similar to the previous blog entry, but here we're using a nextPage property to store our page numbers and we're also passing an insert parameter to the processReply function.

At this point we have a working component ... sort of! Problem is, it doesn't actually do what we set out to do, ie. add further content to the div. To get round this, we need to override the Tapestry functions that actually do the update - processReply + show in Tapestry.ZoneManager. We do this by using JavaScript's prototype keyword:
Tapestry.ZoneManager.prototype.processReply = function(reply, insert) {
Tapestry.loadScriptsInReply(reply, function() {
// In a multi-zone update, the reply.content may be blank or missing.
reply.content && this.show(reply.content, insert);
// zones is an object of zone ids and zone content that will be present
// in a multi-zone update response.
Object.keys(reply.zones).each(function (zoneId) {
var manager = Tapestry.findZoneManagerForZone(zoneId);
if (manager) {
var zoneContent = reply.zones[zoneId];
manager.show(zoneContent, insert);
}
});
}.bind(this));
}

Tapestry.ZoneManager.prototype.show = function(content, insert) {
if (insert) {
this.updateElement.insert(content);
}
else {
this.updateElement.update(content);
}
var func = this.element.visible() ? this.updateFunc : this.showFunc;
func.call(this, this.element);
this.element.fire(Tapestry.ZONE_UPDATED_EVENT);
}

Both these functions have been copied from tapestry.js and then had an insert parameter added to them. They've been modified in a way that means they'll continue to work if called without the parameter but will add more content (by calling insert on the Prototype Element) if the insert parameter is supplied. Of course, it's the show function that we're really interested in, but we need to override processReply because that's what we're calling from our onSuccess handler.

Monday, 9 November 2009

Controlling Ajax in Tapestry

The standard way to do Ajax in Tapestry is with Zones, EventLinks and occasionally Blocks. This can result in a very simple TML file:



Hello ${name}!

Hello World!




Hello Sam


And an equally simple Java class:
@InjectComponent
private Zone helloZone;

@Property
private String name;

Object onHelloEvent(String name) {
this.name = name;
return helloZone.getBody();
}

public boolean isNamePopulated() {
return (name != null) && (name.length() > 0);
}

If that's all you need then job done (and not a line of JavaScript in sight!)

However, I'm finding that real world requirements often require greater control over the implementation details - typically this means we need to write our own JavaScript. Here's how we do it ...

First of all we modify the TML file to use a standard anchor in place of the EventLink:

Hello Sam


Now we need to tell JavaScript what to do with that link. But before we can start writing our JavaScript, we add the following to our Java class:
@Environmental
private RenderSupport renderSupport;

@Inject
private ComponentResources resources;

@Property
private String clientId;

void setupRender() {
clientId = renderSupport.allocateClientId(resources);
}

void afterRender() {
Link helloLink = resources.createEventLink("helloEvent", "Sam");
JSONArray parameters = new JSONArray();
parameters.put(clientId);
parameters.put(helloZone.getClientId());
parameters.put(helloLink.toAbsoluteURI());
renderSupport.addInit("initHello", parameters);
}

This makes use of two methods in Tapestry's rendering lifecycle - setupRender + afterRender. In setupRender we're getting unique client ID for this component. In afterRender we're passing some information to a JavaScript function via Tapestry's JSONArray object - note that one of these parameters is the URI of an EventLink we've created. For this to work, we also need to annotate the class:
@IncludeJavaScriptLibrary("myZone.js")

Finally we get to the JavaScript. What we need is a function that gets called when the page loads and accepts the three parameters we're passing in from the Java class. Here's what it looks like:
Tapestry.Initializer.initHello = function(element, zoneId, url) {
element = $(element);
$T(element).zoneId = zoneId;
element.observe("click", function(event) {
Event.stop(event);
var zoneObject = Tapestry.findZoneManager(element);
if (!zoneObject) return;
zoneObject.updateFromURL(url);
});
}

In fact, all that JavaScript was copied from the linkZone function in tapestry.js - we're doing exactly what Tapestry was doing, but now the JavaScript is under our control. :-)

So now we're in control of the JavaScript, what can we do with it? Pretty much anything we like as it turns out, but for now, let's pass the name in as a standard request parameter. First of all we modify our event handler to look like this:
@Inject
private Request request;

Object onHelloEvent() {
this.name = request.getParameter("name");
return helloZone.getBody();
}

And then we replace zoneObject.updateFromURL with a standard Prototype Ajax request:
// zoneObject.updateFromURL(url);
new Ajax.Request(url, {
method: 'get',
parameters: { "name" : "Sam" },
onException: Tapestry.ajaxFailureHandler,
onFailure: Tapestry.ajaxFailureHandler,
onSuccess : function (transport) {
zoneObject.processReply(transport.responseJSON);
}
});

Note that we are now using GET instead of POST and we have also exposed the onSuccess handler. This can prove very useful when we start implementing more advanced requirements.

Saturday, 24 October 2009

Tapestry Revisited

I've been doing a bit of Tapestry 5 at work recently. Have to say, I haven't been much of a fan in the past, but I am starting to see its merits. Yes, it's sometimes quite hard to do relatively simple things, but this is usually down to a lack of documentation more than anything. Once you've arrived at a solution, the end result is often clean and concise - it's getting there that's the issue!

It's very different to working with an MVC framework and requires you to look at server-side development in an entirely different way. For example, developing a functional component nearly always requires some knowledge of JavaScript / Prototype and I seem to frequently find myself looking through the source code for the Tapestry JavaScript library. Although I was uncomfortable with this at first (why would a Java developer need to understand JavaScript?) I'm now finding that I enjoy the challenge. In fact, the weird and wonderful world of JavaScript is turning out to be a more friendly place than I'd feared!

Friday, 25 September 2009

New Mac

Work have kindly given me a brand new MacBook Pro! It features the new 'Unibody Enclosure' and looks absolutely gorgeous - there's a video of it being made here.

Naturally, I spent some of last night making sure my Pygame efforts still worked on it. This pretty much forced me to try Python 2.6 + Pygame 1.9 (I had previously only tried Python 2.5 + Pygame 1.7) so I was quite pleased when it worked first time. Here's what it looks like:


This also hints at a couple of new features - coins + keys! Unfortunately, although you can now collect these items, you can't actually do anything with them yet.

Tuesday, 25 August 2009

Paying The Bills

A distinct lack of activity on the blogging front lately. This is because I've been (a) moving house and (b) actually enjoying myself at work! ;-)

As Tech Lead on a brand new project, I've been writing a web app using Spring MVC, Freemarker + Hibernate and I thought I would share a few thoughts. I should start by saying that it's been nowhere near as easy as Grails, GSP + GORM, but nonetheless, it's a big improvement on the 'bad old days' of Java web app development.

To be honest, although Spring MVC is both powerful and flexible, I've always found it a little fiddly in the past. However, this time I decided to use annotation-driven controllers and these were way easier to work with than the MultiActionController I've used before. All you need is something like this in your context:
<context:component-scan package="com.royale.sam.web.controller">
And then you can annotate your controller class like this:
@Controller
@RequestMapping(method = RequestMethod.GET)
public class PageController {

...

@RequestMapping(value={"/page.html", "/item.html"})
public ModelAndView pageHandler(HttpServletRequest request, HttpServletResponse response) {
...
}
}
And that's it! Using this technique you can group handler methods in one controller (much like you can in Grails) which makes it easier to reuse domain logic and leads to a well organised application. We found this was particularly useful when implementing handlers for our AJAX functionality.

Freemarker was also pretty good, although - possibly because it's application agnostic - we found that it didn't support a number of features that you take for granted when using JSP. (In fact, I would've used JSP, but requirements dictated that we used a templating language.) For example, we couldn't find an easy + reliable way to get the context path, so we ended up introducing a servlet filter that made this information (and more) available to every page in our app. We also steered clear of using taglibs in our Freemarker templates, preferring to write our own directives.

And Hibernate was Hibernate. Always trickier than you expect to set it up, but once it's done, you generally don't have to worry about it anymore.