Thursday, 27 November 2014

Certificates with SHA-1 and SunCertPathBuilderException

As SHA-1 is heading to deprecation as hashing algoritm for certificate signatures, unpleasant effects start to appear.

Our partners we need to communicate with over HTTPS have brand new certificate signed by GoDaddy Certificate Authority. Accessing their https secured site via browser does not show anything alarming.

But accessing REST endpoint hosted on same site using java HttpUrlConnection blows up with javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

What is going on?

Every internet browser comes with quite a big set of preinstalled Certificate Authorities (CA) trusted certificates, because your browser's vendors trusts them. And because they are CAs and they are trusted, then every certificate that is signed by them is trusted too.

Same story with JVM. There is truststore file inside every JVM named cacerts. In Oracle jdk1.7.0_67 there is 87 Certificate Authorities in it as they are trused by Oracle. GoDady is there too, so why that SunCertPathBuilderException? Let's examine it more closely.

Every JVM is also shipped with command line tool named keytool. Using it you can list and also modify any keystore in jks format such as cacerts Executing... (default password is changeit)

 
keytool -list -v -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storepass changeit | grep -A 14 godaddy
...will print following...
Alias name: godaddyclass2ca
Creation date: 20-Jan-2005
Entry type: trustedCertEntry

Owner: OU=Go Daddy Class 2 Certification Authority, O="The Go Daddy Group, Inc.", C=US
Issuer: OU=Go Daddy Class 2 Certification Authority, O="The Go Daddy Group, Inc.", C=US
Serial number: 0
Valid from: Tue Jun 29 18:06:20 BST 2004 until: Thu Jun 29 18:06:20 BST 2034
Certificate fingerprints:
  MD5:  91:DE:06:25:AB:DA:FD:32:17:0C:BB:25:17:2A:84:67
  SHA1: 27:96:BA:E6:3F:18:01:E2:77:26:1B:A0:D7:77:70:02:8F:20:EE:E4
  SHA256: C3:84:6B:F2:4B:9E:93:CA:64:27:4C:0E:C6:7C:1E:CC:5E:02:4F:FC:AC:D2:D7:40:19:35:0E:81:FE:54:6A:E4
  Signature algorithm name: SHA1withRSA
  Version: 3
Now cmparing to certificate from HTTPS website... GoDaddyG2 cerificate

There is obvious mismatch. Apart from different certificate name, validity date range also notice that Signature Algorithm is "SHA-256 with RSA". GoDaddy's certificate in JVM is different from the one in use on website, therefore SunCertPathBuilderException.

To fix this, we need to add right (G2) GoDaddy's certificate into JVM cacert keystore. Visiting GoDaddy's certificate repository obvious candidate "GoDaddy Class 2 Certification Authority Root Certificate - G2" can be found there.

wget https://certs.godaddy.com/repository/gdroot-g2.crt
keytool -printcert -file gdroot-g2.crt
Will give us something we saw in website certificate...
Owner: CN=Go Daddy Root Certificate Authority - G2, O="GoDaddy.com, Inc.", L=Scottsdale, ST=Arizona, C=US
Issuer: CN=Go Daddy Root Certificate Authority - G2, O="GoDaddy.com, Inc.", L=Scottsdale, ST=Arizona, C=US
Serial number: 0
Valid from: Mon Aug 31 20:00:00 EDT 2009 until: Thu Dec 31 18:59:59 EST 2037
Certificate fingerprints:
  MD5:  80:3A:BC:22:C1:E6:FB:8D:9B:3B:27:4A:32:1B:9A:01
  SHA1: 47:BE:AB:C9:22:EA:E8:0E:78:78:34:62:A7:9F:45:C2:54:FD:E6:8B
  SHA256: 45:14:0B:32:47:EB:9C:C8:C5:B4:F0:D7:B5:30:91:F7:32:92:08:9E:6E:5A:63:E2:74:9D:D3:AC:A9:19:8E:DA
  Signature algorithm name: SHA256withRSA
  Version: 3

Now just import gdroot-g2.crt into JVM cacerts truststore

sudo keytool -import -alias godaddyg2ca -file gdroot-g2.cer -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storepass changeit

Owner: CN=Go Daddy Root Certificate Authority - G2, O="GoDaddy.com, Inc.", L=Scottsdale, ST=Arizona, C=US
Issuer: CN=Go Daddy Root Certificate Authority - G2, O="GoDaddy.com, Inc.", L=Scottsdale, ST=Arizona, C=US
Serial number: 0
Valid from: Tue Sep 01 01:00:00 BST 2009 until: Thu Dec 31 23:59:59 GMT 2037
Certificate fingerprints:
  MD5:  80:3A:BC:22:C1:E6:FB:8D:9B:3B:27:4A:32:1B:9A:01
  SHA1: 47:BE:AB:C9:22:EA:E8:0E:78:78:34:62:A7:9F:45:C2:54:FD:E6:8B
  SHA256: 45:14:0B:32:47:EB:9C:C8:C5:B4:F0:D7:B5:30:91:F7:32:92:08:9E:6E:5A:63:E2:74:9D:D3:AC:A9:19:8E:DA
  Signature algorithm name: SHA256withRSA
  Version: 3

Extensions: 

#1: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:true
  PathLen:2147483647
]

#2: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  Key_CertSign
  Crl_Sign
]

#3: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 3A 9A 85 07 10 67 28 B6   EF F6 BD 05 41 6E 20 C1  :....g(.....An .
0010: 94 DA 0F DE                                        ....
]
]

Trust this certificate? [no]:  yes
Certificate was added to keystore

Problem solved and REST call should succeed from now using this JVM

For completeness sake, if you want to get rid of it, execute

keytool -delete -alias godaddyg2ca -keystore ${JAVA_HOME}/jre/lib/security/cacerts

What in case you are not allowed to modify JVM cacerts truststore?

Then make a copy of it, import gdroot-g2.cer into it and use this custom truststore instead of default JVM truststore using -Djavax.net.ssl.trustStore=/path/to/custom_cacerts -Djavax.net.ssl.trustStorePassword=changeit java parameters

What in case you need multiple keystores or something similarily complex?

Such scenarios cannot be solved simply by using JVM switches and parameters anymore and you have to roll your own X509TrustManager implementation. Then you need to plug it into your http client connection setup - (HttpsUrlConnection SSLSocketFactory) (Apache HttpClient 3 SecureProtocolSocketFactory) (Apache HttpClient 4 SSLConnectionSocketFactory ) (Jersey SslConfigurator)

Monday, 3 November 2014

Airbrake for logback

I've you've been observing this ticket for a while and it seems to be pretty ignored. Well not anymore or at least not by me. Undramatic sources of Airbrake Logback Appender are in GitHub airbrake-logback repo

Grab it from Maven central repo

<dependency>
    <groupId>net.anthavio</groupId>
    <artifactId>airbrake-logback</artifactId>
    <version>1.0.0</version>
</dependency>

Use it...well...as usual

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="30 seconds">

    <appender name="AIRBRAKE" class="net.anthavio.airbrake.AirbrakeLogbackAppender">
        <apiKey>YOUR_AIRBRAKE_API_KEY</apiKey>
        <env>test</env>
        <enabled>true</enabled>

        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

    <root>
        <level value="info" />
        <appender-ref ref="AIRBRAKE" />
    </root>
</configuration>

Happy Logback based Airbraking

Friday, 31 October 2014

Disqus java rest api client library released

After almost year long sleep in 1.0.0-SNAPSHOT I finally released Disquo project which is java client library for Disqus REST API

It is deployed into Maven central repository with following coordinates:

    <dependency>
        <groupId>net.anthavio.disquo</groupId>
        <artifactId>disquo-api</artifactId>
        <version>1.0.0</version>
    </dependency>

It covers all Disqus v3.0 api endpoints functionality and authentication modes: OAuth 2 (access_token), Single sign-on (remote_auth), Anonymous ("Guest Commenting" must be enabled for site/forum)

To get Application keys needed for Disqus API, you should to visit Disqus and Log in or Create an Account then Register new application and grab generated "Public Key" and "Secret Key" and optionaly "Access Token"

DisqusApplicationKeys keys = new DisqusApplicationKeys("...api_key...", "...secret_key...");
//Construct Disqus API client
DisqusApi disqus = new DisqusApi(keys);
DisqusResponse<List<DisqusPost>> response = disqus.posts().list(threadId, null);
List<DisqusPost> posts = response.getResponse();
for (DisqusPost post : posts) {
  String text = post.getAuthor().getName() + " posted " + post.getMessage();
  System.out.println(text);
}
disqus.close();

More examples can be found on GitHub project page. If you find a bug, please report it to GitHub issues page

Happy Disqusing!

Thursday, 30 October 2014

Vaadin & Spring Boot & WebSockets & OpenShift & Java8

Spring Boot is around for a while and because Vaadin caught my interest lately, plus Spring4Vaadin appeared, I gave it a spin. To make it more challenging, I decided to use Java 8 and deploy it on OpenShift. All the code is hosted on GitHub

Application is simple chat with OAuth2 sign in using Facebook/Google/GitHub/LinkedIn/Disqus. High-tech is way it broadcasts chat messages to chat participants using server push which in turn uses WebSocket mechanism.

Some informations about Spring Boot and OpenShift are part of the Spring Boot Documentation and some more in this blog post.

I've created DIY gear using OpenShift application console but same can be done using rhc app create vinbudin -t diy-0.1. Application code was already on GitHub and to get it running on OpenShift, following steps must be performed.

1. Add git upstream repository (openshift remote)

git clone git@github.com:anthavio/vinbudin.git
git remote add openshift ssh://your_uuid@vinbudin-yourdomain.rhcloud.com/~/git/vinbudin.git/

2. Add OpenShift build hooks

Build hooks in .openshift/action_hooks are shell scripts that are executed when you push something into OpenShift remote repository. Typically they stop application, run maven/ant/sbt to build application and start it again.

3. Push to the openshift

That's it. When you execute following command, you will see build hooks executed and project will be built and deployed on your openshift gear.

git push openshift master

You can still git push origin master to GitHub repository without invoking build on OpenShift

Maven 3.2.3 and Java 8 on OpenShift

OpenShift gears (28 Oct 2014) have only Java 1.7.0_71 and Maven 3.0.5. If you want to use different, most probably newer versions, just download and unpack them in .openshift/action_hooks/deploy into ${OPENSHIFT_DATA_DIR}

WebSockets on OpenShift

Situation seems to be same as two years ago when this blog post was written. You still have to access port 8000 to get WebSockets working.

Thanks to Vaadin/Atmosphere push implementation which automaticaly downgrade to long-polling when websockets mechanism is unavailable, user experience is not affected, but observing messages exchanged between browser and server, you will easily spot the problem.

Open Chrome Developer Tool Network tab and navigate to http://vinbudin-openshift.anthavio.net/ui

Request URL:ws://vinbudin-openshift.anthavio.net/ui/PUSH/?v-uiId=0&v-csrfToken=731062c6-712d-4320-a92c-3742fe3b4451&X-Atmosphere-tracking-id=0&X-Atmosphere-Framework=2.1.5.vaadin4-jquery&X-Atmosphere-Transport=websocket&X-Atmosphere-TrackMessageSize=true&X-Cache-Date=0&Content-Type=application/json;%20charset=UTF-8&X-atmo-protocol=true
Request Method:GET
Status Code:501 Not Implemented

WebSocket upgrade request was rejected. Ouch!

Now doing same, but with port 8000 in url - http://vinbudin-openshift.anthavio.net:8000/ui

Request URL:ws://vinbudin-openshift.anthavio.net:8000/ui/PUSH/?v-uiId=0&v-csrfToken=731062c6-712d-4320-a92c-3742fe3b4451&X-Atmosphere-tracking-id=0&X-Atmosphere-Framework=2.1.5.vaadin4-jquery&X-Atmosphere-Transport=websocket&X-Atmosphere-TrackMessageSize=true&X-Cache-Date=0&Content-Type=application/json;%20charset=UTF-8&X-atmo-protocol=true
Request Method:GET
Status Code:101 Switching Protocols

WebSocket upgrade request was accepted and protocol was switched. Hurray!

Happy Vaadin & Spring Boot & OpenShift websocketing!

Tuesday, 16 September 2014

Spring OAuth2RestTemplate and self-signed server certificate

It might happen to you that you ended up using spring-security-oauth2 on OAuth2 client side. Personally I would not recommend to use it as it brings much more complexity into task that is not that difficult. But every use-case is diferent.

If you also happen to integrate with site using self-signed certificate, you'll inevitably encounter following exception.

org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException: Error requesting access token.
 at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:144) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:198) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:538) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:518) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:256) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
...
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://somewhere.something.info/oauth2/token":sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:558) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:511) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:136) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 ... 86 common frames omitted
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.process_record(Handshaker.java:804) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_55]
 at org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:78) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:52) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:542) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 ... 88 common frames omitted
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385) ~[na:1.7.0_55]
 at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292) ~[na:1.7.0_55]
 at sun.security.validator.Validator.validate(Validator.java:260) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323) ~[na:1.7.0_55]
 ... 102 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196) ~[na:1.7.0_55]
 at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268) ~[na:1.7.0_55]
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380) ~[na:1.7.0_55]
 ... 108 common frames omitted

Exception is throw when https://somewhere.something.info/oauth2/token is used to trade OAuth2 code for access token. You probably know what to do. Time for stupid trick with (totaly unsecure) X509TrustManager! As OAuth2RestTemplate extends RestTemplate, it inherits public void setRequestFactory(ClientHttpRequestFactory requestFactory) method and it can be used to make the trick.

private static void disableCertificateChecks(OAuth2RestTemplate oauthTemplate) throws Exception {

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { new DumbX509TrustManager() }, null);
        ClientHttpRequestFactory requestFactory = new SSLContextRequestFactory(sslContext);

        //This is for OAuth protected resources
        oauthTemplate.setRequestFactory(requestFactory);
}

Code for SSLContextRequestFactory and DumbX509TrustManager is gisted here

Now, if you try to run through test once again, you will still get same SunCertPathBuilderException again. Why?

Answer is in hidden in bowels of OAuth2AccessTokenSupport, which is base class of AuthorizationCodeAccessTokenProvider. To cut the story short, it is creating it's own RestTemplates for token endpoint operations.

Luckily again, overridable setRequestFactory(...) is also provided on OAuth2AccessTokenSupport. Repeating same trick again finaly gives us working, exception free solution:

    private static void disableCertificateChecks(OAuth2RestTemplate oauthTemplate) throws Exception {

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { new NastyX509TrustManager() }, null);
        ClientHttpRequestFactory requestFactory = new SSLContextRequestFactory(sslContext);

        //This is for OAuth protected resources
        oauthTemplate.setRequestFactory(requestFactory);

        //AuthorizationCodeAccessTokenProvider creates it's own RestTemplate for token operations
        AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider();
        provider.setRequestFactory(requestFactory);
        oauthTemplate.setAccessTokenProvider(provider);
    }

Remember, you've just created huge security hole in your application. Make doubly sure it is never used in production

Happy unsecure https REST OAuth2 calls!

PS: if you favour more advanced http transport layer then basic Java HttpURLConnection (and you should on server side) like HttpComponents 4.3, then simplest possible ClientHttpRequestFactory creation would be:

        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
        CloseableHttpClient httpClient = HttpClients.custom().setSslcontext(sslContext).build();
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

Wednesday, 4 June 2014

Netflix OSS github repo is full of goodies

If you haven't browse through Netflix OSS github repositories yet, you should do it immediately. It would be very surprising if you could not find something useful there.

I've found three projects (Feign, Hystrix, Archaius) that address directly the same problem that I've been building similar library to solve. Many others are inspirational or interesting at least, like RxJava quite popular in "reactive circles".

Some of them may have (better?) alternatives like Retrofit, some other are used as bricks in interesting projects like Halfpipe.

Apparently everybody is going crazy about Spring Boot. Let's hope it will not share destiny with Spring Roo.

Tuesday, 29 April 2014

Fluent Builder method ordering

Classic simple fluent builder usually suffers from some annoyances.

Let's look a this example:
ComplexClass cc = ComplexClass.Builder()
  .addThis(42).setThat("I'm that").addSomethingOther("I'm other")
  .addYetAnother("yet yet yet").mixPudding(true).setChickenFeedingDevice(device)
  .addThis(99).withTimeout(5000).setThat("I'm another that").build()
Disregarding silly and inconsistent method naming...
  • large number of builder methods confuse user
  • builder method can be mistakenly called multiple times

Having way to enforce order into method chaining will allow to build something more like wizzard or workflow which wiil simplify Builder usage greatly.

Let's introduce some interfaces according following rules
  • Any interface declares only subset of Builder methods
  • Interface method return value is another interface instead of Builder instance
  • Builder itself implements all interfaces

Together this basicaly forms very simple example of formal grammar where order in interface chaining represents production rules

To demonstrate idea just described, I built Selenium2/WebDriver WaitBuilder. It uses WbBegin interface as initial building point, WbAnd interface allowing to add multiple multiple conditions and finaly WbEnd with .seconds(int seconds) method instead of traditional .build().

Selenium has WebDriverWait class allowing conditional waits, which is very useful for testing pages, where elements appear dynamicaly or to perform assertion of Post/Redirect/Get (redirect after form submission) in time-boxed manner. SeleniumWaitBuilder allows to combine multiple conditions together.

It enables to write such cool chains such as...
//pass test if "results-table" element will appear in 5 seconds, fail otherwise
SeleniumWaitBuilder.with(driver)
 .passOn().element(By.id("results-table")).seconds(5);

//pass test if title become "Example Domain" and in 5 seconds or fail immediately if it happen to contain "error" string
SeleniumWaitBuilder.with(driver).passOn()
  .title().equals("Example Domain")
 .and().failOn()
  .title().contains("error")
 .seconds(5);

SeleniumWaitBuilder.with(driver).passOn()
  .title().endsWith("Example Domain")
  .element(By.id("result-table"))
 .and().failOn()
  .title().contains("error")
  .url().contains("/500")
 .seconds(5);

And finally, hero of today's blog post - mighty SeleniumWaitBuilder itself!

I admit that this is overkill for most of the builders but still, it is neat...

Happy contitional waiting!