Posts Tagged java

FitNesse and OSGi

As a demonstrator for a customer, I recently built a set of fixtures that allow FitNesse acceptance tests to talk to an OSGi framework. This code is by no means production quality, but merely intended to show the concept and explain the challenges.

I will not explain the details of the acceptance tests here, however, if there’s one point I would like to get across, it’s your fixtures should be as narrow as possible to easily accommodate for implementation changes. Study the different UserAdmin fixtures for more details. Also, I assume some familiarity with OSGi.

FitNesse and OSGi. Why?

Of course its fun, there is some real benefit to be gained here. While the industry well understands the need for unit- and integration testing, also in a modular context, it becomes more complex to create the necessary link between business and code. Yes, using a modular architecture we can behave in a more agile fashion, but all that agility is no good if the business doesn’t hop on the train, and explain well what it needs. FitNesse allows the business to explain its goals in business-lingo, while forcing the specification to be precise enough to be executable: if a concept cannot be explained by simple scenarios, something is wrong, but that’s a different story.

The modular nature of OSGi means that behavior of an application is more emergent than deterministic, making it harder to reason about its correctness: we can prove that our code and bundles are correct (unit tests), that everything works together as it should (integration tests), and that it looks right (user interface tests). However, proving that the business rules (which may well be one of those emergent properties) are handled correctly in a given setting, is another can of worms: we need to connect our acceptance tests to the OSGi framework.

The big picture

FitNesse and OSGi - overview

The solution presented below uses a special ‘fixtures’ bundle, which can be deployed along side other bundles in your framework. This bundles exposes an interface (in our case, through an HTTPServlet), which is used by a set of connectors, which in turn are used by FitNesse.

The details

FitNesse and OSGi - detailed

The ingredients are two parts connector code, one part boiler plate, and one part genuine OSGi-aware fixtures.

The connectors

Starting at the level closest to FitNesse, we find a set of fixtures that FitNesse can use. For us, these contain merely boiler plate code.

public void removeUser(String name) throws Exception {
    doRemoteCall(buildRemoteCall("UserAdmin", name), Void.class);
}

This code instructs our RemoteInvoker to do some call to the outside world. For more details, see RemoteInvoker.java in the UserAdminRemoteFixtures project.

The fixture bundle

Moving one step closer to our service, and into the OSGi framework, we find a FixtureServlet, whose task it is to receive calls from the RemoteInvoker, and turn them into actual method calls on the fixtures.

The fixtures, then, are almost regular OSGi aware objects. I chose to use the Apache Felix Dependency Manager for the dependency management of the fixtures. So, for our UserAdmin fixture, the dependencies are

manager.add(createService()
    .setInterface(UserAdminListener.class.getName(), null)
    .setImplementation(userAdmin)
    .add(createServiceDependency()
        .setService(UserAdmin.class)
        .setRequired(true)));

Here, we state that we have some instance of a fixture userAdmin that registers itself as a UserAdminListener and needs a UserAdmin. How straightforward is that?

The final step takes us to the actual fixture,

public class UserAdminFixture implements UserAdminListener {
	private volatile UserAdmin m_userAdmin;
...
	public void addUser(String name) {
		m_usersCreatedInLastCall = 0;
		m_userAdmin.createRole(name, Role.USER);
	}
...
}

which is just another component using a the UserAdmin service.

Putting it all together

All we now need to do is deploy the fixture bundle in our project, and instruct FitNesse to use the remote connector. The zip file at the bottom of this post contains two shell scripts to do exactly that.

Future work

As I stated at the top of this story, this is by no means production quality code, but the concepts stand as they are. Given the way FitNesse works, the connectors do not need much extra work, perhaps support for collections. However, we could use

  • a way to reduce the boiler plate code,
  • a way to ensure that that both side of the fixtures use the same function naming, and
  • better integration, for instance by only firing up a framework once a FitNesse suite is started.

Let’s play with it!

I have built a zip file containing everything you need to get started, including a set of scenarios that can run with both a homebrew implementation of a User Admin, and the actual Apache Felix User Admin. A Readme gives you more information on getting it all up and running.

, , , ,

1 Comment

Online video: Beyond OSGi software architecture

Op 11 november 2009 gaf Marcel Offermans samen met Jeroen van Grondelle (Be Informed) een duo presentatie over “Beyond OSGi software architecture”. De video van deze presentatie is inmiddels online beschikbaar. De gehele presentatie van 50 minuten kan je hier downloaden. Maar de eerste tien minuten zijn hieronder gelijk online te bekijken.

OSGi is niet meer weg te denken uit het Enterprise Java domein. Dit lightweight framework krijgt al een aantal jaren flinke aandacht en is met name bekend om z’n modulaire applicaties op basis van bundles. Minder bekend is het services model, waarbij applicaties worden ontwikkeld op basis van service interfaces en van elkaar ontkoppelde implementaties (POJO’s) van die interfaces. Krachtige eigenschappen van dit model zijn:

  • complexiteitsreductie door stricte scheiding van services;
  • declaratieve services en dependency management (IoC);
  • aspect oriëntatie op basis van stub services;
  • security model.

Lean software is een nieuwe manier om enterprise applicaties te bouwen op basis van OSGi, die ook goed aansluit bij Agile methoden, waarbij non-functional requirements ingevuld kunnen worden in een compact gebleven framework. In die context kijken we naar:

  • applicaties deployen op allerlei platformen en omgevingen;
  • applicaties automatisch installeren en updaten;
  • applicaties voorzien van management interfaces;
  • product software uitbreidbaar maken middels een SDK;
  • product software verkopen als combinatie van standaard onderdelen en optionele uitbreidingen;
  • modulaire User Interfaces.

De slides van de presentatie zijn te vinden op de NLJUG website.

, , , , , ,

1 Comment

Swing & OSGi — please play nice!

In a recent blog by Peter Karich, he showed how to create a pluggable Swing application using OSGi. While this works fine for smaller examples, you might run into more serious issues once you application starts to grow.

Plugging Swing: it leaks?

Let’s start with an application not unlike the one from aforementioned blog; it uses a window as host, and has a pluggable menu, and a pluggable table.

SWING_OSGI_Pluggable components600

You can find the code we used at the end of this entry (or, for the impatient, here).

Using this pluggable system, we could end up with several curious situations. For instance, you might have a mixed look and feel in you application.

SWING_OSGI_wrong_menus

Or worse, you might end up with a UI that (sometimes) fails to start, and spits a stacktrace your way.

swing_osgi_NPE

It leaks, but why?

Our host, and all components have been stored in separate bundles, meaning we don’t have full control about the order in which actions are performed (more about that later). However, we do know there are orders of execution that are less than ideal; let’s force one of those.

The project contains an Ant script to make things easier. From the root of the extracted project, run

$ > ant run1

This starts the framework, installing the necessary bundles, but does not start them (note that this step uses Pax Runner, and therefore needs internet access). We can now start our bundles in the order we like.

A tale of two look-and-feels

After starting the framework, wait for the “Welcome to Felix” message, and run

     [java] Welcome to Felix
     [java] ================
     [java]
start 2
start 1

The situation arises because the look and feel is a static concept in Swing. The menu bundle creates its JMenu before (see Menu.java, ln 30) the host sets its look and feel (Host.java, ln 51), and keep that look and feel, even when the host bundle changes it later.

Tables, ScrollPanes and NPEs

The NullPointerException above is a different story, but it goes back to the same staticness of Swing too. To force this situation, start only bundle 4.

     [java] Welcome to Felix
     [java] ================
     [java]
start 4
     [java] Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
     [java]      at net.luminis.swingosgi.part1.scrolltable.impl.TableComponent$1.run(TableComponent.java:31)
     [java]      at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
     [java]      at java.awt.EventQueue.dispatchEvent(EventQueue.java:633)
     [java]      at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:296)
     [java]      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:211)
     [java]      at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:201)
     [java]      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:196)
     [java]      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:188)
     [java]      at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)

Let’s take a look at the line where this NPE happens:

JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getColumnHeader().setBackground(Color.blue);
m_panel.add(scrollPane);

We know that the ColumnHeader is null. This is because its JTable’s responsibility to create the header, but this is only done once the table knows it is part of an AWT hierarchy. The following lines come from the 1.5 JDK on a Mac; configureEnclosingScrollPane() creates the column header. This addNotify method comes from Component, and notifies of, exactly, the event of being added to an AWT container.

public void addNotify() {
  super.addNotify();
  configureEnclosingScrollPane();
}

Order, order!

So, the static nature of Swing and the dynamic nature of OSGi seem to hurt each other seriously here.

One way to get the application right is by fixing the order in which Swing components can be created. By starting bundle 1 first in our application, we at least fix the look and feel. Getting the scrolling table to run correctly is an entirely different story.

Regarding order, a few possible solutions spring to mind immediately,

  1. Put all UI stuff in one bundle
  2. Use OSGi bundle start levels

Sure, all UI in a single bundle will give you the control necessary, but it also defeats the purpose. OSGi start levels can at least solve the ordering issues, but will not get you out of the NullPointerException and might have more impact than you desire.

What order?

As we have seen, absolute order does not solve our problem. How about separating creation and initialization? Still, we need to impose some order, or at least some hierarchy.

SWING_OSGI_Application composition600

We represent each Swing component by an OSGi service, and leverage the OSGi service dependency resolution to build up our hierarchy; this way, we know the host service will be started last.

  1. Resolve services Once the host bundle starts, we know all components are locked and loaded; the host can now start setting up Swing’s static elements like the look and feel.
  2. Create components Component creation ripples downward: the host gets its direct children, adding them to its container, and in the process triggering the children to get their child components.
  3. Initialize components Once the component creation is done, the host instructs each component to initialize; we can now be certain that all components are part of the AWT hierarchy.

To reach this situation, we introduce a new OSGi service that wraps the component.

SWING_OSGI_Using component provider600

All components are handled by a service implementing ComponentProvider; notice how methods are required to be called on the EventDispatchThread, making sure that all components are created on the EDT, while retaining the order necessary.

public interface ComponentProvider {
 /**
 * Constant to identify ComponentProvider services.
 */
 public static final String COMPONENT_ID_KEY = "component.id";
 
 /**
 * This function should always be called from the EDT. The implementor
 * may assume that this function is called once and before {@link #addedToContainer()}
 *
 * @return the implementors (Swing) component which it provides.
 */
 public JComponent getComponent();
 
 /**
 * Triggered when the component is added to a container. The implementation
 * can validate some stuff. This function must be called on the EDT.
 * Implementors may assume this function is called after {@link #getComponent()}.
 */
 public void addedToContainer();
}

The getComponent function is analogous to the create step above; the addedToContainer triggers the initialize.

Let’s try that out!

To check that this actually works OK, run

$ > ant run2

from the root of the project, and start the bundles in any order you like. The UI will only show up once all required components are available; notice that the Table and the ScrollPane component can be used interchangeably.

Is it all good?

For the most part, yes. You do give up some flexibility: the UI is assembled at runtime, but it is no longer possible to (easily) plug components into a running system without special provisions. Then again, how often do you deploy new Swing-based functionality to a running application?

In the example application, we use ServiceTrackers to keep track of the components needed by the host. In a real system, you should consider using some dependency management mechanism; we have used the Apache Felix Dependency Manager in the past.

The project and the story

The project mentioned above is available as a zipped Eclipse project. You can directly import this into Eclipse, or just unzip it and run the Ant build file.

To run the examples, you will need Apache Ant. Also, since we use Pax Runner, you will need an internet connection.

The presentation we gave about this at Devoxx 09 is at SlideShare.

, ,

3 Comments

ApacheCon US 2009 – Celebrating a decade of open source leadership

The Apache Software Foundation celebrated its 10th anniversary last week at the ApacheCon US in Oakland, California. The event, which lasted from November 2nd to 6th, consisted of many different types of events, ranging from full-day trainings to lightning talks, from a hackathon to technical and marketing sessions. On friday, the event featured a full-day track about OSGi, where all OSGi related Apache projects like Felix, ACE, Sling and Tuscany where present. The big announcement of the conference was the fact that Subversion wanted to join Apache. In fact, during the event, just like with any other project, there was a vote to accept Subversion into the incubator. As with many projects, this triggered some discussion, debating the merits of doing a release during incubation, even though this is a project with many seasoned Apache committers on board.

A conference like no other

Apache probably is the strongest brand in the open source space, but the conference itself focusses strongly on content. Here you will see no sponsored talks by commercial vendors, no sales people trying to sell you anything, it’s all about the code, the community and collaborating with each other. In that sense it’s quite different from most other conferences and if you like meeting and discussing fellow developers, this is a great place to visit. Many events facilitate discussion, and power and internet connectivity are available everywhere.

What open source is all about

Brian Behlendorf summarized the three main cultural elements of Apache quite well:

  • write good code and debate it to the bone
  • be humble
  • collaborate

In essence, Apache is a meritocracy, of which only individuals can become a member. It’s sometimes also described as a do-ocracy as projects are driven by contributions: if you want something done, just do it. Another important aspect is that everything that is done on the Apache projects is discussed and archived on the mailing list. All discussions, code diffs and decisions must be recorded there.

Presenting Apache ACE

Tuesday evenings “birds of a feather” session featured a discussion about Apache ACE, where questions mostly centered around the use cases for ACE and possible integrations with other OSGi components. One of the conclusions is that there are probably three different phases of deployment:

  1. Using Apache Felix File Installer, which allows you to drop components in a local folder to have them installed.
  2. Using Apache Felix Karaf’s provisioning components, which allow you to define features which basically group components and allow you to define dependencies on other features.
  3. Using Apache ACE, which allows you to group components and automatically deploy them to many remote systems.

Friday’s OSGi track started with an introduction to OSGi and moved into more advanced topics during the day. The Apache ACE talk was received well, with several people expressing an interest in wanting to use it and contribute to it.

Final thoughts

Summarizing the week, Floris and I had a great time talking to many interesting people and learning about various projects. ApacheCon is a great conference, and I’m already looking forward to the next one.

, , , , , , , , , ,

No Comments

Using GWT to create an OSGi-aware web application

Update 2010-02-20 Both Pax Runner 1.3.0 and GWT 2.0 have caused quite some changes to this post. I have tried to stay up to date as well as I could (the zipped project now uses GWT 2.0), but you might find some inconsistencies when following the tutorial.

Google Web Toolkit is cool, and so is OSGi. However, when building a web UI for Apache ACE, I found out that creating a web application that can use OSGi services is not that easy. By the end of this tutorial, you will have created a GWT project that delivers a usable jar. If you’re impatient, skip to the end for the downloadable Eclipse project.

Step 1: Create a GWT project

Create a regular GWT project using the regular webAppCreator; this will give you a project that includes an Ant buildfile, we will need that later on.

angelos:workspace angelos$ ./gwt-mac-1.6.4/webAppCreator -out GwtDemo net.luminis.gwt.gwtdemo
Created directory GwtDemo/src
Created directory GwtDemo/war
Created directory GwtDemo/war/WEB-INF
Created directory GwtDemo/war/WEB-INF/lib
Created directory GwtDemo/src/net/luminis/gwt
Created directory GwtDemo/src/net/luminis/gwt/client
Created directory GwtDemo/src/net/luminis/gwt/server
Created file GwtDemo/src/net/luminis/gwt/gwtdemo.gwt.xml
Created file GwtDemo/war/gwtdemo.html
Created file GwtDemo/war/gwtdemo.css
Created file GwtDemo/war/WEB-INF/web.xml
Created file GwtDemo/src/net/luminis/gwt/client/gwtdemo.java
Created file GwtDemo/src/net/luminis/gwt/client/GreetingService.java
Created file GwtDemo/src/net/luminis/gwt/client/GreetingServiceAsync.java
Created file GwtDemo/src/net/luminis/gwt/server/GreetingServiceImpl.java
Created file GwtDemo/build.xml
Created file GwtDemo/README.txt
Created file GwtDemo/.project
Created file GwtDemo/.classpath
Created file GwtDemo/gwtdemo.launch
Created file GwtDemo/war/WEB-INF/lib/gwt-servlet.jar

If you want to, you can import this project directly into your Eclipse. If you check the mark “use Google Web Toolkit” in the project properties, you can use all the same goodies that creating the project in Eclipse would have given you. Remember to replace the buildpath entries for gwt-user.jar and gwt-dev-*.jar by a Library import for GWT.

Step 2: Include the necessary OSGi references

Create an ‘ext’ directory, and add org.osgi.core.jar to that. In Eclipse, add this jar to your build path.

Step 3: Use OSGi services from your web applicaiton

We will first add a simple Activator on the server side.

package net.luminis.gwt.server;
 
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
 
public class Activator implements BundleActivator {
    private static BundleContext m_context;
 
    public static BundleContext getContext() {
        return m_context;
    }
 
    public void start(BundleContext context) throws Exception {
        m_context = context;
    }
 
    public void stop(BundleContext context) throws Exception {
    }
}

Then, we up the GreetingServiceImpl to actually use this BundleContext (note that we use it directly here, but you could use it to get other services, create a ServiceTracker, etc.)

public String greetServer(String input) {
  String serverInfo = getServletContext().getServerInfo();
  String userAgent = getThreadLocalRequest().getHeader("User-Agent");
  return "Hello, " + input + "!
 
I am running " + serverInfo
    + ".
 
It looks like you are using:" + userAgent +
    "The framework we run from has " + Activator.getContext().getBundles().length + " bundles in it.";
}

Step 4: Add OSGi dependencies for the compiler

Add our OSGi dependencies to the classpath, so the compiler can find all of it.

    <!-- Add any additional non-server libs (such as JUnit) -->

Right, let’s give it a try!

angelos:GwtDemo angelos$ ant war
Buildfile: build.xml
 
...some output removed...
 
war:
[zip] Building zip: /Users/angelos/workspace/workspace/GwtDemo/gwtdemo.war
 
BUILD SUCCESSFUL
Total time: 36 seconds

You will find a war in your project directory now. There still is one ingredient we need. We need to make this into a proper bundle. We can use bnd to help us with that.

Step 5: use bnd to create a proper war

Download bnd, and put into a lib directory, and add it to your buildfile.

We create a new target that transforms our war into a jar.

<target name="jar">
    <copy file="gwtdemo.war" tofile="gwtdemo.jar"/>
    <echo file="gwtdemo.bnd">Import-Package: junit.framework;resolution:=optional, com.google.gwt.*;resolution:=optional, org.w3c.*;resolution:=optional, sun.misc;resolution:=optional, javax.imageio;resolution:=optional, javax.servlet.*;resolution:=optional, *
Bundle-Name: GWT Demo
Bundle-ClassPath: WEB-INF/classes, WEB-INF/lib/gwt-servlet.jar
Bundle-SymbolicName: net.luminis.gwt.gwtdemo
Webapp-Context: gwtdemo
Bundle-Activator: net.luminis.gwt.server.Activator
    </echo>
    <bndwrap jars="gwtdemo.jar" output="gwtdemo.jar"/>
    <jar file="gwtdemo.jar" update="true">
    <manifest>
        <attribute name="Bundle-ClassPath" value="WEB-INF/classes, WEB-INF/lib/gwt-servlet.jar, ."/>
     </manifest>
    </jar>
    <delete file="gwtdemo.bnd"/>
</target>

What’s happening here?

  • we copy our war to the same file, but with a jar extension,
  • we create a file for bnd to use, stating that we
    • want optional imports for junit and the gwt benchmarks, and non-optional imports for everything else (that what the * is for),
    • have some classes that we want bnd to scan for finding dependencies,
    • want to use a given Webapp-Context (this is a Pax war extender specific entry),
  • let bnd do its magic,
  • update our manifest: we put the . back on the classpath. This is important for the web application to find all resources, but if we would tell bnd to do it like this, it would treat . as the root of the classpath.
  • Finally, we delete that temporary bnd file.

What does that give us?

angelos:GwtDemo angelos$ ant jar
Buildfile: build.xml
 
...some output removed...
 
jar:
[copy] Copying 1 file to /Users/angelos/workspace/workspace/GwtDemo
[bndwrap] gwtdemo 41 910305
[bndwrap] Warnings
[bndwrap] Superfluous export-package instructions: [WEB-INF.classes.net, gwtdemo.gwt.standard.images, WEB-INF, gwtdemo, WEB-INF.classes.net.luminis.gwt, gwtdemo.gwt.standard, WEB-INF.classes.net.luminis, WEB-INF.lib, WEB-INF.classes, gwtdemo.gwt.standard.images.ie6, WEB-INF.classes.net.luminis.gwt.client, WEB-INF.classes.net.luminis.gwt.server, gwtdemo.gwt]
[jar] Updating jar: /Users/angelos/workspace/workspace/GwtDemo/gwtdemo.jar
[delete] Deleting: /Users/angelos/workspace/workspace/GwtDemo/gwtdemo.bnd
 
BUILD SUCCESSFUL
Total time: 23 seconds

That’s it! You can now deploy this jar into a framework that uses the pax web tools. Right, let’s give that a try.

Download pax-runner, and unzip that somewhere. Copy in your new jar, an try the following command

angelos:pax-runner angelos$ sh bin/pax-run.sh --profiles=war,compendium gwtdemo.jar

Now visit http://localhost:8080/gwtdemo:

gwtdemo

Summary

So, what did we need?

  • A fairly regular GWT project, create with an Ant file,
  • some code that tries to use OSGi services,
  • some bnd magic to make the war into a jar,
  • Pax tools to get it all running quickly.

If you don’t want to use pax runner, you can need to deploy pax-web-extender-war(jar, snapshot 23 June) and an http server, preferably pax-web-service(jar), into your framework.

You can download the gwtdemo Eclipse project to play around with it. I have not provided the GWT runtime in this download; you should edit line 4 of the build.xml to point to your installation of GWT.

, , , , , , , , , , ,

12 Comments

Google Wave – Ze doen het weer….

Google presenteerde recentelijk een van de nieuwe ontwikkelingen waarmee ze bezig zijn: Waves. Volgens de documentatie op de Google Wave API website is een wave:


“A wave is a threaded conversation, consisting of one or more participants (which may include both human participants and robots). The wave is a dynamic entity which contains state and stores historical information. A wave is a living thing, with participants communicating and modifying the wave in real time.”

Google Wave draait in je browser. Het is een Javascript applicatie die je browser omtovert tot een collaboration-tool die de concurrentie met een heleboel duurbetaalde tools probleemloos aankan. En het is gratis. De gehele code-base wordt ge-opensourced zodat iedereen heel snel en makkelijk met zijn eigen Wave implementatie aan de slag kan

Is het mooi?

Ja, wat je ziet op de demo die ze gaven op de Google IO conference is absoluut spectaculair. Het duurt even voordat het tot je doordringt (thick skull, sorry), maar dan zie je (als techneut in ieder geval) dat het heel knap is wat ze daar aanbieden. Of je het wilt hebben, of dat het de wereld gaat veranderen of de hoeveelheid CO2 in de atmosfeer gaat verminderen? Geen idee, maar hieronder licht ik even de punten toe die mij erg aanspraken.

Real-time

Alles wat je in een Wave intikt, is onmiddellijk zichtbaar bij alle andere mensen die jouw Wave open hebben staan. Dit biedt dus in ieder geval Instant Messaging, maar dan zonder het wachten op “Mr.X is typing a message”. Er is een open protocol ontwikkelt dat deze communicatie regelt. Even ter herinnering: dit is in je browser. Dus real-time communicatie tussen 2 of meer browsers.

Concurrent

Je werkt met alle betrokkenen tegelijk aan een Wave. Iedereen kan reply-en binnen een Wave, maar kan ook
corrigeren wat een ander nog aan het intikken is. In dat proces wordt bijgehouden wie wat heeft gedaan.

Multi-media

Letterlijk. Het maakt niet uit welk type medium je wilt gebruiken: tekst, beeld, film, geluid. Je kunt het allemaal drag-n-droppen in een wave. Als je een serie plaatjes in een Wave gooit, krijgen alle deelnemers onmiddellijk thumbnails te zien.

Playback

Alle wijzigingen op de Wave staan op een time-line die je kunt terugspelen. Als je met een groep mensen aan een stuk tekst werkt kun je de hele conversatie terughalen, wat noodzakelijk kan zijn omdat iedereen alle tekst kan wijzigen en je alleen het uiteindelijke resultaat ziet. Er is een demo van een schaak-applicatie die in een Wave draait. Behalve dat iedereen op de Wave onmiddellijk de gedane zetten ziet, kun je het hele spel opnieuw afspelen door de time-line te manipuleren. Als ontwikkelaar van een dergelijke app hoef je niets te doen voor de real-time en play-back functionaliteit. Die zit in de Wave en krijg je er gratis bij!

Robots

Er is al een grote hoeveelheid robots geschreven die deelnemen aan een conversatie zoals mensen dat kunnen doen. Deze robots werken voor je als een soort tool of plug-in. Er is bijvoorbeeld een spelling-checker robot die jouw Wave edit terwijl je ermee bezig bent. En zelfs een vertaal-robot die jouw tekst bij jouw Franse collega in het Frans vertaald laat zien. Maar ook robots die tekst herkennen als links en ze meteen clikable maken, of robots die een link naar video herkennen en je meteen de optie geven om een player te embedden.

APIs

Er is een uitgebreide API beschikbaar om zelf robots te maken of om een Wave in je eigen (web)applicatie te embedden. Je vindt er meer over op: http://code.google.com/apis/wave.

Conclusie

Het is eigenlijk nog te vroeg om conclusies te trekken. De developers wereld is er al flink opgesprongen en heeft een veelheid aan robots en gadgets geproduceerd. Dat zegt niet alles. Zoals uit mijn enthousiasme al blijkt is het voor techneuten echt een heel interessant platform. Of het een killer-app is weet ik niet. Het zou heel goed de manier van communicatie kunnen veranderen, omdat je mail, chat en IM in één en dezelfde vormt giet. Ik denk dat het net zo gaat als met de iPhone. Op zichzelf niet briljant, maar doorr de juiste onderliggende technologie maakt het applicaties mogelijk die echt briljant zijn en het product tot grotere hoogte tillen.

, , , , ,

No Comments

JavaOne Reloaded

On tuesday, june 23rd, luminis organizes a JavaOne reloaded event, where all the news and highlights from the 2009 JavaOne are presented. This will take place in the Arnhem offices, starting at 18:00. If you want to be present, please send a mail to Marcel. This year’s JavaOne is special for many reasons. Oracle just bought Sun. Microsoft gives their first keynote. JavaFX comes of age. Larry Ellison hints at a JavaFX frontend for OpenOffice. Enough reasons to be present!

, , , , , , ,

No Comments

Java .Net interoperability

Java .Net interoperability

Have you ever felt the urge and need to connect your Java program to a particular .Net assembly? And more specifically without the hassle of COM bridges, web services, .Net remoting or other service type components. I came across a project in which a simple Java program needed to be connected to an existing .Net assembly (btw written in C#). Everything had to be as simple as possible.
On the Internet a couple of articles were helpful: Espresso, C# method calls within Java Program and Calling a .NET Method from Java via JNI. However it didn’t quite work the way we liked so the ulimate solution came (it so often happens) from Jelle Hissink (http://www.codewise.nl)

The solution is based on:

  • Use JNI for the communication between the Java VM and the .Net CLR
  • Intercept the call to the name resolver and resolve the call to the C# class with your own method

Graphically this looks like:

Let’s work our way from the initial Java call to the final C# implementation.

Java

In Java we have the simple program:

class HelloWorld {
   public static void main(String[] args)
  {
      DotNetBridge dotNetBridge = new DotNetBridge();
      String getString = dotNetBridge.GetString();
      System.out.println("GetString() returned:   " + getString);

      String newString = "Java2.Net world";
      dotNetBridge.SetString(newString);
	getString = dotNetBridge.GetString();
      System.out.println("GetString() returned:   " + getString);
   }
}

Nothing special, just straightforward java code.
However in addition to this code we define an extra class with the DotNetBridge code on the java side. This is needed to kick start the bridging.

public class DotNetBridge
{
  private native void Initialize(String localPath);
  public native String GetString();
  public native void SetString(String newString);

  static
  {
    String path = DotNetBridge.class.getProtectionDomain().getCodeSource().getLocation().getPath();
    int idx = path.lastIndexOf("/");
    if (idx >= 0) {
      path = path.substring(0, idx);
    }
    if (path.startsWith("/")) {
      path = path.substring(1);
    }

    System.load(path + "/DotNetBridge.dll");
    DotNetBridge dotNetBridge = new DotNetBridge();
    dotNetBridge.Initialize(path);
  }
}

In a few words what does it do? It is a class with native methods telling the java compiler that it can find the interface of the methods in this class but the implementation is to be found in a native Windows dll file. On top of that when to class is instantiated for the first time the static part will ensure that the Initialize method is called in which the full path of the dll file with the implementation is passed. Be a little more patient, later on we’ll see why we need this.

DotNetBridge

We now need a simple interface from the Java to the .Net world in which strings can be passed in and out. For this purpose we create a managed C++ project in Visual Studio of the name DotNetBridge. The project consists of:

  • A C++ header file with the definition of the bridge interface
  • A C++ file with the implementation of the bridge
  • A reference to the .Net component with the implementation of the actual HelloWorld code

The header file DotNetBridge.h makes use of JNI (ofcourse) to define the interface of the bridge:

#include "jni.h"

JNIEXPORT void JNICALL Java_DotNetBridge_Initialize(JNIEnv *env, jobject thisobject, jstring localPath);
JNIEXPORT jstring JNICALL Java_DotNetBridge_GetString(JNIEnv *env, jobject thisobject);
JNIEXPORT void JNICALL Java_DotNetBridge_SetString(JNIEnv *env, jobject thisobject, jstring newString);

I’ll not dive into the details of JNI too much (see JNI Primer) but this is the most minimal definition. There are tools to generate this type of definitions (javah –jni) but we leave those out of the equation today. For now watch the prefix Java_DotNetBridge_ which are needed in case of JNI. Also the env and thisobject parameters are specific for each JNI call.

The implementation in C++ has become much more easily with the new .Net framework primitives for managed C++. The two main methods on the interface are the get and set string methods, their implementation looks like:

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace HelloWorldDotNet;

#include 

JNIEXPORT jstring JNICALL
Java_DotNetBridge_GetString (JNIEnv *env, jobject thisobject)
{
    IHelloWorld ^support = gcnew HelloWorld();

    // Get a managed string.
    String ^managedString = support->GetString();

    std::string str = managedToString(managedString);

    // Create the java string...
    jstring result = env->NewStringUTF(str.c_str());

    return result;
}

JNIEXPORT void JNICALL
Java_DotNetBridge_SetString (JNIEnv *env, jobject thisobject,
								jstring newString)
{
    IHelloWorld ^support = gcnew HelloWorld();

    std::wstring newWString = jstringToWString(env, newString);

    String ^ managedString = gcnew String(newWString.c_str());

    support->SetString(managedString);
}

To enable all this we do need some extra string management functions which can translate the java formatted strings to .Net strings and vice versa. Also the transition from the managed to unmanaged environment has its effect on the string management as can be seen from:

std::wstring jstringToWString(JNIEnv *env, jstring jstr)
{
  jboolean iscopy;
  const jchar *strPtr = env->GetStringChars(jstr, &iscopy);

  const jchar *p = strPtr;
  std::wstring result;
  while (*p)
  {
    result += *(p++);
  }

  env->ReleaseStringChars(jstr, strPtr);

  return result;
}

std::string managedToString(String ^mstr)
{
  // Marshal the managed string to unmanaged memory.
  IntPtr hGlobal = Marshal::StringToHGlobalAnsi(mstr);

  char *stringPointer = static_cast(hGlobal.ToPointer());

  // Create the java string...
  std::string result = stringPointer;

  // Always free the unmanaged string.
  Marshal::FreeHGlobal(hGlobal);

  return result;
}

Finally, as said earlier, we need to kick start the whole mechanism. Hence the managed C++ code has currently no idea where to find an implementation of the HelloWorld class in the call to: IHelloWorld ^support = gcnew HelloWorld();
By the way, the import of the namespace has sneaked into the C++ code quite silently with: using namespace HelloWorldDotNet; Without setting a reference to the .Net assembly in the projects’ properties the compiler will not be able to compile this C++ class. Setting such a reference can be done with:

In the Java implementation we have seen that each instantiation of the java bridge class results in a call to the initialize function of the DotNetBridge component. Finally it is time to dive into the details of its implementation. We could have done without it if we deploy all dlls to the java executable directories. From a administration perspective this is not very neat. System administrators normally require that a solution is delivered to one location and the software should take care of its own path settings.

In essence the initialize function of the DotNetBridge is always called once and exactly once with the full path specification of the actual executable directory. So put all dlls into this directory and intercept the name resolver. Because if the runtime tries to load the assembly with the implementation of the HelloWorld class it should look into one specific dll to find this implementation. So this will happen:

  • On the initialize the directory path of the HelloWorld.dll is stored in a static variable
  • The AssemblyResolve event is augmented with its own handler
  • In the handler the directory path is used to load the HelloWorld class from the correct dll
std::wstring assemblyLoadPath;

System::Reflection::Assembly ^ MyResolveEventHandler( Object^ sender, ResolveEventArgs^ args )
{
  String ^name = args->Name;
  int idx = name->IndexOf(L',');
  if (idx >= 0) {
    name = name->Substring(0, idx);
  }
  name = String::Concat(name, gcnew String(L".dll"));

  String ^basePath = gcnew String(assemblyLoadPath.c_str());
  name = System::IO::Path::Combine(basePath, name);

  System::Reflection::Assembly ^ result = System::Reflection::Assembly::LoadFile(name);

  return result;
}

JNIEXPORT void JNICALL Java_DotNetBridge_Initialize (JNIEnv *env, jobject thisobject, jstring localPath)
{
    assemblyLoadPath = jstringToWString(env, localPath);

    AppDomain ^currentDomain = AppDomain::CurrentDomain;
    currentDomain->AssemblyResolve += gcnew ResolveEventHandler( MyResolveEventHandler );
}

This will do the trick on the .Net bridge side. The final implementation of the C# code is straightforward.

C# code

It is a C# class library project. It consists of two classes: the interface and its implementation. The code of the interface definition looks like:

using System;

namespace HelloWorldDotNet
{
    public interface IHelloWorld
    {
        void SetString(string stringValue);
        string GetString();
    }
}

Finally its implementation can be anything. In this case it is simple as:

using System;

namespace HelloWorldDotNet
{
    public class HelloWorld : IHelloWorld
    {
        private static string lastString = "brave new world";
        public void SetString(string stringValue)
        {
            lastString = stringValue;
        }
        public string GetString()
        {
            return String.Format("Hello {0}", lastString);
        }
    }
}

Finally

On purpose, in the code examples details such as exception handling and comments were left out. If you’re interested you can download all the code (see below). On the java side DOS command scripts are used to compile and run the example. You’ll find them easy and straightforward. The C++ and C# code are organized in two separate .Net projects. They were made with Visual Studio 2008. Either migrate to Studio 2008 or use the source code files to build a solution yourself.
If you can’t find your way through all the source files, this is the rough organization:

These are the main steps, with their correct order, to get it to work:

  • Compile the HelloWorld C# project
  • From the DotNetBridge project set the reference to the HelloWorld.dll created in the previous step
  • (possibly) in the DotNetBridge project set the include directories to include the path to the correct JVM include directories (otherwise the jni.h can not be found)
  • Compile the DotNetBridge project
  • Change the compile.cmd and run.cmd scripts in the Java directory to reflect the correct paths.
  • Run compile.cmd
  • Run run.cmd
  • If unchanged you should see something like:





    When you want a full download of all the code in this sample, click here.

    And last but not least: have fun!

, , , , , , , ,

1 Comment

Developing a Confluence Plugin

Introduction

Does your company use Confluence? If you’re looking to extend its possibilities, it may not seem obvious straight away which of the 30+ plugin module types to choose.

Say you want to add a form to Confluence in order to submit (and process) some data. Once the processing is done, the results should be displayed, if possible on the same page. If you’re interested in how to accomplish this, you probably want to keep on reading; it seems easy to create such a plugin, but there’s a few issues that you’re likely to run into.

In this blog I’ll describe the more troublesome/unexpected ones that we encountered and will explain how they were handled (or sometimes: worked around).

Plugin Module Types

First of all, let’s pick the plugin module types to use. In order to embed (HTML) content in a page, the plugin type to use is the Macro Plugin. This is not sufficient, though; for something to happen, the macro needs to display some form elements (e.g. text fields, select fields and a submit button) and when the form is submitted, the right action should take place. For this to happen, we used an XWork Action; the form data is sent to and handled by the Action.

First of all, you need to get a simple Macro Plugin up-and running. Besides creating an Archetype and some plugin-specific steps, most of the steps required to do this can be found here.

Following that, you need to setup an XWork Action (check the ‘XWork Action’ link above to find out more). Once that is done, create a velocity macro (.vm file) containing a form that submits its data to the Action:

    <form name="myForm" action="/confluence/plugins/actions/myaction.action" method="GET">
    	$action.inputText: <input type="text" name="myInput">
        <p>
        <input type="submit" value="Submit">
    </form>

Note that he action to be used (on a ’submit’ of the form) should be defined as the XWork Action ‘myaction’ in atlassian-plugin.xml.

After that, display the form by embedding (all text and controls defined in) the .vm file into the macro by updating its execute(…) method (used when the macro is rendered):

public String execute(Map params, String body, RenderContext renderContext) throws MacroException {
    Map context = MacroUtils.defaultVelocityContext();
    context.put("action", new MyAction());
    return VelocityUtils.getRenderedTemplate("templates/myaction.vm", context);
}

Note that the action that will handle the submitted form data is supplied through a “velocity context” map. Supplying it is only required if the action is queried when rendering the velocity macro (i.e. if you’re using elements in the .vm file that start with $action. – specifically, in this case: $action.inputText).

Also, it’s important to use only HTML body elements in the .vm file; a complete HTML syntax will result in anything on a page following the {macro} tag no longer being displayed! After this is done, it is possible to display / fill in the form (and submitting it to the XWork Action) through rendering the Macro that is wrapped around it.

Return from whence we came

As previously mentioned, we would like to ’stay’ in the same page. In order to achieve this, Confluence offers the possibility of a (server) redirect. Now, all we need to do is figure out from whence we came, since the macro can be embedded pretty much anywhere! Unfortunately, simply checking the request URL does not work (because of Confluence (pre)processing); a generic URL is returned (ending with /pages/viewpage.action). We solved it using a bit of a ’scrippety-trickery’ solution! Since we can’t find out the URL on the server-side, in the form that was submitted, we included a hidden element:

<input type="hidden" name="macroUrl">

and set it with the current URL by including a bit of JavaScript in the .vm file as well:

<script language="JavaScript">
	document.myForm.macroUrl.value = window.location
</script>

Don’t forget to add the matching getter and setter in the action that the velocity macro (form) posts to (in this case ‘MyAction’)! Once this is done, you can retrieve the Action value in the xwork/myaction part of the atlassian-plugin.xml configuration file (note that this is runtime info and not maven setting some properties!):

<action name="myaction" class="com.luminis.confluence.plugin.MyAction">
    <result name="success" type="redirect">${macroUrl}</result>
</action>

Persistent data

In order to store userspecific data, Confluence uses a table in which key-value combinations can be stored: the BANDANA table. Writing to and reading from this table is done through the BandanaManager.

To get hold of the correct manager objects, all you need to do is declare a variable and a setter for the variable and Spring will auto-inject the right managers:

    BandanaManager m_bandanaManager;
    public void setBandanaManager(BandanaManager bandanaManager) {
        m_bandanaManager = bandanaManager;
    }

If, at some point, you can’t wait for Spring to inject them, you can also look them up manually, e.g. for the PlatformTransactionManager:

    PlatformTransactionManager m_transactionManager;
    // ...
    Object o = ContainerManager.getComponent("transactionManager");
    if (o instanceof PlatformTransactionManager) {
        m_transactionManager = (PlatformTransactionManager)o;
    }

Finally, the BandanaManager uses a ‘context’ to get and set values:

    BandanaContext m_bandanaContext;
    // ...
    String rootKey = getClass().getPackage().getName();
    m_bandanaContext = new ConfluenceBandanaContext(rootKey);

As soon as the BandanaManager, the PlatformTransactionManager and the ConfluenceBandanaContext are available, it’s possible to store data into the BANDANA table:

    public void storeKeyValue(String key, Object val) {
        storeDataWithinTransaction(key, val);
        // Stopping and starting Confluence here is no problem.
        Object o = m_bandanaManager.getValue(m_bandanaContext, key);
        // o.equals(val) == true // assuming properly implemented 'equals()'
    }

    // Data should be stored within a transaction; use a Spring TransactionTemplate to wrap a transaction around the operation:
    public void storeDataWithinTransaction(final String key, final Object val) {
        TransactionTemplate tt = new TransactionTemplate(m_transactionManager);
        TransactionCallback callback = new TransactionCallback() {
            public Object doInTransaction(TransactionStatus transactionStatus) {
                m_bandanaManager.setValue(m_bandanaContext, key, val);
                return null;
            }
        };
        tt.execute(callback);
    }

Persistent ‘proprietary’ data

In case you want to define your own objects and store them as well, this will fail when using the BandanaManager, since it can’t find the class. Note that this is a tricky failure because after restarting Confluence, reading the value fails without any logging at all! Storing (new) values after that, however, does work. To make sure this does succeed, use XStream to convert the object to a(n XML) String before storing it:

    XStream m_xStream = new XStream();
    {
        m_xStream.setClassLoader(getClass().getClassLoader());
        m_xStream.alias("my-class", MyClass.class);
    }

    public void storeMyClass(MyClass mc) {
        String xmlStr = m_xStream.toXML(mc);
        storeKeyValue("specifickey", xmlStr);
    }

And convert it back from XML after reading it:

    public MyClass readMyClass() {
        String s = (String)m_bandanaManager.getValue(m_bandanaContext, "specifickey");
        return (MyClass)m_xStream.fromXML(s);
    }

, , , , , , , , ,

No Comments

LWUIT – een lichtgewicht ui toolkit voor Java ME

Introductie

Ooit al eens een Java ME applicatie ontwikkeld voor een specifiek type mobiele telefoon? Wanneer het aankomt op het ontwikkelen van een gelikte interface loop je er al snel tegen aan dat de MIDP implementatie van de telefoon van invloed is op hoe de widgets van de applicatie getoond worden. Nadeel daarvan is dat de UI er op een ander type mobiel nogal anders uit kan zien. Om die reden was men voorheen nog wel eens geneigd om wat dieper in de code te duiken en de wat specifiekere (geliktere) widgets op de (javax.microedition.lcdui.)Canvas uit te werken. Deze Canvas ondersteunt sinds MIDP-2.0 een fullscreen modus: het is mogelijk om de complete applicatie direct op Canvas te ontwikkelen. Hiervan maakt LWUIT gebruik.

Mogelijk, maar wel bijzonder tijds-intensief, tenzij je gebruik maakt van het werk van anderen die al wat voorwerk hebben verricht op dit gebied. Een mooi voorbeeld hiervan is “LWUIT”, een door Sun ontwikkelde open-source UI toolkit. In tegenstelling tot het ontwerpen van UI’s met standaard Java ME is het met LWUIT mogelijk om op een Swing-achtige manier user interfaces te maken: layout managers, maar ook de pluggable look & feel, het MVC concept en “lightweight componenten” (componenten maken geen gebruik van de native UI controls, maar verzorgen zelf hun rendering en event handling) zijn terug te vinden.

Setup simpele MIDlet

Een MIDlet maken waarin met LWUIT een paar knoppen wordt getoond is simpel voor elkaar te krijgen via de volgende stappen:

  • Download NetBeans (kies een versie met ‘Java ME’ support) en installeer het pakket.
  • Download en unzip de LWUIT zip file (via de ‘Download page’ tab – de zip bevat ook de resource editor).
  • Download en installeer de Sprint wireless toolkit (via de ‘Develop’ tab, de ‘Technologies’ link en dan de ‘Java ME’ link) of een andere toolkit, zoals die van Sun.
  • Binnen NetBeans is het handig om de LWUIT jar toe te voegen als library via ‘Tools’, ‘Libraries en dan de ‘New Library…’ knop. Geef naam, type = ‘Class Libraries’, dan: ‘OK’ en ‘Add JAR/Folder…’: en selecteer het bestand “LWUIT.jar” dat te vinden is in de ‘lib’ dir van de LWUIT installatie (unzippen van de zip file). Het ‘LWUIT_stripped.jar’ bestand is de LWUIT library zonder code die gebruik maakt van JSR 184 (M3G: Mobile 3D Graphics), JSR 75 (PIM: Personal Information Management, FC: File Connection) of JSR 226 (SVG: Scalable 2D Vector Graphics).

    Start vervolgens een nieuw project, kies als categorie ‘Java ME’ en als project ‘Mobile Application’ en druk op ‘Next’. Het is niet nodig om NetBeans een default MIDlet aan te laten maken; het vinkje bij ‘Create Hello MIDlet’ mag weg. Druk weer op ‘Next’. Selecteer de WTK die je hebt geïnstalleerd als ‘Emulator Platform’. LWUIT vereist minimaal MIDP-2.0 bovenop CLDC-1.1, dus i.p.v. MIDP-2.1 die default geselecteerd is zou je MIDP-2.0 kunnen kiezen, maar dit hoeft niet. Druk op ‘Finish’. Maak nu een nieuwe MIDlet aan met de naam ‘MyMIDlet’ en kopieer de volgende code er naartoe:

    import com.sun.lwuit.*;
    import com.sun.lwuit.events.ActionEvent;
    import com.sun.lwuit.layouts.BoxLayout;
    import javax.microedition.midlet.MIDlet;
    
    public class MyMIDlet extends MIDlet {
    
        Form form1;
    
        public void startApp() {
            Display.init(this);
            form1 = new Form("LWUIT Demo Form");
            form1.addCommand(new Command("Quit") {
                public void actionPerformed(ActionEvent ae) {
                    destroyApp(true);
                    notifyDestroyed();
                }
            });
    
            form1.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
            Button l = new Button("Left");
            l.setAlignment(Label.LEFT);
            Button r = new Button("Right");
            r.setAlignment(Label.RIGHT);
            Button m = new Button("Middle");
            m.setAlignment(Label.CENTER);
            form1.addComponent(l);
            form1.addComponent(r);
            form1.addComponent(m);
    
            form1.show();
        }
    
        public void pauseApp() {
        }
    
        public void destroyApp(boolean unconditional) {
        }
    }
    

    Omdat het project de LWUIT jar nog niet kent zal je een aantal foutmeldingen zien: via de project properties kan je onder de ‘Build’ categorie ‘Libraries & Resources’ kiezen. Voeg de LWUIT library die je eerder hebt aangemaakt toe. Omdat er slechts 1 MIDlet in het project zit zal deze MIDlet automatisch gestart worden wanneer je het project right-clickt en dan ‘Run’ selecteert. Het resultaat hiervan is een simpele LWUIT applicatie met een paar knopjes!

    MVC

    Een aardige illustratie van het MVC concept bij LWUIT is de List. Voeg het volgende stuk code in voor de ‘form1.show()’ uit het vorige voorbeeld.

    String[] bands = new String[]{
        "AC/DShe",
        "Public Enema",
        "Beverly Beerbellies",
        "Shirley Temple Pilots",
        "Wynona Riders"};
    List list = new List(bands);
    form1.addComponent(list);
    

    .

    Resultaat

  • Het Model is verantwoordelijk voor de data; zaken als hoeveel items er zijn, maar ook bijvoorbeeld welk item welke offset heeft. Wanneer er veranderingen in het model optreden geeft het model een seintje richting alle (geregistreerde) SelectionListener en DataChangedListener implementaties. De List in dit voorbeeld krijgt een array van Strings aangeleverd en maakt onder water gebruik van een default implementatie van de ListModel interface, het DefaultListModel.
  • De View wordt in dit geval grotendeels geregeld door een default implementatie van de ListCellRenderer (LCR), de DefaultListCellRenderer. De View is verantwoordelijk voor het tonen van de data die het Model levert. De LCR rendert het object dat vanuit het Model wordt geleverd naar een com.sun.lwuit.Component object dat vervolgens door de List wordt gebruikt om zichzelf te renderen. De default implementatie (is-a com.sun.lwuit.Label) doet simpelweg een toString() van het aangeleverde object, zet dit als text op zichzelf en geeft this terug.
  • Tot slot is de List zelf de Controller (en eigenlijk ook een beetje van de View – maakt zwaar gebruik van de LCR, maar rendert uiteindelijk toch zichzelf) die user input afhandelt. Wanneer bijvoorbeeld een ander element in de lijst wordt geselecteerd, dan geeft de List dit door aan het model (die vervolgens weer de geregistreerde SelectionListener objecten triggert).
  • Zelf de implementatie van ListModel en ListCellRenderer verzorgen heeft als extra voordeel dat je de reeds bestaande data types direct kan gebruiken en er geen sprake hoeft te zijn van “state copying”: er is geen voorbepaald data type wat in de List past.

    Ook goed om te weten is dat de List z’n Model alleen maar vraagt naar de data-elementen die op het scherm zichtbaar zijn. Dit (performance) aspect wordt belangrijker naarmate de hoeveelheid data groeit of bijv. vanaf een remote locatie moet worden binnengehaald.

    Conclusie

    Via de bovenstaande stappen is het relatief makkelijk om snel een simpele LWUIT applicatie te maken. Met name voor de ontwikkelaars die hun app op meerdere typen mobiele telefoons willen draaien zal het een aangename verrassing zijn om te zien dat deze voorbeeld-applicatie op alle mobieltjes (die MIDP-2.0/CLDC-1.1 supporten) in uiterlijk en gedrag hetzelfde zal zijn. In een volgende blog zal ik wat verder ingaan op “fancier features” zoals Transitions en Theming; het oog wil immers ook wat!

    , , , , ,

    No Comments