Wednesday, 22 December 2010

Implementing a Process Timeout

I had a slightly unusual requirement to implement recently where I need to execute a command line process from Java + have it timeout if it didn't complete within a given timeframe.

Having considered a rather messy solution involving threads, joins, etc, I stumbled across the Executor framework that appeared in Java 1.5. Although the concepts seemed a little abstract at first, it provided a fairly elegant way of doing it:
public void executeCommand(String command, long timeoutInSeconds) throws IOException,
InterruptedException, ProcessException {
ExecutorService service = Executors.newSingleThreadExecutor();
Process process = Runtime.getRuntime().exec(command);
try {
Callable call = new CallableProcess(process);
Future future = service.submit(call);
int exitValue = future.get(timeoutInSeconds, TimeUnit.SECONDS);
if (exitValue != 0) {
throw new ProcessException("Process did not exit correctly");
}
}
catch (ExecutionException e) {
throw new ProcessException("Process failed to execute", e);
}
catch (TimeoutException e) {
process.destroy();
throw new ProcessException("Process timed out", e);
}
finally {
service.shutdown();
}
}

private class CallableProcess implements Callable {

private Process p;

public CallableProcess(Process process) {
p = process;
}

public Integer call() throws Exception {
return p.waitFor();
}
}

Best of all, it actually works. :-)

Monday, 26 July 2010

Spring Schemas

I ran into an interesting problem with Spring and ActiveMQ a few days ago. To make use of the ActiveMQ namespace in our Spring contexts, I had the following beans element:
  xmlns="http://www.springframework.org/schema/beans"
amq="http://activemq.apache.org/schema/core"
xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemalocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.1.0.xsd">

...

This seemed fairly sensible since we're using version 5.1.0 of ActiveMQ and there IS a schema at http://activemq.apache.org/schema/core/activemq-core-5.1.0.xsd.

However, when this was deployed to one of our test environments that does NOT have access to the internet, it fell over with the following exception:
Offending resource: class path resource [appContext.xml];
nested exception is org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException:
Line 10 in XML document from class path resource [messagingContext.xml] is invalid;
nested exception is org.xml.sax.SAXParseException: cvc-complex-type.2.4.c:
The matching wildcard is strict, but no declaration can be found for element 'amq:connectionFactory'
This led me to discover a feature that I was previously unaware of - the spring.schemas file. The documentation says this:
The properties file called 'spring.schemas' contains a mapping of XML Schema locations (referred to along with the schema declaration in XML files that use the schema as part of the 'xsi:schemaLocation' attribute) to classpath resources. This file is needed to prevent Spring from absolutely having to use a default EntityResolver that requires Internet access to retrieve the schema file.
In fact, the default spring.schemas file contains the following entries:
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
So both those URL's are mapped to schemas on the classpath, ie. Spring doesn't need to download them from the internet.

However, the spring.schemas file bundled with ActiveMQ contains these entries:
http\://activemq.org/config/1.0=activemq.xsd
http\://activemq.org/config/1.0/1.0.xsd=activemq.xsd
http\://activemq.apache.org/schema/core=activemq.xsd
http\://activemq.apache.org/schema/core/activemq-core.xsd=activemq.xsd
So although mappings are provided, the mappings are for URL's that don't actually exist. (To be honest, I can't see how/why you would use these mappings unless you were already aware of this feature!)

Nonetheless, I fixed the issue by changing the beans declaration as follows:
  xmlns="http://www.springframework.org/schema/beans"
amq="http://activemq.apache.org/schema/core"
xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemalocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">

...

Note that the URL for the ActiveMQ namespace now matches a URL in spring.schemas.

Thursday, 15 July 2010

Tapestry: Block Delegation

The following technique allows a page to inject content directly into a layout component. In our case, we're injecting metatags into the <head> element, but it could also be used for a breadcrumb trail, navigation panel, etc.

Layout.java contains this:
/*
* An odd but useful tapestry construct - note that the markup for this block is
* in the page itself - see ...
*/
@Property
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private Block metatags;
Layout.tml contains this:


This leaves the page free to implement the metatags however it sees fit. For example in AtoZ.tml we have this:

<meta name="description" content="Find all of your favourite TV shows beginning with ${initial} online."/>
<meta name="keywords" content="${initial} shows, ${initial} shows watch, watch tv online"/>
Just remember you need to define the 'p' namespace as being xmlns:p="tapestry:parameter".

Monday, 10 May 2010

Ubuntu 10.04 : GRUB 2 Issue

I should start by saying that the latest Ubuntu is GREAT. The startup time puts Windows to shame (what exactly is Windows doing for all that time?) and wireless connectivity works flawlessly.

However, I got an unpleasant surprise when I installed it on my Acer laptop. The some reason, GRUB 2 mis-detected my Windows partitions and thought that the hidden recovery partition was the main Windows installation. When I attempted to boot into this partition, I got an unfamiliar Acer screen and, worse still, the Ubuntu partition was completely destroyed! (By completely destroyed I mean that GParted showed the partition as being unallocated.)

After re-installing Ubuntu and reading the GRUB 2 documentation, I came up with the following solution:

1. Disable the os-prober - this is the script that detects the windows partitions and adds them to grub.cfg. You can do this by simply adding the following option to /etc/default/grub:

GRUB_DISABLE_OS_PROBER="true"

2. Put a modified version of the windows menu entries in /etc/grub.d/40_custom - see here for more details.

3. Run update-grub - this recreates the grub.cfg file. In fact, grub.cfg is updated every time there's an update and that's why you should never edit it by hand.

Dual boot restored!

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.