• Share this article:

Services, services, services, …

Thursday, March 12, 2009 - 13:45 by Wayne Beaton

I’m concerned that the Eclipse Business Expense Reporting Tool (EBERT) is getting too complicated.

Since the application can run on the Eclipse Rich Ajax Platform (RAP), it needs to support the notion of multiple users. Since it also runs on Eclipse Rich Client Platform (RCP), I can’t use the RAP-specific notions (like SessionSingletons or RWT.getSessionStore()) to represent user-specific information. I opted, therefore, to create an Equinox Service, IUserContextService which is responsible for providing an IUserContext for the current thread:

public interface IUserContextService {
	/**
	 * This method answers the instance of {@link IUserContext} for
	 * the user operating in the current thread. To ensure that this
	 * method answers the right context object, it should be called
	 * from the UI thread.
	 *
	 * @return an implementor of {@link IUserContext} containing
	 * the state for the current user.
	 */
	IUserContext getUserContext();
}

and

public interface IUserContext {
	ULocale getUserLocale();
	ViewModel getViewModel();
	void dispose();
}

The IUserContextService interface is pretty simple, it currently has a single method that is responsible for providing an IUserContext for the current thread. The comment states that the method should only be called from the UI thread; this is critical in a multiple-user environment like RAP as the UI thread is the only thread that we know the “current” user actually owns. The IUserContext interface shows the sorts of things that the user context is responsible for: knowing what ULocale do we use to display information for the current user, providing the view model for the current user, and cleaning up after itself. Ultimately, I think this object will probably also retain some notion of the user’s identity, but I need to think about that more.

I provide two implementations of this: RapUserContextService/RapUserContext and StandaloneUserContextService/StandaloneUserContext. The “RAP” version has some hooks into the RAP-specific session management code; it creates a “user context” for the user and stuffs it into the session store for later recall.

public IUserContext getUserContext() {
	ISessionStore sessionStore = RWT.getSessionStore();
	RapUserContext context = (RapUserContext) sessionStore.getAttribute(USER_CONTEXT_KEY);
	if (context == null) {
		context = new RapUserContext();
		sessionStore.setAttribute(USER_CONTEXT_KEY, context);
	}
	return context;
}

By adding the restriction that the method must be called from the UI thread, I avoid nasty synchronization issues (though I probably should put in a check to make sure that the calling thread is valid).

The “standalone” version, which is used in RCP and eRCP is far simpler: it just creates and holds a single user context.

public class StandaloneUserContextService implements IUserContextService {
	private StandaloneUserContext userContext;

	public StandaloneUserContextService() {
		userContext = new StandaloneUserContext();
	}

	public IUserContext getUserContext() {
		return userContext;
	}
}

The user interface components are designed to look for user context objects to get their view model. They set up service trackers so that they are notified when an IUserContextService is available; until one is available, the views stay disabled. I added a method in the abstract superclass I created for all of EBERT’s views:

protected void startUserContextServiceTracker() {
	userContextServiceTracker = new ServiceTracker(ExpenseReportingUI.getDefault().getContext(), IUserContextService.class.getName(), null) {
		/**
		 * We keep track of the first service we find and ignore the
		 * rest. This is a great example of where declarative services
		 * would be helpful: you can declare that you want exactly one
		 * instance of a service and that's what you get.
		 */
		protected IUserContextService userContextService;

		/**
		 * This method is called when a matching service is found or
		 * added. This finds both pre-existing and new instances of the
		 * service.
		 */
		public Object addingService(ServiceReference reference) {
			Object service = super.addingService(reference);
			if (userContextService == null) {
				userContextService = (IUserContextService)service;
				/*
				 * Do the part where we get the user context in a
				 * syncExec block. This will make sure that it runs
				 * in the user interface thread for the current user.
				 * This doesn't matter too much on RCP/eRCP, but the
				 * thread that we're running in is pretty critical
				 * in RAP.
				 */
				syncExec(new Runnable() {
					public void run() {
						userContext = userContextService.getUserContext();
						connectToUserContext(userContext);
					}
				});
			}
			return service;
		}

		/**
		 * This method is called when the service is being removed, or the
		 * tracker is being closed.
		 */
		public void removedService(ServiceReference reference, Object service) {
			if (service == userContextService) {
				syncExec(new Runnable() {
					public void run() {
						disconnectFromUserContext(userContext);
					}
				});
				userContext = null;
				userContextService = null;
				// TODO Do we try to match up with a hypothetical second service in this case?
			}
			super.removedService(reference, service);
		};
	};
	userContextServiceTracker.open();
}

This is pretty much boilerplate ServiceTracker stuff. When the tracker is opened, it starts listening for services. The tracker is notified of any existing services which match, and services added thereafter, via the addingService method. We greedily grab the first IUserContextService we’re given, use it to obtain an appropriate IUserContext which we use to bootstrap the instance with a send of the connectToUserContext message. Note how the actual code that obtains the user context is wrapped in a “syncExec” block to ensure that it’s running in the UI thread (as noted earlier, this makes sure we get the right context from the right RAP session when we’re running in that environment). By using services in this way, my view code is completely disconnected from the specifics of how my user state is represented.

What’s cool about this is that the start order doesn’t really matter. If the “view” bundles start first, then the UI comes up unresponsive for a few fractions of an instant; when the user context service comes on line, everything starts working. It sort of cool (in a horribly nerdy way) to start and stop the service from the Equinox console and see the UI respond by showing/hiding the user’s state.

Where it’s starting to get complicated is that each of these services requires its own bundle and the number of bundles seems to be growing with each step. Having all this functionality in separate bundles makes it easy to dynamically change how the application looks and acts at runtime (even while it’s running), but with this flexibility comes some bundle-management complexity. At very least, you need to have some sense for which bundles you’re deploying in which environments; when you’re managing three environments, this can be tricky.

I may get trickier still. I’m trying to add a notion of a persistence service. My initial entry is a Java serialization-based persistence service, but ultimately, I’d like to try and hook in something like EclipseLink. Of course, in the standalone case, it’s relatively easy, but accommodating for multiple users introduces a challenge: what is the identity of the user? Enter the identity service. Even with Java serialization, I still need to be able to distinguish between user, so I need some notion of identity. This will be more true with something real like JPA via EclipseLink. Of course there could be multiple types of identity services: basic authentication with a userid/password is the entry-level, but ultimately, I’d like to add a Higgins option.

I wonder if I’m trying to make this too dynamic? Should I just keep it simple, or are these ideas that need to be explored in the example? I’m worried that the explosion of bundles makes this “simple” example appear very complicated for the first-time user.

Tags