Friday, December 31, 2010

Remote Observer Pattern with Publish-Subcribe via XMPP [Apache Camel Series]



Apache Camel is a powerful message routing framework that can be used to implement all common enterprise integration patterns.

I'm going to look at Remote Observer Pattern, which is understandable complex to implement using traditional techniques.

However, with Apache Camel, it becomes surprisingly simple!

Reviewing the API

I'll just expand upon the previously blogged Asynchronous Observer/Listener with Apache Camel.

Here's the Listener interface, which is still the same:

public interface InvoiceListener {
public void invoiceCreated(int id, String name);
}

Still the same exact old Listener API, to prove that Apache Camel can be unobtrusive if you want. (yes, I want it to "stay out of my API way". Let Camel be the glue.)

And the same way to call the Observer :

invoiceListener.invoiceCreated(243, "Sumba Enterprise");

Publish-Subscribe Pattern via XMPP

What I want to do is route the listener invocation from the caller through a message queue channel/topic.

Interested observers can subscribe to that topic to receive (aka "consume" in Camel-speak) events/notifications/messages.

This is called Publish-Subscribe in Enterprise Integration Patterns.
The usual way to do this would be to use JMS and a message queue server/broker such as Apache ActiveMQ.

But it doesn't have to be. Besides if you use JMS directly you'll be tied to JMS API.

With Apache Camel you're free to change the underlying routing as you wish (XMPP, JMS, direct call, etc.).

Even if you want to change to another routing framework e.g. Spring Integration it's possible since both framework support the POJO bean proxying approach.

I have at least four reasons to use XMPP:
  1. It's very easy to debug. When there are problems you can just use Pidgin or Empathy or Spark or any XMPP/Jabber client to monitor what flows inside the pipes. No need for heavy tools. Even your grandma's mobile phone can do the job.
  2. No tooling needed. Same as above. JMS/ActiveMQ may have more/better tooling but you need the tooling. This can be either strength or weakness depending on how you look at it.
  3. It's just HTTP. I have never tried it but theoretically you can use any HTTP tool to tweak inside XMPP packets since in a way it's "XML over HTTP".
  4. No fancy server needed. You can install Openfire or ejabberd, just like you can install ActiveMQ. But you don't have to, you can use Jabber network or Google Talk or Google Apps' GTalk or even Facebook Chat's XMPP support. All free to use. (If you decide to use these services make sure to comply to their terms of use.)
Note: XMPP server/network's support of XMPP conference room chat functionality may vary.

Adding Camel XMPP and JSON Serialization

To use Camel with XMPP, let's revisit our project's Gradle build script and add camel-xmpp and camel-xstream as dependencies :

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'maven'
sourceCompatibility = '1.6'
repositories {
mavenRepo urls: "file:${System.properties['user.home']}/.m2/repository", name: 'local'
mavenCentral()
}
dependencies {
compile 'org.slf4j:slf4j-api:1.6.1'
runtime 'org.slf4j:jcl-over-slf4j:1.6.1'
runtime 'ch.qos.logback:logback-classic:0.9.26'
compile 'org.apache.camel:camel-core:2.5.0'
runtime 'org.apache.camel:camel-xmpp:2.5.0'
runtime 'org.apache.camel:camel-xstream:2.5.0'
}


While it's possible to use just Camel XMPP without Xstream, in practice it's much easier if we use a serialization library.

Camel provides several providers to serialize (i.e. marshall and unmarshall) our invocation/messages over the network transport (such as XMPP).
Camel calls these providers Data Formats.

I like to use JSON because it's somewhat human-readable (at least it's programmer-readable! :-) ) and relatively compact.

XML is also okay if you prefer it, both are supported by Xstream.

Camel also supports another JSON provider called Jackson but I wasn't able to successfully marshal BeanInvocation (Camel's underlying data class for proxied bean invocation) with it, so it's Xstream for now.

Routing to XMPP

 Let's review the last routing we had :

from("direct:invoice").inOnly().to("seda:invoice.queue");
from("seda:invoice.queue").threads().bean(loggerInvoiceListener);

This is fine. To make our invocation support XMPP all we need to do is change the routes, nothing else:

final String xmppUri = "xmpp://abispulsabot@localhost/?room=abispulsa.refill&password=test";
from("direct:invoice").marshal().json().to(xmppUri);
from(xmppUri).unmarshal().json().bean(loggerInvoiceListener);

This is all that's needed. Apart from adding the dependencies that we've done above, no code needs to be changed!

I'm sure you can guess what the URI means, it tells Camel XMPP connector to create an XMPP endpoint with:
  • username: abispulsabot
  • domain: localhost
  • room: abispulsa.refill
  • password: test
I guess I didn't really have to write that, but just for the sake of assertiveness. ;-)

Note: if you want to use XML just change un/marshal().json() to un/marshal().xstream(). Simple isn't it? No fuss. ;-)

Making It Behave

The above routing is still not cool:
  1. It's single-threaded, i.e. both incoming and outgoing messages are blocking.
  2. It does not handle non-JSON messages.
So let's make it better.

Enabling Multi-threading aka Asynchronous Processing

To enable multi-threading we use the magic threads() DSL :

final String xmppUri = "xmpp://abispulsabot@localhost/?room=abispulsa.refill&password=test";
from("direct:invoice").threads().marshal().json().to(xmppUri);
from(xmppUri).threads().unmarshal().json().bean(loggerInvoiceListener);

That's it. Now each processing will use its own thread (up to a default maximum of 10 threads per thread pool, but you can configure, e.g. "threads(5)")

Logging Non-JSON Messages

So what happens when a non-JSON messages is posted to the XMPP room? Let's just log it:

final String xmppUri = "xmpp://abispulsabot@localhost/?room=abispulsa.refill&password=test";
from("direct:invoice").threads().marshal().json().to(xmppUri);
from(xmppUri).threads().choice()
.when(body().startsWith("{")).unmarshal().json().bean(loggerInvoiceListener)
.otherwise().to("log:xmpp");

We use conditional routing there, using choice() and  when() and otherwise().
And Predicates (constraint expressions) such as body().startsWith().
By the way, Camel supports Predicates with a lot more expression languages, so you won't run out of options.

You'll complain that the above routing still doesn't handle invalid JSON, but let's just be satisfied with that as my example.
I believe you can make better routes much faster than I do. ;-)

And So It Goes...

Again for the sake of completeness, I present you the entire app:

import org.apache.camel.CamelContext;
import org.apache.camel.builder.ProxyBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static Logger logger = LoggerFactory.getLogger(App.class);
private static LoggerInvoiceListener loggerInvoiceListener = new LoggerInvoiceListener();
private static InvoiceListener invoiceListener;
...
CamelContext camelContext = new DefaultCamelContext();
loggerInvoiceListener = new LoggerInvoiceListener();
final String xmppUri = "xmpp://abispulsabot@localhost/?room=abispulsa.refill&password=test";
camelContext.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:invoice").threads().marshal().json().to(xmppUri);
from(xmppUri).threads().choice()
.when(body().startsWith("{")).unmarshal().json().bean(loggerInvoiceListener)
.otherwise().to("log:xmpp");
}
});
camelContext.start();
invoiceListener = new ProxyBuilder(camelContext).endpoint("direct:invoice").build(InvoiceListener.class);
invoiceListener.invoiceCreated(243, "Sumba Enterprise");
logger.info("first invoice sent");
invoiceListener.invoiceCreated(938, "Mina Co.");
logger.info("second invoice sent");
invoiceListener.invoiceCreated(312, "Crux Market");
logger.info("third invoice sent");
try {
while (true) { // event loop so you can send messages
Thread.sleep(1000);
}
} finally {
camelContext.stop();
}

Here's an example log output:

00:57:03.280 [main] INFO  o.a.c.c.xmpp.XmppGroupChatProducer - Joined room: abispulsa.refill@conference.annafi as: abispulsabot
00:57:03.298 [main] INFO  o.a.camel.impl.DefaultCamelContext - Route: route1 started and consuming from: Endpoint[direct://invoice]
00:57:03.338 [main] INFO  o.a.c.component.xmpp.XmppConsumer - Joined room: abispulsa.refill@conference.annafi as: abispulsabot
00:57:03.338 [main] INFO  o.a.camel.impl.DefaultCamelContext - Route: route2 started and consuming from: Endpoint[xmpp://abispulsabot@localhost/?password=******&room=abispulsa.refill]
00:57:03.339 [main] INFO  o.a.camel.impl.DefaultCamelContext - Total 2 routes, of which 2 is started.
00:57:03.339 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext: camel-1) started in 16.448 seconds
00:57:03.510 [main] INFO  id.co.bippo.camelxmppredo.App - first invoice sent
00:57:03.528 [main] INFO  id.co.bippo.camelxmppredo.App - second invoice sent
00:57:03.557 [main] INFO  id.co.bippo.camelxmppredo.App - third invoice sent
00:57:03.570 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #243 name: Sumba Enterprise created
00:57:05.071 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 243/Sumba Enterprise done!
00:57:05.078 [Camel Thread 4 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #938 name: Mina Co. created
00:57:06.579 [Camel Thread 4 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 938/Mina Co. done!
00:57:06.584 [Camel Thread 5 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #312 name: Crux Market created
00:57:08.085 [Camel Thread 5 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 312/Crux Market done!
00:57:26.399 [Camel Thread 6 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #7890 name: Bippo Indonesia created
00:57:27.900 [Camel Thread 6 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 7890/Bippo Indonesia done!
00:57:41.776 [Camel Thread 7 - Threads] INFO  xmpp - Exchange[ExchangePattern:InOnly, BodyType:String, Body:You can also filter messages! :)]

Notice that in the above log I posted an invoice #7890 "Bippo Indonesia" using Pidgin XMPP client, and the application can process it successfully.
I also said "You can also filter messages! :)" that simply gets logged.

As a cool bonus I provide a screenshot. ;-)

Extra: Gradle JavaExec

To quickly run the application from the command line, Gradle provides a JavaExec task that can be used like this in build.gradle :

task(exec, dependsOn: classes, type: JavaExec) {
description = 'Run the application'
classpath = runtimeClasspath
main = 'id.co.bippo.camelxmppredo.App'
}

Extra: logback.xml Configuration File

Logback defaults to logging everything to stdout (which is much better than Log4j's default of not logging anything!)

I prefer logging only INFO level, this can be done by putting the following logback.xml in src/main/resources :

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
    </layout>
  </appender>
  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Utopian dream: I can't help but imagine that had Logback used Gradle-style configuration it would look like this: ;-)

appenders { stdConsole() }
root.level = info

Running the Example

To make it really easy for you (especially to myself, as I tend to forget things!), I've made the examples freely available as camel-xmpp-redo project on GitHub.

You'll need Git and Gradle to checkout then build it (and JDK 1.6).
Gradle will automatically download and cache the required dependencies for you.

git clone git://github.com/ceefour/camel-xmpp-redo.git
cd camel-xmpp-redo
# edit the source and change XMPP credentials
gradle exec
Note: you must change the XMPP credentials used to login.

Easy XMPP with Camel !

Apache Camel is unobtrusive way to add flexibility of routing, messaging, and integration patterns with XMPP or other connectors supported by Camel.
(you can create your own connectors if you want...)

I highly suggest the Camel in Action Book for the best in-depth guide and examples for using Camel to develop your applications more productively.

Implementing Asynchronous Observer Pattern with Bean Proxy [Apache Camel Series]

Apache Camel is a very versatile message routing and enterprise integration framework.

One very useful, practical, and reasonable use case is to implement a Listener/Observer interface that delegates to an actual implementation with a flexible exchange pattern.

So from the point of view of the Observable (event source), the Observer is just a regular "POJO" Java object.

The Listener Interface


Here's the Listener interface :

public interface InvoiceListener {
public void invoiceCreated(int id, String name);
}

What I Want

What I'd like to do is to be able to call a listener object as usual :

InvoiceListener = .....
invoiceListener.invoiceCreated(243, "Sumba Enterprise");


But have the implementation flexible. It can be a local object, a remote web service, or even routed asynchronously using message queue (ActiveMQ) or XMPP.

A nice side effect is that the programming model stays the same. And of course, it's easily testable since your implementation is still POJO.

The Implementation

This is an example implementation of the listener, that simply logs the input.
Note there can be as many implementations as required.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggerInvoiceListener implements InvoiceListener {
Logger logger = LoggerFactory.getLogger(getClass());
public void invoiceCreated(int id, String name) {
logger.info("Invoice #{} name: {} created", id, name);
// Simulate asynchronous processing by delaying
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
logger.error("Interrupted", e);
}
logger.info("{}/{} done!", id, name);
}
}

Note that I've added Thread.sleep() there, to simulate processing.

The Usual Way aka Useful for JUnit Tests

So here's the standard stuff:

InvoiceListener = new LoggerInvoiceListener();
invoiceListener.invoiceCreated(243, "Sumba Enterprise");
logger.info("first invoice sent");
invoiceListener.invoiceCreated(938, "Mina Co.");
logger.info("second invoice sent");

While this works, and very useful in unit testing, in our example it is not practical.

The Listener blocks the thread for processing and the program runs for 3000 ms for two invoices.

Not to mention that the original thread cannot do anything else while waiting for the Listener to return.

Here's how we'll fix it using Apache Camel, without changing how we call the listener at all!

Setting Up Apache Camel

I will use Gradle to set up our project and automatically manage Apache Camel dependencies and SLF4j + Logback, which I use for logging.

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'maven'
sourceCompatibility = '1.6'
repositories {
mavenRepo urls: "file:${System.properties['user.home']}/.m2/repository", name: 'local'
mavenCentral()
}
dependencies {
compile 'org.slf4j:slf4j-api:1.6.1'
runtime 'org.slf4j:jcl-over-slf4j:1.6.1'
runtime 'ch.qos.logback:logback-classic:0.9.26'
compile 'org.apache.camel:camel-core:2.5.0'
}

Maven guys can do something similar, but I increasingly prefer Gradle due to its compact syntax and flexibility.

If you're not using dependency management or still use Ant, I strongly recommend Gradle for your project build system.

It's the flexibility of Ant, can optionally use conventions like Maven, with powerful dependency management of Ivy, with Groovy's clean syntax so won't intimidate you with verbose <angle brackets>.

To create the Eclipse IDE Project we'll create the source folder and execute Gradle's "eclipse" task.

mkdir -p src/main/java src/main/resources
gradle eclipse

Configuring CamelContext

Camel routes runs inside a CamelContext. Here's the usual way to create a default CamelContext, configure routes, start the CamelContext and stop it.

CamelContext camelContext = new DefaultCamelContext();
camelContext.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
// TODO: configure routes here
});
camelContext.start();
// TODO: run your app here
camelContext.stop();

Configuring Camel routes can be done using a Java-based DSL (among other choices).

Here's a route DSL for our use case, that simply invokes a specified bean:

from("direct:invoice").bean(loggerInvoiceListener);

Camelizing Our App


With that in mind, let's see how our original application looks like:

private static Logger logger = LoggerFactory.getLogger(App.class);
private static LoggerInvoiceListener loggerInvoiceListener = new LoggerInvoiceListener();
private static InvoiceListener invoiceListener;
public static void main(String[] args) throws Exception {
invoiceListener = loggerInvoiceListener;
invoiceListener.invoiceCreated(243, "Sumba Enterprise");
logger.info("first invoice sent");
invoiceListener.invoiceCreated(938, "Mina Co.");
logger.info("second invoice sent");
}

And this is how to do the same thing, using Apache Camel :

import org.apache.camel.CamelContext;
import org.apache.camel.builder.ProxyBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static Logger logger = LoggerFactory.getLogger(App.class);
private static LoggerInvoiceListener loggerInvoiceListener = new LoggerInvoiceListener();
private static InvoiceListener invoiceListener;
public static void main(String[] args) throws Exception {
CamelContext camelContext = new DefaultCamelContext();
camelContext.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:invoice").bean(loggerInvoiceListener);
}
});
camelContext.start();
invoiceListener = new ProxyBuilder(camelContext).endpoint("direct:invoice")
.build(InvoiceListener.class);
invoiceListener.invoiceCreated(243, "Sumba Enterprise");
logger.info("first invoice sent");
invoiceListener.invoiceCreated(938, "Mina Co.");
logger.info("second invoice sent");
camelContext.stop();
}

You can see the Listener API usage has not changed at all.

Apart from the Camel runtime, the main difference lies in how we get the InvoiceListener object, which is now a proxy provided by Camel:

invoiceListener = new ProxyBuilder(camelContext).endpoint("direct:invoice")
  .build(InvoiceListener.class);

Here's the output of the Camel-ized app :

23:59:09.629 [main] INFO  o.a.c.i.c.AnnotationTypeConverterLoader - Found 3 packages with 13 @Converter classes to load
23:59:09.666 [main] INFO  o.a.c.i.c.DefaultTypeConverter - Loaded 146 type converters in 0.405 seconds
23:59:09.774 [main] INFO  o.a.camel.impl.DefaultCamelContext - Route: route1 started and consuming from: Endpoint[direct://invoice]
23:59:09.775 [main] INFO  o.a.camel.impl.DefaultCamelContext - Total 1 routes, of which 1 is started.
23:59:09.775 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext: camel-1) started in 0.546 seconds
23:59:09.788 [main] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #243 name: Sumba Enterprise created
23:59:11.289 [main] INFO  i.c.b.c.LoggerInvoiceListener - 243/Sumba Enterprise done!
23:59:11.295 [main] INFO  id.co.bippo.camelasyncredo.App - first invoice sent
23:59:11.296 [main] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #938 name: Mina Co. created
23:59:12.796 [main] INFO  i.c.b.c.LoggerInvoiceListener - 938/Mina Co. done!
23:59:12.797 [main] INFO  id.co.bippo.camelasyncredo.App - second invoice sent
23:59:12.797 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext:camel-1) is shutting down
23:59:12.798 [main] INFO  o.a.c.impl.DefaultShutdownStrategy - Starting to graceful shutdown 1 routes (timeout 300 seconds)
23:59:12.806 [Camel Thread 0 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route1 suspension deferred.
23:59:12.807 [Camel Thread 0 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route1 shutdown complete.
23:59:12.807 [main] INFO  o.a.c.impl.DefaultShutdownStrategy - Graceful shutdown of 1 routes completed in 0 seconds
23:59:12.809 [main] INFO  o.a.c.impl.DefaultInflightRepository - Shutting down with no inflight exchanges.
23:59:12.810 [main] INFO  o.a.camel.impl.DefaultCamelContext - Uptime: 3.581 seconds
23:59:12.810 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext: camel-1) is shutdown in 0.013 seconds

The app runs > 3.5 seconds, while about 400 ms is spent on Apache Camel startup. This is the minimal overhead of Camel.

I Want Them All at Once! Making It Asynchronous

A useful use case is to make the listener invocations asynchronous, i.e. performed with multiple threads. But not changing the caller code at all.
Let's see how we can do this with Camel.

All we need to do is change:

from("direct:invoice").bean(loggerInvoiceListener);

to:

from("direct:invoice").inOnly().to("seda:invoice.queue");
from("seda:invoice.queue").threads().bean(loggerInvoiceListener);

This route tells Camel to transform the original invocation to InOnly (meaning that no response/reply is needed), and send it to a SEDA Queue endpoint.

The messages in SEDA Queue will be split in multiple threads (default thread pool is 10 threads, but you can specify yourself) and each thread will invoke the destination implementation which is loggerInvoiceListener bean.

Here's how it looks right now :

00:11:44.952 [main] INFO  o.a.camel.impl.DefaultCamelContext - Route: route1 started and consuming from: Endpoint[direct://invoice]
00:11:44.954 [main] INFO  o.a.camel.impl.DefaultCamelContext - Route: route2 started and consuming from: Endpoint[seda://invoice.queue]
00:11:44.954 [main] INFO  o.a.camel.impl.DefaultCamelContext - Total 2 routes, of which 2 is started.
00:11:44.954 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext: camel-1) started in 0.529 seconds
00:11:44.969 [main] INFO  id.co.bippo.camelasyncredo.App - first invoice sent
00:11:44.970 [main] INFO  id.co.bippo.camelasyncredo.App - second invoice sent
00:11:44.970 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext:camel-1) is shutting down
00:11:44.971 [main] INFO  o.a.c.impl.DefaultShutdownStrategy - Starting to graceful shutdown 2 routes (timeout 300 seconds)
00:11:44.971 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #938 name: Mina Co. created
00:11:44.971 [Camel Thread 1 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #243 name: Sumba Enterprise created
00:11:44.974 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route2 suspension deferred.
00:11:44.974 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route1 suspension deferred.
00:11:44.974 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Waiting as there are still 2 inflight and pending exchanges to complete, timeout in 300 seconds.
00:11:45.975 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Waiting as there are still 2 inflight and pending exchanges to complete, timeout in 299 seconds.
00:11:46.471 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 938/Mina Co. done!
00:11:46.471 [Camel Thread 1 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 243/Sumba Enterprise done!
00:11:46.976 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route2 shutdown complete.
00:11:46.976 [Camel Thread 3 - ShutdownTask] INFO  o.a.c.impl.DefaultShutdownStrategy - Route: route1 shutdown complete.
00:11:46.976 [main] INFO  o.a.c.impl.DefaultShutdownStrategy - Graceful shutdown of 2 routes completed in 2 seconds
00:11:46.977 [main] INFO  o.a.c.impl.DefaultInflightRepository - Shutting down with no inflight exchanges.
00:11:46.978 [main] INFO  o.a.camel.impl.DefaultCamelContext - Uptime: 2.553 seconds
00:11:46.979 [main] INFO  o.a.camel.impl.DefaultCamelContext - Apache Camel 2.5.0 (CamelContext: camel-1) is shutdown in 2.008 seconds

You can see from the log that Camel has executed our application in multiple parallel threads, as an effect, our application runs in about 2.5 seconds
instead of ~3.5 seconds like the previous.

You can also see that the order of execution is very different now.

Synchronous invocations:

23:59:09.788 [main] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #243 name: Sumba Enterprise created
23:59:11.289 [main] INFO  i.c.b.c.LoggerInvoiceListener - 243/Sumba Enterprise done!
23:59:11.295 [main] INFO  id.co.bippo.camelasyncredo.App - first invoice sent
23:59:11.296 [main] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #938 name: Mina Co. created
23:59:12.796 [main] INFO  i.c.b.c.LoggerInvoiceListener - 938/Mina Co. done!
23:59:12.797 [main] INFO  id.co.bippo.camelasyncredo.App - second invoice sent
Asynchronous invocations, with Camel:
00:11:44.969 [main] INFO  id.co.bippo.camelasyncredo.App - first invoice sent
00:11:44.970 [main] INFO  id.co.bippo.camelasyncredo.App - second invoice sent
...
00:11:44.971 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #938 name: Mina Co. created
00:11:44.971 [Camel Thread 1 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - Invoice #243 name: Sumba Enterprise created
...
00:11:46.471 [Camel Thread 2 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 938/Mina Co. done!
00:11:46.471 [Camel Thread 1 - Threads] INFO  i.c.b.c.LoggerInvoiceListener - 243/Sumba Enterprise done!

Instead of objects waiting for the previous invocation to complete, they can run independently.

All without changing the caller code nor the interface API nor the implementation code.

What you simply do is change the Camel routes.

Running the Example

To make it really easy for you (especially to myself, as I tend to forget things!), I've made the examples freely available as camel-async-redo project on GitHub.

You'll need Git and Gradle to checkout then build it (and JDK 1.6).
Gradle will automatically download and cache the required dependencies for you.

git clone git://github.com/ceefour/camel-async-redo.git
cd camel-async-redo
gradle exec

Conclusion

Apache Camel is unobtrusive way to add flexibility of routing, messaging, and integration patterns into your Java application.
(although, Camel can be useful to seam/weave/integrate any combination of applications, even remote services, so not just Java, but that's another topic...)

I highly suggest the Camel in Action book for the best in-depth guide and examples for using Camel to develop your applications more productively.

Sunday, December 26, 2010

How to Dump/Inspect Object or Variable in Java

Scala (console) has a very useful feature to inspect or dump variables / object values :

scala> def b = Map("name" -> "Yudha", "age" -> 27)
b: scala.collection.immutable.Map[java.lang.String,Any]

scala> b
res1: scala.collection.immutable.Map[java.lang.String,Any] = Map((name,Yudha), (age,27))

Inside our application, especially in Java programming language (although the techniques below obviously works with any JVM language like Scala and Groovy) sometimes we want to inspect/dump the content of an object/value. Probably for debugging or logging purposes.

My two favorite techniques is just to serialize the Java object to JSON and/or XML. An added benefit is that it's possible to deserialize the dumped object representation back to an actual object if you want.

JSON Serialization with Jackson

Depend on Jackson (using Maven):
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.6.3</version>
</dependency>
Then use it:
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

..
Logger logger = LoggerFactory.getLogger(getClass());

@Test
public void level() throws ServiceException, JsonGenerationException, JsonMappingException, IOException {
MagentoServiceLocator locator = new MagentoServiceLocator();
Mage_Api_Model_Server_HandlerPortType port = locator.getMage_Api_Model_Server_HandlerPort();
String sessionId = port.login("...", "...");
logger.info(String.format("Session ID = %s", sessionId));
Map[] categories = (Map[]) port.call(sessionId, "catalog_category.level", new Object[] { null, null, 2 } );
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
logger.info( mapper.writeValueAsString(categories) );
}

Example output :

6883 [main] INFO id.co.bippo.shop.magentoclient.AppTest - [ {
  "position" : "1",
  "level" : "2",
  "is_active" : "1",
  "name" : "Gamis",
  "category_id" : "3",
  "parent_id" : 2
}, {
  "position" : "2",
  "level" : "2",
  "is_active" : "1",
  "name" : "Celana",
  "category_id" : "5",
  "parent_id" : 2
} ]

XML Serialization with XStream

As a pre-note, XStream can also handle JSON with either Jettison or its own JSON driver, however people usually prefer Jackson than XStream for JSON serialization.

Maven dependency for XStream:
<dependency>
<groupId>xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.2.2</version>
</dependency>
Use it:
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Map;

import javax.xml.rpc.ServiceException;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.thoughtworks.xstream.XStream;
...
@Test
public void infoXml() throws ServiceException, RemoteException {
MagentoServiceLocator locator = new MagentoServiceLocator();
Mage_Api_Model_Server_HandlerPortType port = locator.getMage_Api_Model_Server_HandlerPort();
String sessionId = port.login("...", "...");
logger.info(String.format("Session ID = %s", sessionId));
Map category = (Map) port.call(sessionId, "catalog_category.info",
new Object[] { 3 } );
XStream xstream = new XStream();
logger.info( xstream.toXML(category) );
}

Sample output:

5949 [main] INFO id.co.bippo.shop.magentoclient.AppTest - <map>
  <entry>
    <string>position</string>
    <string>1</string>
  </entry>
  <entry>
    <string>custom_design</string>
    <string></string>
  </entry>
  <entry>
    <string>custom_use_parent_settings</string>
    <string>0</string>
  </entry>
  <entry>
    <string>custom_layout_update</string>
    <string></string>
  </entry>
  <entry>
    <string>include_in_menu</string>
    <string>1</string>
  </entry>
  <entry>
    <string>custom_apply_to_products</string>
    <string>0</string>
  </entry>
  <entry>
    <string>meta_keywords</string>
    <string>gamis, busana muslim</string>
  </entry>
  <entry>
    <string>available_sort_by</string>
    <string></string>
  </entry>
  <entry>
    <string>url_path</string>
    <string>gamis.html</string>
  </entry>
  <entry>
    <string>children</string>
    <string></string>
  </entry>
  <entry>
    <string>landing_page</string>
    <null/>
  </entry>
  <entry>
    <string>display_mode</string>
    <string>PRODUCTS</string>
  </entry>
  <entry>
    <string>level</string>
    <string>2</string>
  </entry>
  <entry>
    <string>description</string>
    <string>Gamis untuk muslimah</string>
  </entry>
  <entry>
    <string>name</string>
    <string>Gamis</string>
  </entry>
  <entry>
    <string>path</string>
    <string>1/2/3</string>
  </entry>
  <entry>
    <string>created_at</string>
    <string>2010-12-24 11:37:41</string>
  </entry>
  <entry>
    <string>children_count</string>
    <string>0</string>
  </entry>
  <entry>
    <string>is_anchor</string>
    <string>1</string>
  </entry>
  <entry>
    <string>url_key</string>
    <string>gamis</string>
  </entry>
  <entry>
    <string>parent_id</string>
    <int>2</int>
  </entry>
  <entry>
    <string>filter_price_range</string>
    <null/>
  </entry>
  <entry>
    <string>all_children</string>
    <string>3</string>
  </entry>
  <entry>
    <string>is_active</string>
    <string>1</string>
  </entry>
  <entry>
    <string>page_layout</string>
    <string></string>
  </entry>
  <entry>
    <string>image</string>
    <null/>
  </entry>
  <entry>
    <string>category_id</string>
    <string>3</string>
  </entry>
  <entry>
    <string>default_sort_by</string>
    <null/>
  </entry>
  <entry>
    <string>custom_design_from</string>
    <null/>
  </entry>
  <entry>
    <string>updated_at</string>
    <string>2010-12-24 11:37:41</string>
  </entry>
  <entry>
    <string>meta_description</string>
    <string>Jual baju gamis untuk muslim</string>
  </entry>
  <entry>
    <string>custom_design_to</string>
    <null/>
  </entry>
  <entry>
    <string>path_in_store</string>
    <null/>
  </entry>
  <entry>
    <string>meta_title</string>
    <string>Gamis</string>
  </entry>
  <entry>
    <string>increment_id</string>
    <null/>
  </entry>
</map>

Which one is better?

I personally prefer JSON, but fortunately, you always have a choice. :-)

Maven 3.0 does not support *Maven* 1.0 (legacy) repository

Today I was frustrated by this :

Maven 3.x no longer supports repositories using <layout>legacy</layout>. Users that need to access repositories created with Maven 1.x are advised to use a repository manager that is capable of providing a Maven 2.x compatible view of the legacy repository.

WHAT???

Sonatype could simply change their suggestion to "Users that need to access repositories created with Maven 1.x are advised to use Apache Ivy."

I understand the need to force users to use the new technology, but not by disabling access to older technology without providing a practical alternative. At least wait for your competitors to stop supporting your legacy product before you yourself do it.

java.net Maven 1 repository contains artifacts that I need at times, now I have to hunt the artifacts or do workarounds that was simply solved using repository layout=legacy. Argh!

Thursday, December 23, 2010

Writing JAX-RS REST API Server with Jersey / GlassFish v3

Writing REST API Web Service application in Java EE 6 is now trivial with JAX-RS API and Jersey (the reference implementation of JAX-RS, bundled with GlassFish v3). I'll show you how, with raw real code. ;-)

Requirements

  1. Eclipse IDE, pick the Java EE edition. Latest version is 3.6 SR1 (Helios)
  2. JBoss Tools plugin for Eclipse IDE (probably optional)
  3. A Java EE 6 Server like GlassFish v3 or JBoss AS 6 (not GA yet)

Create the Web (WAR) Project


Create a new Dynamic Web project in Eclipse.

You'll need to choose a Java EE container runtime, it will download the libraries ("Java EE 6 SDK") as well.

Implement the Resource Class


Create a new JAX-RS resource class. What makes it a resource class is you sprinkle it with javax.ws.rs.Path annotation.

I use JBoss Tools and GlassFish server plugin for Eclipse so either or both of them adds the Java EE libraries to my build path, including the most important  here is jsr311-api.jar. If you use Maven you can depend on javax.ws.rs:jsr311-api:1.1.1 artifact.
A sample resource class is like below. Note that I split my resource class to interface and implementation class.

package com.abispulsa.bisnis.service;

import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

import org.eclipse.emf.ecore.EObject;

/**
* <!-- begin-user-doc -->
* A representation of the model object '<em><b>Refiller</b></em>'.
* <!-- end-user-doc -->
*
* <!-- begin-model-doc -->
* Refill the target mobile number by some amount.
*
* This is the public facing REST API.
* <!-- end-model-doc -->
*
* <p>
* The following features are supported:
* <ul>
*   <li>{@link com.abispulsa.bisnis.service.Refiller#getSession <em>Session</em>}</li>
* </ul>
* </p>
*
* @see com.abispulsa.bisnis.service.ServicePackage#getRefiller()
* @model
* @generated
*/
public interface Refiller extends EObject {

/**
* <!-- begin-user-doc -->
* This actually only queues it. The real work is done in the queue processor job.
* <!-- end-user-doc -->
* @model
* @generated
*/
@Path("refill")
@POST
String refill(@FormParam("mobileNumber") String mobileNumber, @FormParam("voucherCode") String voucherCode);

} // Refiller

You may notice I'm using the mighty EMF to design the resource class. Yes, that's right! ;-)
(ok, so that's the excuse for me for posting this article to EclipseDriven, I'm hoping people will be curious "what's EMF? sounds cool!" hahahaha)

Ok so here's the actual resource class :

package com.abispulsa.bisnis.service.impl;

import javax.ws.rs.Path;

import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.impl.ENotificationImpl;
import org.eclipse.emf.ecore.impl.EObjectImpl;

import com.abispulsa.bisnis.service.Refiller;
import com.abispulsa.bisnis.service.ServicePackage;
import com.abispulsa.bisnis.service.Session;

/**
* <!-- begin-user-doc -->
* An implementation of the model object '<em><b>Refiller</b></em>'.
* <!-- end-user-doc -->
* <p>
* The following features are implemented:
* <ul>
*   <li>{@link com.abispulsa.bisnis.service.impl.RefillerImpl#getSession <em>Session</em>}</li>
* </ul>
* </p>
*
* @generated
*/
@Path("/")
public class RefillerImpl extends EObjectImpl implements Refiller {

/**
* <!-- begin-user-doc -->
* Make it public constructor for used in JAX-RS.
* We should wrap it inside application and use CDI though.
* <!-- end-user-doc -->
* @generated NOT
*/
public RefillerImpl() {
super();
}


/**
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @generated NOT
*/
public String refill(String mobileNumber, String voucherCode) {
return String.format("MobileNumber=%s voucherCode=%s", mobileNumber, voucherCode);
...
}

private void checkAuthentication() {
// TODO Auto-generated method stub

}

...

} //RefillerImpl

There are several things to notice here:

  1. The implementation resource class is @Path annotated. Of course, since you can't instantiate an interface!
  2. It's possible to "split" JAX-RS annotations between interface, class, interface methods and class method. In my example, the interface defines the the REST API and specific structure of the service. However, the implementation can decide where to put the service itself using @Path. The implementation do not override any of the JAX-RS annotations (REST API structure) defined by the interface.
  3. A public constructor is needed (EMF generated default constructor is protected, there is a reason why), because Jersey will instantiate the resource class directly. It's possible to implement javax.ws.rs.Application so you can instantiate your resource classes yourself and configure Jersey for that. More on this later.

Configuring web.xml for Jersey


The last thing is you need to configure the Jersey servlet in your web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

<servlet>
<servlet-name>ServletAdaptor</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>com.abispulsa.bisnis.service.impl</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>ServletAdaptor</servlet-name>
<url-pattern>/api/v1/*</url-pattern>
</servlet-mapping>

<session-config>
<session-timeout>30</session-timeout>
</session-config>

</web-app>

The web.xml above tells the Java EE 6 Container (i.e. GlassFish v3) to:

  1. Create a Jersey servlet at URL path: /api/v1
  2. Load JAX-RS resource classes from my package:
com.abispulsa.bisnis.service.impl com.sun.jersey.config.property.packages init-param contains a comma-separated list of package names (so you can list multiple packages there). Note that this is optional. I use it because my Web/WAR project doesn't actually contain these packages, but the packages are provided by another JAR that I included in WEB-INF/lib. If your resource classes are inside the WAR itself (i.e. built to WEB-INF/classes) you can leave out that configuration and Jersey will find your classes just fine.

Deploy the Web Project


Deploying the app into GlassFish v3 outputs the following log:

INFO: Scanning for root resource and provider classes in the packages:
  com.abispulsa.bisnis.service.impl
INFO: Root resource classes found:
  class com.abispulsa.bisnis.service.impl.RefillerImpl
INFO: No provider classes found.

INFO: GlobalStatsProvider registered
INFO: Initiating Jersey application, version 'Jersey: 1.1.5 01/20/2010 04:04 PM'
INFO: Adding the following classes declared in META-INF/services/jersey-server-components to the resource configuration:
  class com.sun.jersey.multipart.impl.FormDataMultiPartDispatchProvider
  class com.sun.jersey.multipart.impl.MultiPartConfigProvider
  class com.sun.jersey.multipart.impl.MultiPartReader
  class com.sun.jersey.multipart.impl.MultiPartWriter

INFO: Loading application com.abispulsa.bisnis.server.rest at /apbrest
INFO: Instantiated an instance of org.hibernate.validator.engine.resolver.JPATraversableResolver.

INFO: com.abispulsa.bisnis.server.rest was successfully deployed in 8.179 milliseconds.

Testing the JAX-RS REST API Server with curl

Ok so now l want to test it, using curl is my favorite (in Ubuntu/Debian you can just sudo apt-get install curl), but you can use any tool that understands HTTP requests.

ceefour@annafi:~/project/AbisPulsa/workspace/com.abispulsa.bisnis.service$ curl -v --data-urlencode mobileNumber=08123456 --data-urlencode voucherCode=A5 'http://localhost:8080/apbrest/api/v1/refill'

* About to connect() to localhost port 8080 (#0)
*   Trying ::1... connected
* Connected to localhost (::1) port 8080 (#0)
> POST /apbrest/api/v1/refill HTTP/1.1
> User-Agent: curl/7.21.0 (i686-pc-linux-gnu) libcurl/7.21.0 OpenSSL/0.9.8o zlib/1.2.3.4 libidn/1.18
> Host: localhost:8080
> Accept: */*
> Content-Length: 36
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
< X-Powered-By: Servlet/3.0
< Server: GlassFish Server Open Source Edition 3.0.1
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Date: Thu, 23 Dec 2010 09:34:31 GMT
<
* Connection #0 to host localhost left intact
* Closing connection #0
MobileNumber=08123456 voucherCode=A5

Alright! So I guess my application works! ;-)

Pretty straightforward, huh?

Using your Own javax.ws.rs.core.Application Implementation


As mentioned before, Jersey will create your resource classes.

If you want to control instantiation of your classes, you can create class that implements javax.ws.rs.core.Application.

After that you configure it in your web.xml like this:

<web-app> <servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>MyApplication</param-value> </init-param> </servlet>     ....
I haven't tried it yet, but I think one probable use case is if you want to use Dependency Injection (CDI) in your resource classes.......... ALTHOUGH GlassFish already does this for you (you can use JAX-RS and CDI at the same time), so you probably only use Application for this purpose if the container doesn't support integration between these specs.

Say Goodbye to web.xml


Another, more valid use case and I guess more common if you want to be "pure Java EE 6" is to leave out web.xml altogether:
JAX-RS 1.1 offers a @ApplicationPath annotation applicable to javax.ws.rs.core.Application which let you specify the webcontext and remove the need for any web.xml

References

Wednesday, December 22, 2010

JSF is not Perfect. But let's see how can we can make it better over time

Max from Exadel (the original creator of JBoss Tools) has written a great reply of the ten points that Bruno Borges complained about JSF.

I agree with all of Max's points, and disagree with most of Bruno's views (although technically they’re valid complaints).

However I don’t agree with Max's “In conclusion, JSF is not perfect, no framework is.” The purpose of a comparison is not to declare whether some framework is perfect or that some framework is all bad.

It’s to analyze its strong points relative to another framework(s) which have weaker areas. I would love to know despite these 10 ‘hate points of JSF’, there should be some hate points JSF still does better (i.e. “yeah it’s bad, but others are worse”) than some other framework.

Also to analyze its weak points (in the case of JSF which is the subject here), also relative to other framework(s) which can do better. I guess this is the most useful.

For example, referring to point 10 “the web is fast, standards are slow” I think the recent standards somewhat admit this, and provide mechanism to solve this “problem.”

RichFaces, IceFaces, PrimeFaces, the versatile PrettyFaces, and the Seam project is a testimony that the standard itself (be it JSF 1.2 or JSF 2.0) is not the the only thing that matters, innovation can still (and will always) happen elsewhere. It just happens that the JSF is the core functionality that we can kind of rely on.

The CDI spec addresses precisely this “standards are slow” problem. It does not try to be everything, but it embraces everything that can be developed on it (CDI Extensions).

Monday, December 20, 2010

Eclipse RAP Single Sourcing Awesomeness (with EMF Editor and Teneo+Hibernate as bonus!)

Eclipse Rich Client Platform has come a looong way since it was first introduced (and used in Eclipse IDE). The new Eclipse RAP (Rich Application Platform) is also becoming more and more attractive for deploying existing or new Eclipse RCP applications to the web.

One of my the projects I'm working on is developed on top of Eclipse RCP. It uses additional plugins such as EMF (Eclipse Modeling Framework) including EMF Editor UI, Teneo (EMF Persistence for Relational Databases), and Hibernate.

After some work, I managed to run the whole application on both Eclipse RCP (desktop) and Eclipse RAP (web-based). See the screenshots for proof.

Thanks to the recently released EMF Support for RAP I don't have to let go any of the nice EMF generated editor UIs for the web-based RAP version.

What's amazing is how little the work I have to do to port the RCP app to RAP.

The changes I needed to do is not changing code, but juggling dependencies to plugins and/or packages. Also creating a few platform-specific plugins (different based on whether I deploy on RCP or RAP).

It boils down to:

  1. Do not hard-depend on org.eclipse.ui plugin. Either depend on both org.eclipse.ui and org.eclipse.rap.ui plugins as optional dependencies, or import the specific packages. I prefer optional dependency on both plugins because it's much faster and easier.
  2. Be aware that there will be multiple sessions at once.
  3. 2D Drawing functions are not yet fully available. (and I guess will never be available)

See the Eclipse RAP FAQ on Single Sourcing for more information.

Fixing error: The type org.eclipse.core.runtime.IAdaptable cannot be resolved. It is indirectly referenced from required .class files.

If you get one of the following errors :

The type org.eclipse.core.runtime.IAdaptable cannot be resolved. It is indirectly referenced from required .class files.

The type org.eclipse.core.runtime.CoreException cannot be resolved. It is indirectly referenced from required .class files.

First of all, check that your plugins depend on org.eclipse.core.runtime plugin.

The classes above are located in org.eclipse.equinox.common plugin, and should be included (along with org.eclipse.core.runtime plugin) in your target platform.

If it still occurs, most likely you get your target platform plugins mixed up. I got this error because I tried to mix plugins from my Eclipse IDE installation (Helios 3.6-SR-1) with Eclipse 3.7M4 Platform plugins.

Simply unchecking the plugins from target platform Contents tab is not enough. I have to actually remove the location. If you notice duplicate plugins in your Target Platform Definition's Contents tab, then you're getting this problem.

Remove the offending plugins location from the target platform definition. If you must add plugins from Eclipse IDE location, cherry pick each plugin from the Locations UI, instead of just adding the whole SDK and unchecking them from the Contents tab.

Sunday, December 19, 2010

Creating an About Dialog for your Eclipse RCP Application

Displaying an About box in your Eclipse RCP Application/Product is actually very simple. However as is typical of a framework ("don't call us, we'll call you" principle), there are conventions you must follow.

Before you do this, you must already create an Eclipse RCP Application class that implements IApplication and register it as an Eclipse extension in your plugin.xml.

Adding the Help > About Menu Item


First, edit your ApplicationActionBarAdvisor class (which extends org.eclipse.ui.application.ActionBarAdvisor base class), as follows:

    protected void makeActions(IWorkbenchWindow window) {
        aboutAction = ActionFactory.ABOUT.create(window);
        register(aboutAction);
    }

That will register the About action. You will also need to add the menu action itself to the menu bar:

    protected void fillMenuBar(IMenuManager menuBar) {
        MenuManager helpMenu = new MenuManager("&Help", IWorkbenchActionConstants.M_HELP);
       
        menuBar.add(helpMenu);
        helpMenu.add(aboutAction);
    }

Launch your Eclipse RCP application and you can display the About dialog.

Customizing the About Dialog Box


To customize the About dialog box's contents, first you must create an Eclipse RCP Product Configuration.
Click File > New > Product Configuration and define your product.

This should also add your product to extension point 'org.eclipse.core.runtime.products'.

Edit your project's Launch Configuration to use the your Product Configuration instead of Eclipse Application. Verify that it works well.

Now you can customize the About box contents by editing your Product Configuration (.product file), go to Branding tab and About Dialog section. Specify the About Image and About Text there.

To specify the image, import an image resource (PNG, GIF, JPG) to your plugin project (e.g. inside an /icons folder). Important: Your image will need to be included inside your resulting plugin binary. Edit your plugin's manifest, go to Build tab, and make sure your images/icons are included (checked) for the binary build and source build.

After editing your Product Configuration, you must synchronize it with the plugin manifest. Go to the Product's Overview tab and click Synchronize (under Testing).

That action will update several properties in the org.eclipse.core.runtime.products extension, for example:

   <extension
         id="abispulsa_rcp"
         point="org.eclipse.core.runtime.products">
      <product
            application="com.abispulsa.bisnis.rcp.application"
            description="Layanan bagi korporasi untuk dapat dengan mudah mengisi pulsa."
            name="AbisPulsa Bisnis RCP">
         <property
               name="appName"
               value="AbisPulsa Bisnis RCP">
         </property>
         <property
               name="aboutText"
               value="AbisPulsa Bisnis merupakan layanan bagi korporasi untuk dapat dengan mudah mengisi pulsa.">
         </property>
         <property
               name="aboutImage"
               value="icons/AbisPulsa_icon_75x75.png">
         </property>
      </product>
   </extension>

For your information: Other properties you can use include:

  • windowImages
  • aboutImage
  • aboutText
  • appName
  • welcomePage
  • preferenceCustomization

References:
  1. http://help.eclipse.org/helios/index.jsp?topic=/org.eclipse.platform.doc.isv/guide/product_def_extpt.htm
  2. http://help.eclipse.org/helios/index.jsp?topic=/org.eclipse.platform.doc.isv/reference/extension-points/org_eclipse_core_runtime_products.html

Fixing Eclipse RCP Launch Error: Application "org.eclipse.ui.ide.workbench" could not be found in the registry.

If you encounter the following error message when launching your Eclipse RCP application/plugin :

!ENTRY org.eclipse.osgi 4 0 2010-12-20 00:49:08.433
!MESSAGE Application error
!STACK 1
java.lang.RuntimeException: Application "org.eclipse.ui.ide.workbench" could not be found in the registry. The applications available are: org.eclipse.ant.core.antRunner, org.eclipse.jdt.core.JavaCodeFormatter, org.eclipse.help.base.infocenterApplication, org.eclipse.help.base.helpApplication, org.eclipse.help.base.indexTool, com.abispulsa.bisnis.rcp.application, org.eclipse.equinox.app.error.
at org.eclipse.equinox.internal.app.EclipseAppContainer.startDefaultApp(EclipseAppContainer.java:248)
at org.eclipse.equinox.internal.app.MainApplicationLauncher.run(MainApplicationLauncher.java:29)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:110)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:79)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:369)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:179)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:619)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:574)
at org.eclipse.equinox.launcher.Main.run(Main.java:1407)
at org.eclipse.equinox.launcher.Main.main(Main.java:1383)

It means that you haven't added the following plugin to your target platform / enabled plugins:

org.eclipse.ui.ide.application

That means you'll use the default "workbench" application as used by Eclipse IDE.

When you've created your own application class which implements org.eclipse.equinox.app.IApplication interface, you need to register an Eclipse extension to org.eclipse.core.runtime.applications in your plugin.xml like the following example:

   <extension id="application"
         point="org.eclipse.core.runtime.applications">
      <application>
         <run class="com.abispulsa.bisnis.rcp.Application">
         </run>
      </application>
   </extension>

Then edit your Eclipse Application launch configuration to use your own application class.

Mirroring an Eclipse Update Site to a Local p2 Repository

Installing or updating Eclipse plugins/features is easy using Eclipse p2 Update Sites. However, since it requires downloading from the Internet the process is often very slow.

Some projects provide an archived update site (.zip file) but the rest do not provide them. When installing or updating features for multiple Eclipse IDE or RCP Application installations, downloading the same files multiple times from the Internet can get annoying. Not to mention it definitely wastes precious time AND bandwidth.

Thankfully there is a way to create a local p2 repository that acts as a mirror site to the original Eclipse p2 Update Sites.

This is useful for making available a full Eclipse release or a set of Eclipse features/plugins to internal corporate users, for example, reducing the bandwidth normally used with dozens of users downloading the same bits from external Eclipse p2 Update sites.

Documentation


First, you need to mirror the site (or a particular feature), so take a look at the mirror command described here:
Running Update Manager from Command Line

then create a site policy (a type of redirection) as described here:
Controlling the Eclipse Update Policy

Command Examples


You can start the update manager in a standalone mode to create a mirror of an update site by using
this command:

java -Dhttp.proxyHost=yourProxy -Dhttp.proxyPort=yourProxyPort \
  -jar plugins/org.eclipse.equinox.launcher_<version>.jar \
  -application org.eclipse.update.core.standaloneUpdate -command mirror \
  -from %updateSiteToMirror% -mirrorUrl %urlOfYourUpdateSite% \
  -to %fileLocationToMirrorTo%

Run this command from Eclipse install directory (i.e. where startup.jar is).
Replace %fileLocationToMirrorTo% with the local directory where the update site contents will be copied to, and %urlOfYourUpdateSite% with a URL which will point to the directory you informed.

Of course you will need to install a local web server, like Apache HTTPD and configure it according the directory/URL you specified before.

It even supports to create one mirror-site of multiple sites if you specify the same location for multiple sites it will append them to the site.xml giving you one big (and messy) update site.

An easy way to use this is use a dos or bash scipt ofcourse. For example the following script to mirror the relevant update sites:

set LAUNCHER=C:\opt\springsource-2.1\sts-2.1.0.RELEASE\plugins/plugins/org.eclipse.equinox.launcher_1.0.200.v20090520.jar

call updateSite http://subclipse.tigris.org/update_1.6.x subclipse
call updateSite http://pmd.sourceforge.net/eclipse pmd
call updateSite http://m2eclipse.sonatype.org/update/ m2eclipse
call updateSite http://findbugs.cs.umd.edu/eclipse/  findbugs
call updateSite http://moreunit.sourceforge.net/org.moreunit.updatesite/  moreunit
call updateSite http://www.springsource.com/update/e3.5 sprinsource-e35
call updateSite http://eclipse-cs.sf.net/update checkstyle
call updateSite http://update.atlassian.com/atlassian-eclipse-plugin atlassian
call updateSite http://commonclipse.sourceforge.net commonclipse
call updateSite https://ajax.dev.java.net/eclipse glassfish
call updateSite http://andrei.gmxhome.de/eclipse/ gmx-plugins
call updateSite http://regex-util.sourceforge.net/update/ regex
call updateSite http://ucdetector.sourceforge.net/update/ ucdetector

goto:eof

:updateSite
java -Dhttp.proxyHost=yourProxy -Dhttp.proxyPort=yourProxyPort -jar %LAUNCHER% -application org.eclipse.update.core.standaloneUpdate -command mirror -from %1 -mirrorUrl http://server/eclipseupdatesite/%2 -to Y:\%2 &goto:eof
goto:eof
This gives us multiple update sites under http://server/eclipseupdatesite/ like http://server/eclipseupdatesite/m2eclipse etc. Of course you still need 1 computer to have unrestricted/fast internet access, but you can always create those sites at home.

Aggregating Specific Features

You can also aggregate several features from other update sites to your own, using either p2.mirror, p2 Composite Repositories, b3, or Nexus Pro.

See http://stackoverflow.com/questions/4378112/p2-repositories-aggregator


Sources:

  1. http://dev.eclipse.org/newslists/news.eclipse.platform/msg29529.html
  2. http://stackoverflow.com/questions/4378112/p2-repositories-aggregator
  3. http://www.willianmitsuda.com/2007/03/09/mirroring-callisto-update-site/
  4. http://www.denoo.info/2009/09/mirroring-eclipse-update-sites/

Mirroring an Eclipse Update Site to a Local p2 Repository

Installing or updating Eclipse plugins/features is easy using Eclipse p2 Update Sites. However, since it requires downloading from the Internet the process is often very slow.

Some projects provide an archived update site (.zip file) but the rest do not provide them. When installing or updating features for multiple Eclipse IDE or RCP Application installations, downloading the same files multiple times from the Internet can get annoying. Not to mention it definitely wastes precious time AND bandwidth.

Thankfully there is a way to create a local p2 repository that acts as a mirror site to the original Eclipse p2 Update Sites.

This is useful for making available a full Eclipse release or a set of Eclipse features/plugins to internal corporate users, for example, reducing the bandwidth normally used with dozens of users downloading the same bits from external Eclipse p2 Update sites.

Documentation


First, you need to mirror the site (or a particular feature), so take a look at the mirror command described here:
Running Update Manager from Command Line

then create a site policy (a type of redirection) as described here:
Controlling the Eclipse Update Policy

Command Examples


You can start the update manager in a standalone mode to create a mirror of an update site by using
this command:

java -Dhttp.proxyHost=yourProxy -Dhttp.proxyPort=yourProxyPort \
  -jar plugins/org.eclipse.equinox.launcher_<version>.jar \
  -application org.eclipse.update.core.standaloneUpdate -command mirror \
  -from %updateSiteToMirror% -mirrorUrl %urlOfYourUpdateSite% \
  -to %fileLocationToMirrorTo%

Run this command from Eclipse install directory (i.e. where startup.jar is).
Replace %fileLocationToMirrorTo% with the local directory where the update site contents will be copied to, and %urlOfYourUpdateSite% with a URL which will point to the directory you informed.

Of course you will need to install a local web server, like Apache HTTPD and configure it according the directory/URL you specified before.

It even supports to create one mirror-site of multiple sites if you specify the same location for multiple sites it will append them to the site.xml giving you one big (and messy) update site.

An easy way to use this is use a dos or bash scipt ofcourse. For example the following script to mirror the relevant update sites:

set LAUNCHER=C:\opt\springsource-2.1\sts-2.1.0.RELEASE\plugins/plugins/org.eclipse.equinox.launcher_1.0.200.v20090520.jar

call updateSite http://subclipse.tigris.org/update_1.6.x subclipse
call updateSite http://pmd.sourceforge.net/eclipse pmd
call updateSite http://m2eclipse.sonatype.org/update/ m2eclipse
call updateSite http://findbugs.cs.umd.edu/eclipse/  findbugs
call updateSite http://moreunit.sourceforge.net/org.moreunit.updatesite/  moreunit
call updateSite http://www.springsource.com/update/e3.5 sprinsource-e35
call updateSite http://eclipse-cs.sf.net/update checkstyle
call updateSite http://update.atlassian.com/atlassian-eclipse-plugin atlassian
call updateSite http://commonclipse.sourceforge.net commonclipse
call updateSite https://ajax.dev.java.net/eclipse glassfish
call updateSite http://andrei.gmxhome.de/eclipse/ gmx-plugins
call updateSite http://regex-util.sourceforge.net/update/ regex
call updateSite http://ucdetector.sourceforge.net/update/ ucdetector

goto:eof

:updateSite
java -Dhttp.proxyHost=yourProxy -Dhttp.proxyPort=yourProxyPort -jar %LAUNCHER% -application org.eclipse.update.core.standaloneUpdate -command mirror -from %1 -mirrorUrl http://server/eclipseupdatesite/%2 -to Y:\%2 &goto:eof
goto:eof
This gives us multiple update sites under http://server/eclipseupdatesite/ like http://server/eclipseupdatesite/m2eclipse etc. Of course you still need 1 computer to have unrestricted/fast internet access, but you can always create those sites at home.

Aggregating Specific Features

You can also aggregate several features from other update sites to your own, using either p2.mirror, p2 Composite Repositories, b3, or Nexus Pro.

See http://stackoverflow.com/questions/4378112/p2-repositories-aggregator


Sources:

  1. http://dev.eclipse.org/newslists/news.eclipse.platform/msg29529.html
  2. http://stackoverflow.com/questions/4378112/p2-repositories-aggregator
  3. http://www.willianmitsuda.com/2007/03/09/mirroring-callisto-update-site/
  4. http://www.denoo.info/2009/09/mirroring-eclipse-update-sites/

Making Software Literate: The Parser, The Interpreter, and The Literal

In my last post, the title suggests "teaching code to read itself". However all I wrote was about Interpreter.

For code to read itself, it must be able to generate a model from itself. Therefore, you need the metamodel of the programming lanaguage the software is written in, and the grammar of that language. By using those two ingredients and the proper tools, you can generate a model from the software.

An example of a comprehensive tool for doing this is Eclipse MoDisco. It comes with a complete tooling for discovering / reflecting / introspecting a Java project.

However, for software to understand the model, it must also have an Interpreter/Evaluator, which can do something with the model.

In a way, a generator is a specialized kind of interpreter which simply outputs the concrete representation ("artifact") of the model being processed.

To not only read a model but also to make changes to it, we need a Manipulator (what a scary name), which is a kind of interpreter that performs actions on a model. Sample action: delete an EClass node named 'Car'.

After making changes, the resulting model can be generated back to file artifacts using generator. The project can then be rebuilt, only the changed artifacts are needed.

To rebuild the project from scratch though, we need a complete set of project files.

A typical software project, not only consists of a single language (hence metamodel) but several other artifacts including:
- build.xml (Ant build)
- plugin.xml, MANIFEST.MF (PDE project)
- pom.xml (Maven project)
- build.gradle (Gradle build)
- .project, .classpath, build.properties (Eclipse JDT project)

Depending on requirements, it may not be needed (sometimes not even desirable) to model all of those artifacts properly. Sometimes, it's enough to model a file as a 'Literal':

File: EClass
-----------------------
name: EString
directory: EString
contents: EString

Which in practice means, that these artifacts are not part of the model-driven lifecycle at all. (i.e. You can actually ignore it and it won't evenmatter)

Model-driven is all about transformation, or processing, or shapeshifting, or (meta)morphing. If an artifact or model stays the same throughout the lifecycle, and it's not being used as a metamodel for transformation of its instances, then it's the same thing as if modeling is not used at all.

When all project artifacts are understood, 'literalled', or generated, the project can be rebuilt from scratch using the model. With a good headless build system such as Maven or Gradle, this should be simple.

The other part to include is the information "in programmer's head". We deal with this everyday that it seldom occurs to us that it *is* information.

Things like:
- the directory location of the project
- project name
- location and name of the generated binaries
- SCM user, password, URL
- SCM tool
- test status (whether the tests pass, how many tests, how many fails, how many passes)

These information should be modeled, and a specialized interpreter can be created to act on the project.

A final behavior is 'replaceSelf', which requires the following information:
1. self source location
2. self binary location
3. self descriptor model
4. location of *this* self descriptor model
5. prototype source location
6. prototype binary location
7. prototype descriptor model

where 'prototype' is the project that we've discussed and built above.

The replaceSelf behavior, given the 'self descriptor model' will update/replace itself using the prototype locations, and also update the self descriptor model (e.g. Update the version number).

If the software runs continuously (as a server/daemon), it can then fork a new version of itself, then terminate its own instance.

I guess now the lifecycle is complete. ;-)

 
Copyright 2009 Spring vs Java EE Web Dev. Powered by Blogger Blogger Templates create by Deluxe Templates. WP by Masterplan