Friday, November 20, 2009

Unit tests, Mock objects, and App Engine

For my [still a secret] project which is running on Google App Engine infrastructure [1], I want to make it as solid as possible from the beginning by applying most of the best practices of the Agile methodology [2].

Update 2009/12/05:
With the release of the App Engine Java SDK 1.2.8 (read release notes, I had to update my code and this post on two points:
  • Without the specification of the JDO inheritance type, the environment assumes it's superclass-table. This type is not supported by App Engine. Only subclass-table and complete-table are supported. In the Entity class described below, I had to add @Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE). Read the documentation about Defining data classes for more information.
  • With the automation of the task execution, the MockAppEngineEnvironment class listed below had to be updated to serve an expected value when the Queue runs in the live environment. Read the details on the thread announcing the 1.2.8. SDK prerelease on Google Groups.
Now, all tests pass again ;)

As written on my post from September 18, I had to develop many mock classes to keep reaching the mystical 100% of code coverage (by unit tests) [3]. A good introduction of mock objects is given by Vincent Massol in his book “JUnit in Action” [4]. To summarize, mock objects are especially useful to inject behavior and force the code using them to exercise complex control flows.

Developing applications for Google App Engine is not that complex because the system has a good documentation and an Eclipse plug-in ease the first steps.

Use case description

Let's consider a simple class organization implementing a common J2EE pattern:

  • A DTO class for a Consumer;
  • The DAO class getting the Consumer from the persistence layer, and sending it back with updates; and
  • A Controller class routing REST requests. The Controller is an element of the implemented MVC pattern
Use case illustration

The code for the DTO class is instrumented with JDO annotations [5]:

Consumer DTO class definition
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public class Consumer extends Entity {
    @Persistent
    private String address;
 
    @Persistent
    private String displayName;
 
    @Persistent
    private String email;
 
    @Persistent
    private String facebookId;
 
    @Persistent
    private String jabberId;
 
    @Persistent
    private Long locationKey;
 
    @Persistent
    private String twitterId;
 
    /** Default constructor */
    public Consumer() {
        super();
    }
 
    /**
     * Creates a consumer
     * @param in HTTP request parameters
     */
    public Consumer(JsonObject parameters) {
        this();
        fromJson(parameters);
    }
 
    public String getAddress() {
        return address;
    }
    
    public void setAddress(String address) {
        this.address = address;
    }
    
    //...
}

My approach for the DAO class is modular:

  • When the calling code is doing just one call, like the ConsumerOperations.delete(String) method deleting the identified Consumer instance, the call can be done without the persistence layer knowledge.
  • When many calls to the persistence layer are required, the DAO API offers the caller to pass a PersistenceManager instance that can be re-used from call to call. With the combination of the detachable="true" parameter specified in the JDO annotation for the Consumer class, it saves many cycles.
Excerpt from the ConsumerOperations DAO class definition
/**
 * Persist the given (probably updated) resource
 * @param consumer Resource to update
 * @return Updated resource
 * @see ConsumerOperations#updateConsumer(PersistenceManager, Consumer)
 */
public Consumer updateConsumer(Consumer consumer) {
    PersistenceManager pm = getPersistenceManager();
    try {
        // Persist updated consumer
        return updateConsumer(pm, consumer);
    }
    finally {
        pm.close();
    }
}
 
/**
 * Persist the given (probably updated) resource while leaving the given persistence manager open for future updates
 * @param pm Persistence manager instance to use - let opened at the end to allow possible object updates later
 * @param consumer Resource to update
 * @return Updated resource
 */
public Consumer updateConsumer(PersistenceManager pm, Consumer consumer) {
    return pm.makePersistent(consumer);
}

The following piece of the abstract class BaseOperations shows the accessor made availabe to any controller code to get one handle of a valid PersistenceManager instance.

Excerpt from the abstract BaseOperations DAO class definition
/**
 * Accessor isolated to facilitate tests by IOP
 * @return Persistence manager instance
 */
public PersistenceManager getPersistenceManager() {
    PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
    pm.setDetachAllOnCommit(true);
    pm.setCopyOnAttach(false);
    return pm;
}

To finish the use case setup, here is a part of the controller code which deals with incoming HTTP requests and serves or operates accordingly. This specific piece of code replies to a GET request like:

  • Invocation: http://<host:port>/API/Consumer/43544"
  • Response:
    • {key:43544, displayName:"John", address:"75, Queen, Montréal, Qc, Canada", 
      locationKey:3245, location: {id:3245, postalCode:"H3C2N6", countryCode:"CA",
      latitude:43.3, longitude:-73.4}, ...}
Excerpt from the ConsumerRestlet Controller class definition
@Override
protected JsonObject getResource(JsonObject parameters, String resourceId, User loggedUser) throws DataSourceException {
    PersistenceManager pm = getBaseOperations().getPersistenceManager();
    try {
        // Get the consumer instance
        Consumer consumer = getConsumerOperations().getConsumer(pm, Long.valueOf(resourceId));
        JsonObject output = consumer.toJson();
        // Get the related information
        Long locationKey = consumer.getLocationKey();
        if (locationKey != null) {
            Location location = getLocationOperations().getLocation(pm, locationKey);
            output.put(Consumer.LOCATION, location.toJson());
        }
        // Return the complete set of information
        return output;
    }
    finally {
        pm.close();
    }
}

Simple mock

Now, it's time to test! To start slowly, let's deal with the Restlet getResource() method to verify:

  • Just one and only one instance of PersistenceManager is loaded by the function;
  • The PersistenceManager instance is cleanly closed at the end of the process;
  • There's a call issued to get the identified Consumer instance;
  • There's possibly a call issued to get the identified Location instance;
  • The output value has the expected information.

In the corresponding unit test series, we don't want to interfere with the App Engine infrastructure (the following chapter will address that aspect). So we'll rely on a mock for the PersistenceManager class that will be injected into the ConsumerRestlet code. The full source of this class is available on my open source project two-tiers-utils: javax.jdo.MockPersistenceManager.

Custom part of the mock for the PersistenceManager class
public class MockPersistenceManager implements PersistenceManager {
    private boolean closed = false; // To keep track of the "closed" state
    public void close() {
        closed = true;
    }
    public boolean isClosed() {
        return closed;
    }

    // ...
}

Here are the unit tests verifying the different flow paths:

  • When an exception is thrown, because the back-end does not serve the data for example;
  • When the Consumer instance returns without location coordinates;
  • When the Consumer instance is fully documented.
Three tests validating the behavior of the ConsumerRestlet.getResource() method
@Test(expected=IllegalArgumentException.class)
public void testUnexpectedError() {
    // Test prepration
    final PersistenceManager pm = new MockPersistenceManager();
    final BaseOperations baseOps = new BaseOperations() {
        boolean askedOnce = false;
        @Override
        PersistenceManager getPersistenceManager() {
            if (askedOnce) {
                fail("Expects only one call");
            }
            askedOnce = true;
            return pm;
        }
    };
    final Long consumerId = 12345L;
    final ConsumerOperations consumerOps = new ConsumerOperations() {
        @Override
        Consumer getConsumer(PersistenceManager pm, Long id) {
            assertEquals(consumerId, id);
            throw new IllegalArgumentException("Done in purpose!");
        }
    };
    ConsumerRestlet restlet = new ConsumerRestlet() {
        @Override BaseOperation getBaseOperations() { return baseOps; }
        @Override ConsumerOperation getConsumerOperations() { return consumerOps; }
    }
    
    // Test itself
    JsonObject response = restlet.getResource(null, consumerId.toString, null);
}
@Test
public void testGettingOneConsumer() {
    // Test prepration
    final PersistenceManager pm = new MockPersistenceManager();
    final BaseOperations baseOps = new BaseOperations() {
        boolean askedOnce = false;
        @Override
        PersistenceManager getPersistenceManager() {
            if (askedOnce) {
                fail("Expects only one call");
            }
            askedOnce = true;
            return pm;
        }
    };
    final Long consumerId = 12345L;
    final ConsumerOperations consumerOps = new ConsumerOperations() {
        @Override
        Consumer getConsumer(PersistenceManager pm, Long id) {
            assertEquals(consumerId, id);
            Consumer consumer = new Consumer();
            consumer.setId(consumerId);
            return consumer;
        }
    };
    final Long locationId = 67890L;
    final LocationOperations locationOps = new LocationOperations() {
        @Override
        Location getLocation(PersistenceManager pm, Long id) {
            fail("Call not expected here!");
            return null;
        }
    };
    ConsumerRestlet restlet = new ConsumerRestlet() {
        @Override BaseOperation getBaseOperations() { return baseOps; }
        @Override ConsumerOperation getConsumerOperations() { return consumerOps; }
        @Override LocationOperation getLocationOperations() { return locationOps; }
    }
    
    // Test itself
    JsonObject response = restlet.getResource(null, consumerId.toString, null);
    
    // Post-test verifications
    assertTrue(pm.isClosed());
    assertNotSame(0, response.size());
    assertTrue(response.containsKey(Consumer.ID);
    assertEquals(consumerId, response.getLong(Consumer.ID));
}
@Test
public void testGettingConsumerWithLocation() {
    // Test prepration
    final PersistenceManager pm = new MockPersistenceManager();
    final BaseOperations baseOps = new BaseOperations() {
        boolean askedOnce = false;
        @Override
        PersistenceManager getPersistenceManager() {
            if (askedOnce) {
                fail("Expects only one call");
            }
            askedOnce = true;
            return pm;
        }
    };
    final Long consumerId = 12345L;
    final Long locationId = 67890L;
    final ConsumerOperations consumerOps = new ConsumerOperations() {
        @Override
        Consumer getConsumer(PersistenceManager pm, Long id) {
            assertEquals(consumerId, id);
            Consumer consumer = new Consumer();
            consumer.setId(consumerId);
            consumer.setLocationId(locationId);
            return consumer;
        }
    };
    final LocationOperations locationOps = new LocationOperations() {
        @Override
        Location getLocation(PersistenceManager pm, Long id) {
            assertEquals(locationId, id);
            Location location = new Location();
            location.setId(locationId);
            return location;
        }
    };
    ConsumerRestlet restlet = new ConsumerRestlet() {
        @Override BaseOperation getBaseOperations() { return baseOps; }
        @Override ConsumerOperation getConsumerOperations() { return consumerOps; }
        @Override LocationOperation getLocationOperations() { return locationOps; }
    }
    
    // Test itself
    JsonObject response = restlet.getResource(null, consumerId.toString, null);
    
    // Post-test verifications
    assertTrue(pm.isClosed());
    assertNotSame(0, response.size());
    assertTrue(response.containsKey(Consumer.ID);
    assertEquals(consumerId, response.getLong(Consumer.ID));
    assertTrue(response.containsKey(Consumer.LOCATION_ID);
    assertEquals(locationId, response.getLong(Consumer.LOCATION_ID));
    assertTrue(response.containsKey(Consumer.LOCATION);
    assertEquals(consumerId, response.getJsonObject(Consumer.LOCATION).getLong(Location.ID));
}

Note that I would have been able to override just the PersistenceManager class to have the Object getObjectById(Object arg0) method returning the expected exception, Consumer, and Location instances. But I would have pass over the strict limit of a unit test by then testing also the behavior of the ConsumerOperations.getConsumer() and LocationOperations.getLocation() methods.

App Engine environment mock

Now, testing the ConsumerOperations class offers a better challenge.

As suggested above, I could override many pieces of the PersistenceManager class to be sure to control the flow. But to do a nice simulation, I almost need to have the complete specification of the Google App Engine infrastructure to be sure I mock it correctly. This is especially crucial when processing Query because Google data store has many limitations [6] that others traditional database, like MySQL, don't have...

Because this documentation is partially available and because Google continues to update its infrastructure, I looked for a way to use the standalone environment made available with the App Engine SDK [1]. This has not been easy because I wanted to have the test running independently from the development server itself. I found first some documentation on Google Code website: Unit Testing With Local Service Implementations, but it was very low level and did not fit with my JDO instrumentation of the DTO classes. Hopefully, I found this article JDO and unit tests from App Engine Fan, a great community contributor I mentioned many times in previous posts!

By cooking information gathered on Google Code website and on App Engine Post, I've produced a com.google.apphosting.api.MockAppEngineEnvironment I can use for my JUnit4 tests.

Three tests validating the behavior of the ConsumerRestlet.getResource() method
package com.google.apphosting.api;
 
// import ...
 
/**
 * Mock for the App Engine Java environment used by the JDO wrapper.
 *
 * These class has been build with information gathered on:
 * - App Engine documentation: http://code.google.com/appengine/docs/java/howto/unittesting.html
 * - App Engine Fan blog: http://blog.appenginefan.com/2009/05/jdo-and-unit-tests.html
 *
 * @author Dom Derrien
 */
public class MockAppEngineEnvironment {
 
    private class ApiProxyEnvironment implements ApiProxy.Environment {
        public String getAppId() {
          return "test";
        }
 
        public String getVersionId() {
          return "1.0";
        }
 
        public String getEmail() {
          throw new UnsupportedOperationException();
        }
 
        public boolean isLoggedIn() {
          throw new UnsupportedOperationException();
        }
 
        public boolean isAdmin() {
          throw new UnsupportedOperationException();
        }
 
        public String getAuthDomain() {
          throw new UnsupportedOperationException();
        }
 
        public String getRequestNamespace() {
          return "";
        }
 
        public Map getAttributes() {
            Map out = new HashMap();

            // Only necessary for tasks that are added when there is no "live" request
            // See: http://groups.google.com/group/google-appengine-java/msg/8f5872b05214...
            out.put("com.google.appengine.server_url_key", "http://localhost:8080");

            return out;
        }
    };
 
    private final ApiProxy.Environment env;
    private PersistenceManagerFactory pmf;
 
    public MockAppEngineEnvironment() {
        env = new ApiProxyEnvironment();
    }
 
    /**
     * Setup the mock environment
     */
    public void setUp() throws Exception {
        // Setup the App Engine services
        ApiProxy.setEnvironmentForCurrentThread(env);
        ApiProxyLocalImpl proxy = new ApiProxyLocalImpl(new File(".")) {};
 
        // Setup the App Engine data store
        proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString());
        ApiProxy.setDelegate(proxy);
    }
 
    /**
     * Clean up the mock environment
     */
    public void tearDown() throws Exception {
        // Verify that there's no pending transaction (ie pm.close() has been called)
        Transaction transaction = DatastoreServiceFactory.getDatastoreService().getCurrentTransaction(null);
        boolean transactionPending = transaction != null;
        if (transactionPending) {
            transaction.rollback();
        }
 
        // Clean up the App Engine data store
        ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
        if (proxy != null) {
            LocalDatastoreService datastoreService = (LocalDatastoreService) proxy.getService("datastore_v3");
            datastoreService.clearProfiles();
        }
 
        // Clean up the App Engine services
        ApiProxy.setDelegate(null);
        ApiProxy.clearEnvironmentForCurrentThread();
 
        // Report the issue with the transaction still open
        if (transactionPending) {
            throw new IllegalStateException("Found a transaction nor commited neither rolled-back." +
                    "Probably related to a missing PersistenceManager.close() call.");
        }
    }
 
    /**
     * Creates a PersistenceManagerFactory on the fly, with the exact same information
     * stored in the /WEB-INF/META-INF/jdoconfig.xml file.
     */
    public PersistenceManagerFactory getPersistenceManagerFactory() {
        if (pmf == null) {
            Properties newProperties = new Properties();
            newProperties.put("javax.jdo.PersistenceManagerFactoryClass",
                    "org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory");
            newProperties.put("javax.jdo.option.ConnectionURL", "appengine");
            newProperties.put("javax.jdo.option.NontransactionalRead", "true");
            newProperties.put("javax.jdo.option.NontransactionalWrite", "true");
            newProperties.put("javax.jdo.option.RetainValues", "true");
            newProperties.put("datanucleus.appengine.autoCreateDatastoreTxns", "true");
            newProperties.put("datanucleus.appengine.autoCreateDatastoreTxns", "true");
            pmf = JDOHelper.getPersistenceManagerFactory(newProperties);
        }
        return pmf;
    }
 
    /**
     * Gets an instance of the PersistenceManager class
     */
    public PersistenceManager getPersistenceManager() {
        return getPersistenceManagerFactory().getPersistenceManager();
    }
}

With such a class, the unit test part is easy and I can build complex test cases without worrying about the pertinence of my mock classes! That's really great.

Excerpt of the TestConsumerOperations class
public class TestConsumerOperations {
 
    private MockAppEngineEnvironment mockAppEngineEnvironment;
 
    @Before
    public void setUp() throws Exception {
        mockAppEngineEnvironment = new MockAppEngineEnvironment();
        mockAppEngineEnvironment.setUp();
    }
 
    @After
    public void tearDown() throws Exception {
        mockAppEngineEnvironment.tearDown();
    }
 
    @Test
    public void testCreateVI() throws DataSourceException, UnsupportedEncodingException {
        final String email = "unit@test.net";
        final String name = "Mr Unit Test";
        Consumer newConsumer = new Consumer();
        newConsumer.setDisplayName(name);
        newConsumer.setEmail(email);
        assertNull(newConsumer.getId());
 
        // Verify there's no instance
        Query query = new Query(Consumer.class.getSimpleName());
        assertEquals(0, DatastoreServiceFactory.getDatastoreService().prepare(query).countEntities());
 
        // Create the user once
        ConsumerOperations ops = new ConsumerOperations();
        Consumer createdConsumer = ops.createConsumer(newConsumer);
 
        // Verify there's one instance
        query = new Query(Consumer.class.getSimpleName());
        assertEquals(1, DatastoreServiceFactory.getDatastoreService().prepare(query).countEntities());
 
        assertNotNull(createdConsumer.getId());
        assertEquals(email, createdConsumer.getEmail());
        assertEquals(name, createdConsumer.getName());
    }
    
    // ...
}

Conclusion

As a big fan of TDD, I'm now all set to cover the code of my [still a secret] project efficiently. It does not mean everything is correct, more that everything I thought about is correctly covered. At the time of this writing, just for the server-side logic, the code I produced covers more than 10,000 lines and the unit tests bring an additional set of 23,400 lines.

When it's time to refactor a bit or to add new features (plenty of them are aligned in my task list ;), I feel comfortable because I know I can detect most of regressions (if not all) after 3 minutes of running the test suite.

If you want to follow this example, feel free to get the various mock classes I have added to my two-tiers-utils open-source project. In addition to mock classes for the App Engine environment, you'll find:

  • Basic mock classes for the servlet (see javax.servlet) and javamocks.io packages -- I had to adopt the root javamocks because the JVM class loader does not accept the creation on the fly of classes in the java root).
  • A mock class for twitter4j.TwitterUser -- I needed a class with public constructor and a easy way to create a default account.
  • A series of mock class for David Yu's Project which I use to allow users with OpenID credentials to log in. Read the discussion I had with David on ways to test his code, in fact the code he produced and I customized for my own needs and for other security reasons.

For other details on my library, read my post Internationalization and my two-tiers-utils library.

I hope this helps.

A+, Dom
--
References:

  1. Google App Engine: the homepage and the SDK page.
  2. See my post on Agile: SCRUM is Hype, but XP is More Important... where I mentionned the following techniques: Continuous Integration (CI), Unit testing and code coverage (CQC), and Continuous refactoring.
  3. I know that keeping 100% as the target for code coverage numbers is a bit extreme. I read this article Don't be fooled by the coverage report soon after I started using Cobertura. In addition to reducing the exposition to bugs, the 100% coverage gives a very high chance to detect regressions before pushing the updates to the source control system!
  4. Vincent Massol; JUnit in Action; Editions Manning; www.manning.com/massol and Petar Tahchiev, Felipe Leme, Vincent Massol, and Gary Gregory; JUnit in Action, Second Edition; Editions Massol; www.manning.com/tahchiev. I was used to asking any new developer joigning my team to read at least this chapter 7: Testing in isolation with mock objects.
  5. JDO stands for Java Data Objects and is an attempt abstract the data storage manipulation. The code is instrumented with Java annotations like @Persistent, it is instrumented at compile time, and dynamically connect to the data source thanks few properties files. Look at the App Engine - Using the Datastore with JDO documentation for more information.
  6. For general limitations, check this page Will it play in App Engine. For JDO related limitations, check the bottom of the page Usin JDO with App Engine.

Friday, October 16, 2009

Canadian Wireless Management Forum - my review

Last week, I attended the Canadian Wireless Management Forum in Montreal. Honestly, I've been a bit disappointed because none of the presenters were as great as the ones I met last year. Over the day, I've still been able to gather bits of information I want to share here ;) Thanks to my company Compuware to have allowed sparing one day there.

iPhone and other smart phones deployed in enterprises

The main focus of the conference is around managing wireless (read mobile phone) communications in enterprises. Some presenters talked about how to control expenses, from sharing guidelines up to using monitoring tools suggesting policies from the statistical analysis of the monthly bills. Testimonies showed that applying a strict control on the mobile phone usage and on the contracts allowed cutting costs from 10 to 30%!

At one point, Nicolas Arsenault made a strange point. Here is what I remind from his talk:


Credits: smoothouse

Credits:Josh Bancroft
Deploying an iPhone application for your employees, when compared to a BlackBerry application, has a much lower cost because the employee already owns the device or he's more likely to buy it. With the employee paying for the phone, probably paying to get new phones regularly (like to move from a iPhone 3G to the latest 3GS), employers can just assume the data plans and can spend more resources on the application development, that at one point can be offered to the company customers. Another benefit is related to the technical support: phone owners stop annoying enterprises' help desk for their phone, they contact the manufacturers or the software vendors directly (they are on their own)...

If this approach have evident economical benefits, I disagree with it because:
  • In general, letting employees owning their work mobile phones cause problems when they leave the company. Most of employees have a non concurrence clause in their contract, usually valid 6 months after the employment contract termination. During that period, they cannot compete with their previous employer. Imagine a salesperson, an account manager, or a consultant who owns his phone number, who is referenced in the phone directories all his previous contacts. When these contacts want to deal with the company, who do you think they'll call: the mobile phone or the company front line? Now that these employees are out, they don't have to submit their phone bills anymore (as they did with their expenses reports). No one can detect the issues anymore… And this is without mentioning that the employee replacement or his colleagues have no way to get the mobile phone contact list to continue the business as usual!
  • In the company I worked, none let me use my own computer on their network! For the IT departments, this practice would raise too much security concerns. I even know cases that just installing a VPN software on your own machine install silently a bunch of monitoring tools that can mess up your systems (thanks to VirtualBox, it easy to limit these nasty side effects ;). In the old days, when phones were just stupid, just able to handle voice call and exchange text messages (SMS), the risk of phone infection by viruses were pretty negligible. The widely used phone operating system is Symbian, used on Nokia and Sony Ericsson phones, and J2ME is a common application framework, even on phones running Windows Mobile. If the identified viruses are not a lot, and if they are not some damageable (send on your behalf SMS to expensive services, for example), the newest smart platforms provide much more threats because the corresponding phones can host tons of applications. In such an environment, how can a company force software upgrades on systems it does not own? How can it force an employee to upgrade to a new hardware because the current one is compromised?
  • My last point is more ethical: how far companies should go with putting the burden on their employees? An iPhone costs around 800$. It's often offered around 200$ with a 3-year contract, which costs basically around 60$/month with a simple data plan. If the telecom operators (telco) subsidize so aggressively such a phone, they surely expect higher average revenue per user (ARPU). In addition to be linked to the telco for a long period of time (compare 3 years with the 3 to 6 months between technological evolution: 6 to 12 times longer!), employees have to support costs the company should assume. Usually, companies try to provide a comfortable work environment to get most of their employees, and that's fair to me: if the company gives more than the salary, employees are more likely to deliver better work. With the incitation of employees assuming the cost of the new mobile phones, I see a regression: companies give less while expecting more (reachable—possibly traceable—outside the office hours, for example). IMO, it's yet another example of a technological progress that might worsen fragile people condition...

Mobile payment and Near-field-communication (NFC)

Shortly on this topic, I want to mention presentation made by Daniel Martin for Atlas Telecom Mobile, David Robinson for Rogers Wireless, and Prakash Hariramani for Visa. They talked about an experiment conducted downtown Toronto where they were able, thanks to Motorola phones equipped with a NFC emitter and a good number of retailers there, to allow mobile payment over the mobile phone network. Visa's solution, called PayWave™ (MasterCard's one is called PayPass™), was inserted into the Motorola phone extension and allow consumers to pay for their purchase quite easily.

During the discussion, Mr. Robinson talked about Rogers Wireless approach being strictly based on standards. He mentioned the recent u-turn of telco who continued to invest in closed and proprietary solutions (like CDMA) and now move to standardized ones (like HSPA which is an upgrade of GSM, on the road to LTE—see example of Bell and Telus in Canada). Rogers Wireless' approach is then to work with the rest of the major industry players (Orange, Vodafone, etc.) to define a solution for anyone anywhere. Mr. Robinson talked about the possibility to define an extended SIM card (for Subscriber Identity Module; the card contains a micro-SIMCARD processor which manage very securely information). This new SIM card will have NFC capabilities and will be able to interact with contactless payment terminals. It's possible that these SIM cards will contain additional information like driver license identifier that police officers will be able to read, insurance number for the government agencies, etc. The phone will provide the interface to enable the data access, and smart phones with touch screens open the door to various and robust verification techniques.

Because more and more people are more attached to their phone than to their wallet, this approach will possibly be more convenient, more secure, and smaller (no more cash, business cards, credits cards, etc. ;) Who said that implanting the SIM under anyone's skin, a SIM that can unlock your phones, cars, houses, computers, etc, is just science fiction?

Telco business model in danger!

iBwave offers solutions improving in-building wireless coverage. Knowing the fact that 60%-80% of mobile usages are indoor, that telco have difficulties to boost the power or to multiply outdoor antennas near high density areas, these places stay mostly uncovered. Mr. Bouchard illustrated his point with the simulation of the poor performance of the traditional networks on the McGill campus—really amazing! In conclusion, iBwave sits in a very nice and promising niche ;)
The last point of interest to me came from Mario Bouchard, from iBwave.

To introduce his company activities, Mr. Bouchard shows two diagrams which made me think for a while. Let me try to reproduce them.


Mr. Bouchard states that 15 years ago, the innovation came from the network manufacturers: they invented the technology, others created cell phones to connect to the new network, some operators offered (very expensive) plans, and consumers (locked with long term contracts) tried to communicate.



Credits: DigitalAlan
Today, the order has been scrambled:
  • Thanks to the rapid technology evolution, designers of mobile devices can embed many types of sensors into communicant machines, and with the increasing miniaturization, such machines are pervasiveness!
  • Because of the reduced delay between big technology evolutions (think about the iPhone which is just 2 years old), consumers choose their devices carefully, and bargain more to get the best quality-price ratio.
  • Network manufacturers now to their best to provide networks than can deliver at the rhythm the devices can consume. When the European community created the Groupe Spécial Mobile (initial meaning of the GSM acronym, known now for Global System for Mobile communications) in 1982, we had to wait up to 1991 to experiment the first GSM network. GSM is also known the 2G technology. The EDGE (Enhanced Data rates for GSM Evolution, or 2.5G) has been introduced in 1999, the first 3G technology (HSPA/UMTS, and EV-DO/CDMA) has been delivered late 2001, and the coming 4G ones (LTE for Long Term Evolution or WiMax) are on the bench.
  • At the end of the line, now there are the telcos:
    • Investments are phenomenal
    • The competition is rude (not rude enough in Canada, IMO)
    • Customers are volatile and they always want the latest phone
    • They have to subsidize the phones to lower the barriers to entry
    • They have to provide the best coverage everywhere
    • Customers are quick to leverage social tools to complain about them
    • Customers don't respect the old rules (read: jailbreak their phone)
    • Customers are not the cash cows they used to be...

I don't think the telco future looks very nice. As traditional telecommunication service providers, they are more and more just Internet providers. Personally, I'm fine with communication on Internet (VOIP/SIP), with the possibility to stream on Internet (Qik.com, Layar.com), to receive instant messages (IM) instead of text messages (SMS). And look, when I've a chance to connect my phone to a wifi network, I'm happy to get a better connectivity while saving few bucks.

Interesting developments to follow, aren't they?

A+, Dom

Monday, October 5, 2009

Internationalization and my two-tiers-utils library

This is a follow-up article of Internationalization of GAE applications, which itself is part of the series Web Application on Resources in the Cloud.

In my initial article, I explained some hurdles of globalizing applications, especially the ones being implemented with many programming languages. In this article, I'm going to describe few use-cases and how my open-source library two-tiers-utils can ease the implementation. Here are the covered topics:
  1. Get the user's preferred locale
  2. Display messages in different locales
  3. Handle localized messages with different programming languages
  4. Generate the localized bundles per programming language
  5. Bonus

Get the user's preferred locale

For this use-case, let's only consider the Java programming language. Another assumption is the availability of the localized resources in the corresponding Java format (i.e. accessible via a PropertyResourceBundle instance).

In a Web application, the user's preferred locale can be retrieved from:
  • The HTTP headers:
    locale = ((HttpServletRequest) request).getLocale();
  • The HTTP session (if saved there previously):
    HttpSession session = ((HttpServletRequest) request).getSession(false);
    if (session != null) {
      locale = new Locale((String) session.getAttribute(SESSION_USER_LOCALE_ID));
    }
  • The record containing the user's information:
    locale = ((UserDTO) user).getPreferredLocale();
To ease this information retrieval, the two-tiers-utils library provides the domderrien.i18n.LocaleController class

Excerpt of public methods offered within domderrien.i18n.LocaleController

This class can be used in two situations:
  1. In a login form, for example, when we can just guess the desired locale from the browser preferred language list or from an argument in the URL.
  2. In pages accessible to identified users thanks to the HTTP session.
Usage example of the domderrien.i18n.LocaleController.detectLocale()
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<%@page
    language="java"
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    import="org.domderrien.i18n.LabelExtractor"
    import="org.domderrien.i18n.LocaleController"
%><%
    // Locale detection
    Locale locale = LocaleController.detectLocale(request);
%><html>
<head>
    <title><%= LabelExtractor.get("dd2tu_applicationName", locale) %></title>
    ...
</head>
<body>
    ...
    <img
        class="anchorLogo"
        src="images/iconHelp.png"
        width="16"
        height="16"
        title="<%= LabelExtractor.get("dd2tu_topCommandBox_helpIconLabel", locale) %>"
    />
    ...
</body>
</html>

The same message in different locales

The previous example introduces also a second class: domderrien.i18n.LabelExtractor. Being given an identifier, an optional array of Object references, and a locale, the get static method loads the corresponding string from the localized resource bundle.

Excerpt of public methods offered within domderrien.i18n.LabelExtractor
A series of localized entries like en:“Welcome {0} {1}”, fr:“Bonjour {2} {1}”, and ja:“お早う {0}{1}” can be easily invoked with a simple command like: LabelExtractor.get("welcome_message", new Object[] { user.getFirstName(), user.getLastName() }, user.getLocale());.

The same message used from different programming languages

Java is a pretty neat language with a large set of editors and code inspectors. But Java is not the only languages used for Web applications. If the two-tiers-utils library provides nice Java features, the delivery of the same library interfaces for the programming languages JavaScript and Python libraries makes it way more valuable!

Code of the domderrien.i18n.LabelExtractor.get() method for the JavaScript language.
(function() { // To limit the scope of the private variables

    /**
     * @author dom.derrien
     * @maintainer dom.derrien
     */
    var module = dojo.provide("domderrien.i18n.LabelExtractor");

    var _dictionnary = null;

    module.init = function(/*String*/ namespace, /*String*/ filename, /*String*/ locale) {
        // Dojo uses dash-separated (e.g en-US not en_US) and uses lower case names (e.g en-us not en_US)
        locale = (locale || dojo.locale).replace('_','-').toLowerCase();

        // Load the bundle
        try {
            // Notes:
            // - Cannot use the notation "dojo.requirelocalization" because dojo parser
            //   will try to load the bundle when this file is interpreted, instead of
            //   waiting for a call with meaningful "namespace" and "filename" values
            dojo["requireLocalization"](namespace, filename, locale); // Blocking call getting the file per XHR or <iframe/>

            _dictionary = dojo.i18n.getLocalization(namespace, filename, locale);
        }
        catch(ex) {
            alert("Deployment issue:" +
                    "\nCannot get localized bundle " + namespace + "." + filename + " for the locale " + locale +
                    "\nMessage: " + ex
                );
        }

        return module;
    };

    module.get = function(/*String*/key, /*Array*/args) {
        if (_dictionary == null) {
            return key;
        }
        var message = _dictionary[key] || key;
        if (args != null) {
            dojo.string.substituteParams(message, args);
        }
        return message;
    };

})(); // End of the function limiting the scope of the private variables

The following piece of code illustrates how the JavaScript domderrien.i18n.LabelExtractor class instance should be initialized (the value of the locale variable can come from dojo.locale or a value injected server-side into a JSP page) and how it can be invoked to get a localized label.

Usage example of the domderrien.i18n.LocaleController.get()
(function() { // To limit the scope of the private variables

    var module = dojo.provide("domderrien.blog.Test");

    dojo.require("domderrien.i18n.LabelExtractor");

    var _labelExtractor;

    module.init = function(/*String*/ locale) {
        // Get the localized resource bundle
        _labelExtractor = domderrien.i18n.LabelExtractor.init(
                "domderrien.blog",
                "TestBundle",
                locale // The library is going to fallback on dojo.locale if this parameter is null
            );

        ...
    };

    module._postData = function(/*String*/ url, /*Object*/ jsonParams) {
        var transaction = dojo.xhrPost({
            content : jsonParams,
            handleAs : "json",
            load : function(/*object*/ response, /*Object*/ioargs) {
                if (response == null) {
                    // Message prepared client-side
                    _reportError(_labelExtractor.get("dd2tu_xhr_unexpectedError"), [ioargs.xhr.status]);
                }
                if (!response.success) {
                    // Message prepared server-side
                    _reportError(_labelExtractor.get(response.messageKey), response.msgParams);
                }
                ...
            },
            error : function(/*Error*/ error, /*Object*/ ioargs) {
                    // Message prepared client-side
                _reportError(error.message, [ioargs.xhr.status]);
            },
            url : url
        });
    };

    var _reportError = function(/*String*/ message, /*Number ?*/xhrStatus) {
        var console = dijit.byId("errorConsole");
        ...
    };

    ...

})(); // End of the function limiting the scope of the private variables

The following series of code excerpts show the pieces involved in getting the localized resources with the Python programming language.

LabelExtractor methods definitions from domderrien/i18n/LabelExtractor.py
# -*- coding: utf-8 -*-

import en
import fr
 
def init(locale):
    """Initialize the global dictionary for the specified locale"""
    global dict
    if locale == "fr":
        dict = fr._getDictionary()
    else: # "en" is the default language
        dict = en._getDictionary()
    return dict

Sample of a localized dictionary from domderrien/i18n/en.py
# -*- coding: utf-8 -*-
 
dict_en = {}
 
def _getDictionary():
    global dict_en
    if (len(dict_en) == 0):
        _fetchDictionary(dict_en)
    return dict_en
 
def _fetchDictionary(dict):
    dict["_language"] = "English"
    dict["dd2tu_applicationName"] = "Test Application"
    dict["dd2tu_welcomeMsg"] = "Welcome {0}."
    ...

Definitions of filters used by the Django templates, from domderrien/i18n/filters.py
from google.appengine.ext import webapp
 
def get(dict, key):
    return dict[key]
 
def replace0(pattern, value0):
    return pattern.replace("{0}", str(value0))
 
def replace1(pattern, value1):
    return pattern.replace("{1}", str(value1))
 
...
 
# http://javawonders.blogspot.com/2009/01/google-app-engine-templates-and-custom.html
# http://daily.profeth.de/2008/04/using-custom-django-template-helpers.html
 
register = webapp.template.create_template_register()
register.filter(get)
register.filter(replace0)
register.filter(replace1)
...

Django template from domderrien/blog/Test.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>{{ dictionary|get:dd2tu_applicationName }}</title>
    ....
</head>
<body>
    ...
    <div class="...">{{ dictionary|get:dd2tu_welcomeMsg|replace0:parameters.loggedUser }}</div>
    ...
</body>
</html>

Test handler from domderrien/blog/Test.py
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template

def prepareDictionary(Request):
    locale = request.get('lang', 'en')
    return LabelExtractor.init(locale)

class MainPage(webapp.RequestHandler):
    def get(self):
        parameters = {}
        parameters ['dictionary'] = domderrien.i18n.LabelExtractor.init(self.request)
        parameters ['loggedUser'] = users.get_current_user()
        path = os.path.join(os.path.dirname(__file__), 'domderrien/blog/Test.html')
        self.response.out.write(template.render(path, parameters))

application = webapp.WSGIApplication(
    [('/', MainPage)],
    debug=True
)
 
def main():
    webapp.template.register_template_library('domderrien.i18n.filters')
    run_wsgi_app(application)
 
if __name__ == "__main__":
    main()

Generate the localized bundles per programming language

In my previous post Internationalization of GAE applications, I suggest to use a dictionary format that would be programming lnaguage agnostic while being known by translator: TMX, for Tanslation Memory eXchange.

Snippet of a translation unit definition for a TMX formatted file
<tu tuid="dd2tu_welcomeMessage" datatype="Text">
 <tuv xml:lang="en">
  <seg>Welcome {0}</seg>
 </tuv>
 <note>{0} is going to be replaced by the logged user's display name</note>
 <prop type="x-tier">dojotk</prop>
 <prop type="x-tier">javarb</prop>
 <prop type="x-tier">python</prop>
</tu>

The two-tiers-utils library provides a Java runtime domderrien.build.TMXConverter that generates the resource bundles for Java/JavaScript/Python. If a simple series of XSL-Transform runs can do the job, the TMXConverter does a bit more by:
  • Comparing the modification dates of the generated files with the TMX one to generate them only if needed
  • Check the uniqueness of the label keys
  • Generate the list of supported languages
Invoking the TMXConverter runtime from an ant build file is very simple, while a bit verbose:

Ant target definition invoking the TMXConverter
<target name="step-tmx-convert">
    <mkdir dir="${temp.dir}/resources" />
    <mkdir dir="src/WebContent/js/domderrien/i18n/nls" />
    <java classname="domderrien.build.TMXConverter" fork="true" failonerror="true">
        <classpath refid="tmxconverter.classpath" />
        <classpath location="${temp.dir}/resources" />
        <jvmarg value="-Dfile.encoding=UTF-8" />
        <arg value="-tmxFilenameBase" />
        <arg value="${dd2tu.localizedLabelBaseFilename}" />
        <arg value="-sourcePath" />
        <arg value="${basedir}\src\resources" />
        <arg value="-jsDestPath" />
        <arg value="${basedir}\src\WebContent\js\domderrien\i18n\nls" />
        <arg value="-javaDestPath" />
        <arg value="${temp.dir}/resources" />
        <arg value="-languageFilenameBase" />
        <arg value="${dd2tu.languageListFilename}" />
        <arg value="-buildStamp" />
        <arg value="${dd2tu.stageId}" />
    </java>
    <native2ascii
        src="${temp.dir}/resources"
        dest="${temp.dir}/resources"
        encoding="UTF8"
        includes="*.properties-utf8"
        ext=".properties"
    />
    <copy
        file="${temp.dir}/resources/${dd2tu.localizedLabelBaseFilename}.properties"
        tofile="${temp.dir}/resources/${dd2tu.localizedLabelBaseFilename}_en.properties"
    />
    <mkdir dir="src/WebContent/js/domderrien/i18n/nls/en" />
    <copy
        file="src/WebContent/js/domderrien/i18n/nls/{dd2tu.localizedLabelBaseFilename}.js"
        todir="src/WebContent/js/domderrien/i18n/nls/en"
    />
</target>

With the TMX file as the source of thruth for the label definitions, it is just a matter of altering the value a <prop/> tag and running the build once again to move one label definition from one programming language to another. No more error prone copy-and-paste of text between different file formats!

Excerpt of the generated Java resource bundle
bundle_language=English
unit_test_sample=N/A
dd2tu_applicationName="Test Application"
dd2tu_welcomeMessage=Welcome {0}
...
x_timeStamp=20091001.1001

Excerpt of the generated JavaScript resource bundle
({bundle_language:"English",
unit_test_sample:"N/A",
dd2tu_applicationName:"Test Application",
dd2tu_welcomeMessage:"Welcome ${0}",
...
x_timeStamp:"20091001.1001"})

Excerpt of the generated Python class definition
# -*- coding: utf-8 -*-
 
dict_en = {}
 
def _getDictionary():
    global dict_en
    if (len(dict_en) == 0):
        _fetchDictionary(dict_en)
    return dict_en
 
def _fetchDictionary(dict):
    dict["_language"] = "English"
    dict["dd2tu_applicationName"] = "Test Application"
    dict["dd2tu_welcomeMsg"] = "Welcome {0}."
    ...
    dict["x_timestamp"] = "20091001.1001"

Bonus

The TMXConverter being part of the build process and going over all localized TMX files, it generates the list of supported languages.

JSP code fetching a HTML &ltselect/> box with the list of supported languages
<span class="topCommand topCommandLabel"><%= LabelExtractor.get("rwa_loginLanguageSelectBoxLabel", locale) %></span>
<select
    class="topCommand"
    dojotType="dijit.form.FilteringSelect"
    id="languageSelector"
    onchange="switchLanguage();"
    title="<%= LabelExtractor.get("rwa_loginLanguageSelectBoxLabel", locale) %>"
><%
    ResourceBundle languageList = LocaleController.getLanguageListRB();
    Enumeration<String> keys = languageList.getKeys();
    while(keys.hasMoreElements()) {
        String key = keys.nextElement();%>
        <option<% if (key.equals(localeId)) { %> selected<% } %> value="<%= key %>"><%= languageList.getString(key) %></option><%
    } %>
</select>

The following figures illustrates the corresponding code in action.

Part of a login screen as defined with the default (English) TMX file.


Part of a login screen as defined with the French TMX file.


Conclusion

The two-tiers-utils library is offered with the BSD-like license. Anyone is free to use it for his own purposes. but I'll appreciate any feedback, contribution, and feature requirement.

See you on github.com ;)
“May the fork be with you.”

A+, Dom

Friday, September 18, 2009

Progress update

Almost three months without publishing anything! I am definitively not proud of this score...

I have been busy on three fronts:
  1. At work, I continue working on mobile related development. The easiest platform to play with is the Android one (see my post on Android Dev Phone 1) and I thrilled to see new handsets being made available, like the HTC Dream and Motorola Cliq, with their respective HTC Sense and Moto BLUR custom UIs (see videos below). Even if its SDK is not as rich as Android's one, even if the gadgets are not as polished as Android's ones, I also like developing for BlackBerry phones, like the BlackBerry Storm.
  1. For my side project, running on Google App Engine infrastructure, it is progressing very well. Today, I reached a milestone: the first part of [still a secret] runs live. In terms of coding, it represents ~6,000 lines for the source files and ~12,000 lines for the test. I have always considered source vs test as being 50-50; it seems I should re-evaluate the balance to 1/3-2/3 ;)


    Thanks to different contributors, I have developed a series of mock classes allowing to test transactions with BigTable, the database used by App Engine. A really neat piece of code I am going to describe here later.
  1. On the social side, I am working with the board of Diku Dilenga Canada (board I am member of) to move as its Executive Director (still as a volunteer). The move has been inspired by Jean-Pierre Tchang, founder of IRIS Mundial. I hope this update will make Diku Dilenga (Canada) as successful as IRIS Mundial.

    I had a lot of activities on this front recently: a fundraiser thanks to Louis Lamontagne walking between Saint Jean Pied de Port (France) and Santiago de Compostela (Spain), a trip of 780km, a first series of computers to be shipped to Kananga, Democratic Republic of the Congo, etc.

    The plan to link the [still a secret] project with Diku Dilenga activities have been formally approved by the board during the summer. This side, there is a possibility I will do a presentation to the 2010 Africa/Middle East Regional Microcredit Summit (AMERMS) to be held in Nairobi, Kenya April 7-10, 2010. It would be nice to participate, isn't it?

Stay tuned, I should be back in few days with technical information about unit testing transactional code on Google App Engine ;)

A+, Dom
--
Videos:


HTC Hero, the first phone with the HTC Sense, a customized UI scheme on the top of Android.



Motorola Cliq, the first phone with Moto BLUR, a customized UI scheme on the top of Android.



BlackBerry Storm, first BlackBerry phone with a touch screen

Friday, June 12, 2009

JavaOne Conference


Duke and myself ;)
It has been a long week in San Francisco while I was attending the JavaOne conference. Sessions started at 8:30 AM and finished after 9:30 PM!

Among all reviews that have been published, read this ones: Community day on Monday, Conference day 1 on Tuesday, Day 2 on Wednesday, Day 3 on Thursday, and Day 4 on Friday. Sun's website contains also a complete list of conference articles. Pictures of the event are available on Sun's Photo Center website.

I went there with two objectives:
  • See JavaFX in action;
  • Look at all efforts around JavaME and the MSA initiative.
JavaFX technology stack


Eric Klein, Vice President, Java Marketing, Sun Microsystems
I came without preconceived idea around JavaFX, just with my background as a JavaScript/Java developer and my knowledge of Flex and AIR.

The first deception came from looking at the scripting language: Man! they invented yet another language :( For sure, the JavaFX scripting language is nicely handled by NetBeans 6.5+ (except the code formatting) but new paradigms and new conventions are a big barrier to adoption to me. Can you figure out what the following code is doing?

public class Button extends CustomNode {
  public var up: Node;
  content: Node = up;
  public override function create(): Node {
    return Group {
      content: bind content
    }
}
If the visual effects to animate texts, pictures, and video are really great, the native support of a sound library is really missing! For example, I would expect to be able to synchronize KeyFrame objects with sound tracks but the KeyFrame.time attribute has to be set manually—not very flexible when it's time to change the sound track...

The pros are:
  • A coming visual editor to prepare clips;
  • A large library of image and video effects;
  • The multi-platform support, especially for mobile devices.
Platform dependent packaging is nicely handled: NetBeans project properties pane provides choices among {Standard Execution, Web Start Execution, Run in Browser, Run in Mobile Emulator}. As a Web developer, I am just sorry to see that the application window size cannot be expressed in percentage, that the auto-resizing is handled transparently, which is not better than usual Flex applications.

Last point: I have not seen how to invoke JavaFX handlers from JavaScript ones, and vice-versa, when the application is deployed to run in browsers. If you have a source for these information, please, drop the link in a comment.

JavaME technology stack


Christopher David, Rikko Sakaguchi and Patrik Olsson
during Sony Ericsson General Session
This was definitively the most interesting domain to me. During one session, it has been announced that 60% of shipped mobile devices in 2008 are Java-enabled. In 2009, the market share should grow up to 70%. In relation with the iPhone, Scott McNeally did this joke: the possible future Sun owner Larry Ellison might succeed to open the iPhone platform to Java because he is a well known friend of Steve Jobs.

Compared to the Java Standard Edition (J2SE) and the Java Enterprise Edition (J2EE), the Java Mobile Edition (J2ME) has more external contributors. Under the Mobile Service Architecture (MSA) initiative, a lot of mobile device manufacturers and telecommunication operators participate to the Java Community Process (JCP) to deliver Java Specification Requests (JSRs). Note that MSA itself is defined as a JSR: JSR 248. As of today, most of the recent phones are MSA 1.1 compliant (this is a mandatory requirement for the telco Orange, for example). Nokia and Sony Ericsson have shipped a lot of MSA-compliant handsets, LG, Samsung, and Motorola shipped very few ones. The standard MSA 2 (JSR 249) is being finalized and it contains the promising JSRs:
Additional APIs I imagine many developers are looking forward: JSR 257 Contactless communication API and JSR 229 Payment API.


MSA evolution and its JSR set
(from the MSA specification documentation)
(click to enlarge)

All major manufacturers have opened or are opening "App Stores" a-la Apple. They open also their development platforms. More companies will be able to adapt their software offering to mobile devices. Even Sony allows anyone to write application to run in Blu-ray Disc players. The main difficulty on developer-side is the fragmentation: there is no standard API allowing to discover the features supported by a device! Developers have to rely on each manufacgturer's feature list and on exception handling :(

The Blackberry platform is pretty well controlled and should be easy to develop on. Then follows Sony Ericsson which provides consistent phone classes (i.e. what works for one phone in the JP 8.4 class work for all phones in that class). The delivery of the Sun JavaME SDK 3.0 containing many third-party emulators (even one for Windows Mobile devices) added to better on-device deployment and on-device debugging capabilities, should motivate more and more developers.

I have not enough experience with Android (just got one Android dev phone two weeks ago) to compare it to the JavaME technology stack. I don't know neither about Symbian (Nokia devices) or LiMo (Motorola devices) platforms.

Exhibition hall

Besides the visit of mobile device manufacturers (RIM and Ericsson) booths, I visited:
  • Sun's project Fuji (open ESB) with a Web console using <canvas/> from HTML5, like Yahoo! Pipes.
  • Convergence, the Web client for Sun's communication suite, built on the top of Dojo toolkit ;)
  • INRIA (French national R&D lab) for its static code analysis Nit.
  • Isomorphic Software for its SmartClient Ajax RIA System.
  • eXo Platform (on OW2 booth) for its eXo Portal offering.
  • Liferay, Inc. for its eponymous portal.
Other discoveries


James Gosling and myself ;)
I attended very good presentations, like the opening keynote which was fun. Among the good presenters, I can mention (ordered alphabetically):
If you have a Sun Developer Network (SDN) account (by the way, it's free), you can view the slides of the technical sessions at: http://developers.sun.com/learning/javaoneonline/.

Special mention

I want also to mention the call to developers by MifOS people who have been awarded by James Gosling during the Friday morning general session. This organization develops open source software for microfinance institutions (MFIs) to help them managing loans and borrowers (see demo). Really nice initiative started by the Grameem Bank!

Excerpt from James Gosling Toy Show report:
Microfinancing Through Java EE Technology

Gosling next introduced a group whose great innovation with Java technology was social and not technical. Sam Birney, engineering manager and Mifos alumnus, and Van Mittal-Hankle, senior software engineer at the Grameen Foundation, took the stage to receive Duke's Choice awards for their work using Java EE to serve 60,000 clients worldwide in microfinancing, a highly successful means of helping poor people get small loans and start businesses.

Mifos is open-source technology for microfinance that is spearheaded by the Grameen Foundation.

"Sometimes excellence comes not from technical innovation but in how technology is used," explained Gosling. "This is an example of using Java technology to really improve people's lives."

With an estimated 1.6 billion people left in the world who could benefit from microfinance, the men put out a call for volunteers to contribute to the Mifos project..

What's next?

What are my resolutions? Get a Mac as the development platform (Eclipse works on MacOS and I can use a Win7 image within VirtualBox), and start development on Java enabled phone (at least MSA 1.1 compliant).

A+, Dom

Wednesday, June 10, 2009

Internationalization of GAE applications

Costs of badly planned internationalization

In my experience, internationalizing an application is very expensive if it is not planned upfront.

The first source of costs is due to developers who are used to lazily hard-coding labels. Extracting them at the end of the development process is always error prone because developers may have done assumptions on exact labels. Good regression test suites can help detecting such situations but they cannot avoid the cost of the required fix and its corresponding test runs. With late label extraction, developers without enough context tend to add extra dictionary entries. With the label extraction near the release milestone, developers in a rush and without the initial context tend also to produce non documented dictionaries—that is limit their re-usability and increase the difficulty of possible defect fixings.

Non localized Java code
protected String formulatePageIndex(int index, int total) {
    String out = "Page " + index;
    if(0 < total) {
        out += " of " + total;
    }
    return out;
}

In the example above, I have seen the extraction of the labels Page and of instead of Page %0 and Page %0 of %1. The quick extraction leads to the impossibility to invert the two arguments! Think about the people naming conventions: in many countries, the last name is displayed before the first name, in others the first name precedes the last name (%last %first compared to %first %last), and in Japan both names are printed without separator in between (%last%first).

Localized Java code
protected String formulatePageIndex(int index, int total) {
    // Get the resource bundle already initialized for the correct locale
    ResourceBundle rb = getCurrentResourceBundle();
    // Prepare values
    Object[] values = Object[] {Integer.valueOf(index), Integer.valueOf(total)};
    // Get the right localized label
    String label = rb.getString("PageIndexLabel_Page");
    if(0 < total) {
        label = rb.getString("PageIndexLabel_PageOf");
    }
    // Return the localized label with injected values
    return Message.format(label, values);
}

The second source of costs is due to the missed opportunities of deploying localized builds early in the development process. In Agile environments, we expect to get runnable builds on regular basis (at the end of each sprints, each 4 to 6 weeks, for example). And these fool-proof builds can be demoed to customers to get early feedback. If the development organization can work with translators iteratively, there are a lot of chance to detect localization defects while their fixing cost is not too high. In the past, I have seen product developments hugely hit when bi-directional languages (like Arabic and Hebrew) had been introduced...

Different aspects of the internationalization

Internationalization (i18n) [1] has two aspects:
  • The translation of the labels;
  • The localization (l10n) of these labels.
The localization takes into account the language and the country, sometimes with variants in a country. For example, the Spanish language spoken 19 identified countries. In Mexico (ES_MX), the language is slightly different from the one generally spoken in Spain (ES or ES_ES). In Spain, there are many regional languages like the Catalan (CA_ES). The different locales are normalized by the Unicode consortium (ISO-639 and ISO-3166). Codes are composed of a sequence of two letters for the language plus two letters for the country plus two letters for the region. If letters are missing after the language, most of programming languages fallback on common defaults.

In order to ease application localizations, Unicode references a Common Locale Data Repository (CLDR) [2]. This repository is used and updated by many companies like IBM, Sun, Microsoft, Oracle, etc. The repository describes rules on how to:
  • Localize currencies;
  • Localize metrics (distance, speed, temperature, etc.);
  • Localize dates and calendars.
As of today, I think only timezone definitions are still not centrally managed... This is especially bad because conversions between Universal Time (UTC) dates and local dates are operating system dependent (Sun Solaris have small differences with Microsoft Windows, for example). Many tools use the Unicode CDLR information. For example, each release of the Dojo toolkit use its information to provide the Calendar widget for 27 locales [3].

Internationalization with different programming languages

Almost all programming languages have ways to facilitate application globalization. Java provide resource bundles (*.properties file), Microsoft .Net has resource files (.rc files), Python has dictionaries, etc. If JavaScript lacks of native support for globalization, some libraries offer various support. To my knowledge, Dojo toolkit is the first providing a full support.
If developing an application on Google App Engine infrastructure can be done with only one programming language, Python or Java as this time of writing, it is highly possible that developers will use some JavaScript libraries to speed up their development. This is without counting the delivery of a similar program front-end as a native application (made with Adobe AIR, Microsoft .Net, C/C++, Groovy, etc.).

In different situations, I have seen developers moving manually label definitions from one environment to another one. Sometimes, definitions were left over, cluttering the system. In Agile environments, developers should focus on the requirements for the current sprint, leaving some tuning for later sprints. For example, at one point during the development, some labels defined in a JavaScript bundle might be moved to a Java bundle because the localization will be done server-side into a JSP file.

My solution is to put all labels in one localized central repository. The dispatch among the different programming languages is done at build time. When I looked for this repository format, my solution was selected against the following criteria:
  • Easily editable;
  • Has a standard format;
  • Usable by static validation processes;
  • Has excellent re-usability factors;
  • Easily extensible to new programming languages.
I chose the TMX format (TMX for Translation Memory eXchange [4]). This is an XML based format (good for edition, extensibility, and use by static validation tools) which has been defined to allow translation memory export/import between different translation tools like DejaVu. The XlDiff format would have been another good candidate.

The following table illustrates the flow of interactions between the different actors in a development team. This sequence diagram shows that, once the developers have delivered a first TMX file, testers and translators can work independently to push tested and localized builds to the customer. As explained later, if developers tune the TMX entries without updating the labels themselves, translators and testers (at least from the l10n point-of-view) can stay out of the loop—only steps [1, 7, 8] are replayed.

Simplified view of the overall interaction flow
Developers Testers Translators Build process End-users
1. Write labels in one language into the TMX file. These labels are extracted from design documents.
2. Generate the application with for one locale.
3. Produce a generic bundle to identify non extracted labels.
4. Generate the application with for two locales.
5. Use the application in one locale (switching to the test language is hidden).
6. Use the initial TMX to produce n localized TMXs.
7. Generate the application with for 2 + n locales.
8. Can use the application in 1 + n locales.


The following code snippet shows how an entry into the base TMX file is defined.

Snippet of a translation unit definition for a TMX formatted file
<tu tuid="entry identifier" datatype="Text">
 <tuv xml:lang="locale identifier">
  <seg>localized content</seg>
 </tuv>
 <note>contextual information on the entry and relations with other entries</note>
 <prop type="x-tier">dojotk</prop>
 <prop type="x-tier">javarb</prop>
</tu>

The key features of the TMX format are:
  • The format can be validated with an external XSD (XML Schema Description);
  • One entry (tu: translation unit) can contain many localized contents (tuv: translation unit value);
  • Developers have a normalized placeholder (<note/>) to register contextual information;
  • Extensions are used by the build process to target the type of resource bundle to receive the localized label.
With such an approach, I have seen a drastic reduction of translation mistakes, especially thanks to the <note.> tag. Sometimes, graphical elements contain inter-related labels that cannot be grouped under a generic entity. The following set of elements illustrates the situation. The TMX approach saves translators headaches because they are simply informed about the relation between four entities.


The conversion from the TMX to the various resource bundles is done by an XSL-Transform. With the continuous integration handled by ant, the corresponding task generates the output after having appended the XSLT file coordinates to a copy of the TMX file and after asked for the transformation with the corresponding <xsl/> ant task. Depending on the machine performance, depending on the TMX file size, I found that the process can be time consuming. If this is your case too, I suggest you write your own little Java program to handle it. You can also use mine ;)

Stylesheet transforming label definitions for the Dojo toolkit
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text" />
 <xsl:template match="/tmx/body">
  {
  <xsl:for-each select="tu">
    <xsl:for-each select="prop">
      <xsl:if test="@type='x-tier' and .='dojotk'">
        "<xsl:value-of select="../@tuid" />":"<xsl:value-of select="../tuv/seg" />",
      </xsl:if>
    </xsl:for-each>
  </xsl:for-each>
  "build", "@rwa.stageId@"}
 </xsl:template>
</xsl:stylesheet>

Use of the stylesheet above to convert Dojo toolkit related definitions from the TMX files by an Ant task [5]
<target name="convert-tmx">
  <style
    basedir="src/resources"
    destdir="src/resources"
    extension=".js"
    includes="*.tmx"
    style="src/resources/tmx2dojotkxsl"
  />
</target>

A+, Dom
--
Sources:
  1. Introduction to internationalization and localization on Wikipedia.
  2. Unicode Common Locale Data Repository (CLDR).
  3. Dojo toolkit API: dojo.cldr, dojo.i18n, private dijit._Calendar, dojox.widget.Calendar, and dojox.widget.DailyCalendar.
  4. Definition of the Translation Memory eXchange (TMX) format.
  5. Reference of the XSLT/tyle task for Ant scripts.

Friday, June 5, 2009

Android Dev Phone 1 Setup

To start investigations on mobile application development for Compuware, I have just acquired a first Android Dev Phone, also known as G1 [1]. Last week at Google I/O, Vic Gundotra delivered an Oprah event by offering to the audience a second generation Android phone, also known as G2 or HTC Magic [2, 3]. The G1 is more limited but it is still the only Android platform legally available.

With a bit of luck, few of 18 new phones Google expects [4] will be made available to developers during the year.

I have been able to activate the phone with the Fido pre-paid plan (10 $/month) [5]:
  • I inserted the SIM card as illustrated into the documentation, put the the battery in place, and connected the phone to the AC adapter.
  • Before signing in with my Google account, I had to create an Access Point Network (APN) entry:
    • Name: Fido
    • APN: internet.fido.ca
    • Username: fido
    • Password: fido
    • MCC: 302
    • MNC: 37
  • In some forums, it is reported that new Fido SIM cards use 370 as the MNC value.
  • A post of Olivier Fisher's blog [6] gives also the coordinates to connect to Rogers network, another GSM provider in Canada.
  • To limit interference, I deleted all pre-loaded APN entries (related to T-Mobile networks).
  • At one point, a popup asked me to enable data transfers. It is important to enable it and to activate the Data roaming, disregard how expensive are the costs for a prepaid plan.
  • Then I specified my Google account credentials and let the phone contacting Google servers via Fido network.
  • Once the activation has been successfully reported, I disabled the Data roaming, even before the synchronization of the applications {GMail, Contacts, Calendar} ended. The impact on my plan should be limited ;)
  • Then I added the description of my home Wi-Fi network.
  • I found the MAC address of the phone in the menu Settings > About phone > Status, with the entry near the end of the list. I used it to let my Wi-Fi controller accepting connections from the phone.
  • At this step, I was able to use my phone for regular calls over Fido network, and for data transfers over my Wi-Fi network.
The phone comes installed with Android 1.0 installed. I will blog later about updating the phone OS to the 1.5 version (also known as Cupcake)...

Update 2009/06/16:
Instructions on how to upgrade the Android dev phone to Android 1.5 is pusblished on HTC website: Flashing your Android Dev Phone with a Factory System Image.

Update 2009/07/15
Because of some restrictions to access Internet from the office, I have decided to pay for a 1GB/month data plan with Fido (30$/month). The activation has been made pretty quickly but none mentionned the following limitation:
  • On HTC website, you can see the network specifications for the G1: HSPA/WCDMA (US) on 1700/2100Mhz.
  • Fido/Rogers GSM only operates on 850/1900Mhz, so there's no possibility to go at a 3G speed in Canada!
Using this phone mainly for development purposes, it is not a blocking issue. It is just sad to not benefit from a better bandwidth...

A+, Dom
--
Sources:
  1. Order the Android dev phone 1 from Android Market.
  2. Techcrunch reports the Oprah moment by Vic Gundotra.
  3. G2 review by MobileCrunch
  4. Google expects 18 to 20 new phones on the market by the end of 2009.
  5. Fido pre-paid plan.
  6. Android G1 Phone in Canada on Rogers by Olivier Fisher. Posted comments are especially useful.

Sunday, May 31, 2009

Le cap des 40 ans !

1969-2009
Me voilà 40 ans révolu ! Ce 30 mai, j'ai eu le plaisir de recevoir de proches amis que j'ai rencontrés au long de mes 10 années passées à Montréal. Ce n'est pas tant le chiffre des 40 ans qui est magique, c'est plus celui des 10 ans et tous les événements qui se sont produits depuis.

En effet il y a 10 ans, ma femme Sophie et moi avons pris un aller simple Nantes-Montréal avec 2 valises, pendant que le reste de nos affaires traversait l'atlantique en bateau. Rapidement, nous avons loué un haut de duplex à Ville LaSalle. Et après quelques temps de vacances au Lac Saint Jean et dans le Saguenay, nous nous sommes mis à la recherche d'un boulot.

Pour moi, c'est la compagnie CS&T (plus tard renommée en Steltor) qui a décidé de me faire confiance. J'y ai d'abord réalisé une console d'administration Web pour le serveur de calendrier, un outil de collaboration instantanée et distribué avant l'heure. Je dois en partie mon recrutement à Patrice Lapierre, toujours employé par Oracle à Montréal ;)

Une fois acquise Steltor par Oracle, j'ai travaillé pour une équipe californienne. Mon expérience des applications Web a retenue d'Attila Bodis. Avec quelques autres, nous avons construit un environnement pour une application Web 2.0 avant même que le buzz existe ! Une première version a été commercialisé. Les débuts de la seconde version étaient très prometteurs, notamment grâce à la vision d'Amir Borna, et avec la bonne coordination de Vince Wu. Mais des aléas politiques ont cependant plombé le projet et l'équipe s'est quasiment dissoute...

Quand la semaine dernière, j'ai vu la présentation de Google Wave par Lars Rasmussen et Stephanie Hannon, j'ai trouvé beaucoup de similitudes avec ce que nous avions sur la planche de travail pour OCS (Oracle Collaboration Suite) en 2005-2006. Parce qu'Oracle évolue dans le monde de l'entreprise, les médias comme le courriel ou l'événement de calendrier devaient perdurer—j'attends à ce propos de voir quelle stratégie Google va offrir aux entreprises pour migrer leur legacy systems. Il y a sûrement des concepts OCSiens de l'époque qui s'appliqueraient à Wave aujourd'hui. Chapeau bas les gens d'en bas ;) car vous avez réussi où ceux d'en haut n'ont pas su persévérer !

C'est alors qu'IBM Rational m'a offert un poste d'architecte technique. Le projet n'était pas sensationnel (il est même mort depuis), mais les gens rencontrés et l'expérience de Big Blue ont été sans pareil. Chapeau à Steven Milstein, à Toufik Bahloul et Arthur Ryman.

Présentement, je suis chez Compuware dans un groupe d'experts multi-disciplinaires et j'aide à définir les stratégies de l'entreprise dans les domaines des applications Web et des mobiles. Évoluant dans une compagnie très conservatrice, certaines situations ont tendance à me frustrer royalement, mais les résultats que j'arrive à obtenir par ma persévérance font que finalement l'équilibre est positif. Je ne sais pas combien de temps cela va durer, mais j'en apprécie la valeur. Et je remercie Paul Czarnik, Mathieu Pageau et Abdel Belkarsi.

Du côté extra-professionnel, il y a eu mon passage dans le groupe RÉSULTATS, merci Sunnie Kim, pour lequel l'action des citoyens fait pression sur le gouvernement fédéral pour que l'aide au développement vise les plus pauvres. Maintenant, je suis impliqué dans Diku Dilenga au Canada, dont je remercie le co-fondateur de l'organisation en République démocratique du Congo révérend Tambwe Musangelu.

Je n'oublie pas non plus ma relativement récente entrée dans le monde du karaté au Dojo de Don Lorenzetti, pour le style Chito Ryu. C'est d'autant plus passionnant que toute la famille Derrien y participe : Sophie, Erwan, Goulven et moi-même chacun à des horaires différentes deux fois par semaine.

Merci Sophie, mes enfants, et mes amis pour votre amitié et les richesses que vous m'apportez. J'espère être à la hauteur et vous donner autant de plaisir.

A+, Dom

Wednesday, May 6, 2009

Revue du livre « le dip » de Seth Godin

Une fois n'est pas coutume, je prends le parti d'écrire une entrée de blogue en français. Ce qui me motive à le faire c'est la récente lecture du livre de Seth Godin qui n'était disponible qu'en français à la bibliothèque de mon quartier. Depuis quelques années, j'utilise la langue de Shakespeare mise au goût du jour par Ron McDonald pour mon travail. En plus, 95 % de mes sources d'information sont anglophones. C'est donc assez déroutant de lire du Seth Godin en français, mais aussi très rafraîchissant.

Le thème de ce court livre publié en 2007 (100 pages au format d'un livre de poche) traite de la description de différents profils d'activité et sur les stratégies à adopter pour avoir du succès. Seth est très honnête sur la notion de succès : s'il incite le lecteur à être le « meilleur du monde », il précise que ce « monde » est propre à chacun et qu'il peut varier au court du temps. Voici les trois profils d'activités qu'il identifie :

  • Il y a le profil que tous connaissent quand ils démarrent une nouvelle activité, où l'excitation rend les choses faciles. Cette phase est suivie d'un creux où l'évolution est lente et pénible. Quand l'obstination porte ses fruits, la sortie de ce creux est souvent couronnée de succès. Seth appelle ce creux le dip.
  • Il y a le profil de la falaise où les activités progressent bien jusqu'à s'écrouler totalement. Seth fait le parallèle avec le monde de la presse écrite : tout allait bien jusque récemment mais la démocratisation d'Internet a tout chamboulé. Plus besoin d'attendre le quotidien du lendemain pour connaître les nouvelles car la plupart des grands médias [2] diffusent sur Internet. Plus besoin du journal local pour vendre ou acheter un bien car les services comme craigList [3] offre de meilleures couvertures. Plus besoin du mensuel pour voir des photographies extraordinaires, il suffit de se rendre sur Flickr [4]...
  • Il y a enfin le profil de la progression plate, infiniment plate, sans perspective de succès. C'est le chemin vers une voie sans issue.

Seth explique que les deux derniers profils sont à abandonner rapidement, car il n'y a que de la médiocrité à en retirer. Le premier profil, celui avec le dip, est à évaluer consciencieusement.

Il arrive des situations, ou malgré les perspectives de réussite évidentes, le palier à traverser pour atteindre le succès est trop pénible. En fait, Seth écrit que la péniblité du palier est proportionnel au succès à atteindre. Si ce n'était pas pénible tout le monde serait couronné de succès, ce qui n'est évidement pas le cas. Parmi ses exemples, Seth cite le cas des grands sportifs, des dirigeants de grandes sociétés, des créateurs, etc. Il dit, par exemple, que c'est facile d'être un grand patron, mais c'est difficile de gravir les échelons pour arriver à ce poste.

La partie qui m'interpelle particulièrement, c'est son discours sur la nécessité d'identifier les situations de dip que l'on peut traverser et celles qui sont insurmontables compte tenu de nos propres contraintes. Quand une situation de dip trop difficiles, tout comme celle de la falaise ou de la progression, il est important d'y renoncer tôt. Il affirme même qu'il est important d'identifier les critères de renonciation avant d'être dans la situation de blocage pour être sûr de quitter pour les bonnes raisons. Si les raisons sont trouvées alors qu'on est dans le trouble, elles provoqueront des remords par la suite. Avec l'analogie d'un marathon, Seth dit que par exemple se mettre des objectifs de temps de parcours et de s'évaluer par rapport à eux plutôt qu'aux sensations du moment (crampes, fringale, etc.) donne de meilleurs chances de succès. Si on déjà parcouru 35 km, les sensations rendront la fin plus pénible à supporter s'il n'y a pas l'estimation initiale de succès établie en fonction de temps établis par avance.

Seth dit que le renoncement à des situations trop difficiles laisse la chance à la réalisation d'autres défis plus à sa mesure. Comme le critère de réussite est d'être « le meilleur de son monde », peu importe que d'autres pensent que l'abandon d'un situation soit un échec. Il arrive même que ceux qui vous jugeront mal sont eux-mêmes incapable de sortir du dip, qu'ils nagent dans une médiocrité relative.

Si je fais un peu d'introspection, j'en arrive aux constats suivants :

  • Autant mon immigration au Canada, mes derniers changements d'emploi (de Oracle à IBM Rational, puis à Compuware [5]), ainsi que le déplacement de mes activités para-sociales (de RÉSULTATS à Diku Dilenga [6]) ont été motivés par des constats de dip devenus infranchissables (en tout cas, avec des perspectives moindres lorsque comparées à celles offertes par les changements).
  • J'aime les difficultés, les challenges qui sont rudes, dans la mesure où la perspective de grandir est bonne. Instinctivement avant, clairement maintenant, j'abandonne les situations qui sont sans issue, ou pour lesquelles les compromis à faire sont sans commune mesure avec la satisfaction à en retirer. Cette attitude peut paraître fruste et égoïste mais c'est relatif : j'imagine qu'en étant le « meilleur de mon monde » cela bénéficie aux gens de « mon monde ». Ce qui est mieux, à mon avis, que d'affecter mon entourage parce que je reste « un médiocre dans mon monde ».

Étonnant comme un petit livre peut faire gamberger, n'est-ce pas ? À votre tour d'y consacrer quelques heures et de partager votre propre cheminement en me laissant un commentaire.

Pour le plaisir, voici la vidéo d'une présentation donnée par Seth Godin en 2003 [7]. Tout ce qui dit est du « gros bon sens » et c'est ce qui le rend pertinent. Le livre est du même acabit.


Seth Godin at Gel 2006 from Gel Conference on Vimeo.



A+, Dom
--
Sources :
  1. Le livre décrit sur le site de Seth, et la présentation francophone sur le site des Éditions du trésor caché.
  2. Google News, Associated Press (AP), Agence France Presse (AFP), Cable News Network (CNN), Radio de l'information (RDI, du réseau de Radio Canada), France24, Al-Jazeera, etc.
  3. craiglist, kijij, lespac, etc.
  4. Flickr, PicasaWeb, PhotoBucket, etc.
  5. Oracle, IBM Rational, Compuware.
  6. RÉSULTATS, Diku Dilenga.
  7. Entrée sur le blog de Seth : This is broken.

Wednesday, April 29, 2009

Meet you at JavaOne Conference

RockstarAs the title let you imagine, I will be in San Francisco the week of June 2-5 for the JavaOne 2009 conference.

It will be my first visit to the event, not to the Moscone Center. While working for Oracle, I attended one Open World event there. With the recent acquisition (not officially closed yet) of Sun by Oracle, I am going to go in California for Oracle again ;)

With another Compuware colleague, we will mainly focus on mobile related sessions, but cloud computing and rich interactive application related ones will have my attention too. With sessions finishing at 10:20 PM, days will be surely long. I expect a fruitful trip.

Are you going to attend too? Tweet me your schedule to meet you there ;)


JavaOne June 2-5, 2009


A+, Dom