Monday, January 31, 2011

Preventing multiple form submits in ICEFaces by disabling commandButton

I created a form in ICEFaces 2 to upload a file. ICEFaces 2 has a new component for files, called the fileEntry (see http://wiki.icefaces.org/display/ICE/FileEntry). The old inputFile component has been removed from ICEFaces 2. Uploading a file takes a while, and I quickly noticed the user could click the "upload" button multiple times while the upload was in progress. This is obviously something you want to prevent. There is a very easy approach to this: just disable the submit button once it has been pressed. All you need to do is add this code to the commandButton tag:
onclick="this.disabled=true"
This works fine, and if you always go to a new page after the form has been submitted this is all you have to do. In most applications this unfortunately won't be the case: if something goes wrong with the file upload or if some other form components fail validation, you will want to stay on the same page, giving the user a warning message and allowing him to try again. The problem is that your commandButton is now disabled, so the user can't try again...
Re-enabling the commandButton when the action finishes is not as easy as it may seem: there's no "onAfterSubmit" action or something similar on the form. My first try was to add the disabled property with a value of "false":
disabled="false"
I thought that ICEFaces would re-evaluate the form after the submit, causing it to reset the disabled property to false. Unfortunately ICEFaces is smarter than that, and uses a technique called "partial dom updates". What this means is it checks if any of the components properties were changed in the action and it will only update those components whose properties have actually changed. This is a smart technique, since it reduces http traffic. It also means that our commandButton will not be updated, since for ICEFaces nothing changed to this component (it isn't aware of the JavaScript function we performed on it). This means we will have to trick ICEFaces into thinking a property of our commandButton has actually changed, causing it to refresh the commandButton. I choose to "abuse" the styleClass property for this use. I set it to:
styleClass="#{backingBean.randomValue}"
Then in my backing bean I implemented the getter for this value as follows:
public String getRandomValue() {
return "a" + System.currentTimeMillis();
}
This ensures we will get a different value for the styleClass property on each form submit. This will trick ICEFaces into thinking the commandButton has changed, causing it to refresh it and setting the disabled value to false, effectively re-enabling our commandButton.

I agree this approach isn't very clean, but it's the only technique I found to accomplish this in ICEFaces. If anyone knows of a better way, I'd be glad to know!

Monday, January 24, 2011

Avoiding the "javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed" error

Please note: this post focuses on the standard Java https implementation. If you are using the Apache HttpClient library, please see this post.

The SSLHandshakeException is thrown by java when the host you are trying to contact doesn't have a valid SSL certificate for that hostname. Most of the time this is very useful, since it means something on that host is wrong (the certificate has expired, the machine you're contacting is not who it is pretending to be etc...). However, in development mode you often don't want to pay for a "real" certificate, signed by a CA (certificate authority) like Verisign. You will then use a self-signed certificate, which gets rejected by java. It's for these cases that we're going to build a workaround. Please note that you should probably not use this code in a production environment. If you do, there's no reason to use https, since you're bypassing its functionality and you might just as well stick to http.

The first thing we need to do is create a custom TrustManager for SSL. SSL uses a protocol called X.509 (see http://en.wikipedia.org/wiki/X.509 for more info on this). We will build a TrustManager that trusts all servers:
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
}
};
As you can see, the checkXXXTrusted() methods throw Exceptions when something is wrong. We never throw an exception, effectively trusting all hosts.

The next thing we'll need to do is use this TrustManager on an SSLContext. An SSLContext (http://download.oracle.com/javase/1.5.0/docs/api/javax/net/ssl/SSLContext.html) is a factory class that is used to create socket factories, which in their turn create the actual ssl sockets used to communicate with the server. Here's how we do this:
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { tm }, null);
SSLContext.setDefault(ctx);

There now remains one more thing to be done: set a custom HostnameVerifier. A HostnameVerifier (http://download.oracle.com/javase/1.5.0/docs/api/javax/net/ssl/HostnameVerifier.html) is a class that makes sure the host you are contacting doesn't use a spoofed URL. We will again build a HostnameVerifier that trusts all hosts:

HttpsURLConnection conn = (HttpsURLConnection) new URL("https://serverAddress").openConnection();
conn.setHostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String paramString, SSLSession paramSSLSession) {
return true;
}
});


Again, this HostnameVerifier will trust all hosts.

Putting all our code together, the final class will look like this:
public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException {
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { tm }, null);
SSLContext.setDefault(ctx);  
HttpsURLConnection conn = (HttpsURLConnection) new URL("https://serverAddress").openConnection();
conn.setHostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String paramString, SSLSession paramSSLSession) {
return true;
}
});
conn.connect();

}

One final note: I prefer the way the Apache HttpClient library handles this. In the HttpClient library you can make a clean separation between the ssl verification logic and the code that does the actual work. This allows you to easily remove the code in the production environment or to use a switch between the development and production environment. This is much harder in the plain java version, since the code is more entangled. See this post for how to do this with the Apache HttpClient.

Saturday, January 22, 2011

ICEFaces 2.0 charts migration

Today I was upgrading an ICEFaces 1.8 / JSF1.2 project to ICEFaces 2 and JSF 2. Everything worked well, except my charts weren't displayed anymore. After snooping around in the ICEFaces code, I found out that they used a new JSF 2 mechanism to serve the chart images, called resource handlers. The problem is that the ICEFaces resource handler URL isn't handled by the JSF servlet. This means the resource handler never gets executed. To make sure it gets executed, you should add the mapping to your web.xml file as follows:
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/icefaces/*</url-pattern>
</servlet-mapping>

This will make the charts visible again. The same goes for the DataExporter component, which also doesn't work as long as you don't add this mapping.