Creating a New Service
Overview
Services provide JavaRosa with a link between the internal platform and the outside world. This includes both the data sources that are available on the specific handset, such as the file system and devices such as cameras, as well as transport layers for communicating beyond the phone, such as http and bluetooth.
A new service should be implemented in order to make one of these new data sources available.
Services should be the most modular components of JavaRosa. Great care should be taken to ensure no code other than the service is ever strictly dependent on a specific device or requires a specific physical configuration.
Important Background
Terms: Service, Service Provider, JavaRosa Service Provider (JRSP), Shell
Interfaces: IService, JavaRosaServiceProvider
Projects : org.javarosa.core
The first and most important part of creating a new Service is to identify its scope. Will the new service be creating an interface for a general class of devices, or for a specific device? In general the latter is preferable, as it allows the development of new Services that can be used in place of each other when one might not be available.
Service Names
A service is only required to implement one method, getName(). The most vital aspect of getName() is that it is a uniquely named key that will be used to return the service later. As such, it is vital that when creating a new service this name be as specific to the type of service you are providing as possible to avoid namespace collisions.
It is important to note that although namespace collisions are very bad, it is not necessarily a collision for two providers to share the same name. An example of this will be seen in the Services and Modularity section below.
Registering Services
Services are used in an application by requesting them from the JRSP after they have been registered with it. The registration should occur once at the very beginning of the Application's execution. Afterward, the Service should be returned by calling the JRSP's getService() method, and passing in the appropriate name. The service can then be cast to that of the appropriate type. If the service is unavailable, an UnavailableServiceException is thrown, because systems that rely on a service being able shouldn't be able to proceed in the general case without that service.
It is almost never acceptable to catch failures in this process silently. IE:
try {
IService service = JavaRosaServiceProvider.instance().getService("MyService");
MyService castService = (MyService)service;
} catch(UnavailableServiceException e) {
//Didn't get the service, do nothing.
e.printStackTrace();
} catch(ClassCastException cce) {
//Didn't get the service, do nothing.
e.printStackTrace();
}
//continue with code
//...
This does not adhere to JavaRosa's Fail-Fast behavior practices. If code expects a service to be available, it should not be able to proceed without it. Additionally, if the service returned is not the one requested, the System shouldn't be able to reasonably proceed. The fact that we cannot semantically enforce these relationships is a limitation of J2ME, and it is vital that code handle these failures in a disciplined manner.
If either of these exceptions are caught here, they should be transformed into a different exception, such as GpsModuleFailureException, or something else informative in the context in which the service request call fails.
Services and Modularity
Since Services are registered into the JavaRosa Service Provider with unique keys, if a Service is going to be general there are two primary techniques for allowing more than one implementation to take over. One of them is to create your Service as an interface, one implementation of which is the actual Service you're going to implement. The other is a Service Provider, which is a Service which can be used to retrieve other Services. Confused yet? Here's an Example.
Assume we'd like to make a service which retrieves GPS data from a Samsung phone-specific GPS device, and that no other GPS services have been created. The simplest way to do so would be to create a service called SamsungPretend4052GPSService which implements the IService interface. That might look like this.
public class SamsungPretend4052GPSService implements IService {
public String getName() {
return "SamsungPretend4052GPSService";
}
public String getLattitude() {
...
}
public String getLongitude() {
...
}
...
}
This service would be plenty valid, and could be used. Unfortunately, it can only be used by projects that want to rely specifically on the SamsungPretend4052GPSService. One way around this would be to change the string returned by getName() to be something more generic. This requires that we change the service returned though. In general, what we want is a contract between the service returned and the code that will be using it. As such we'll define an interface for a GPS Service.
public interface IGPSProviderService extends IService {
public String getLattitude();
public String getLongitude();
...
}
And we'll implement this new Service Interface for our device-specific interface.
public class SamsungPretend4052GPSService implements IGPSProviderService {
public String getName() {
return "GPS Service";
}
public String getLattitude() {
...
}
public String getLongitude() {
...
}
...
}
Since the name of our service can be shared with other services that fulfill the same contract, we are able to write code in a project that requires a GPS device to exist, but is agnostic about which one to use. In a Mapping project, we could have a file that uses our GPS Service with a call similar to
...
IGpsProviderService gps = (IGpsProviderService)JavaRosaServiceProvider.instance().getService("GPS Service");
processLocation(gps.getLattitude(), gps.getLongitude());
And when we build a project on our made-up Samsung phone that includes the Mapping libraries, we can register our Service in the shell's initialization. We could also compile for a different phone entirely, and hand it a GPS service specific to that device, allowing us to use the Mapping library anywhere.
... JavaRosaServiceProvider.registerService(new SamsungPretend4052GPSService()); ...
Service Providers
One limitation of implementing generic services in the manner described in the last section, is that we can only register one valid instance of a GPS Provider on a single application. For the GPS example, this is not terribly limiting, but for some services it is extremely advantageous to be able to use multiple implementations on a single device.
As an example, Bluetooth, SMS, and GPRS could all conceivably be used to transmit some structured data to an external source. In general we'd like to be agnostic about which one is used, but it could also be important to have a few available, in case one is broken in the physical world at a given time. As such, we'd like to be able to have a uniform way of accessing differently inclined services. We accomplish this through the use of a Service Provider.
Like in the GPS example, it's important to have a uniform contract for the functionality provided by each individual service. However, instead of having it implement the Service contract directly, it will be returned from a Service Provider.
public interface IStructuredDataTransmitter {
public String getTransmitterName();
public boolean pushStructuredData(byte[] data);
}
An implementation of this interface would provide a single transmitter
public class GPRSStructuredDataTransmitter implements IStructuredDataTransmitter {
public String getTransmitterName() {
return "GPRS Structured Data Transmitter";
}
public boolean pushStructuredData(byte[] data) {
...
}
}
Given this contract, we can create a provider for this service.
public class StructuredDataTransmitterProvider implements IService, IStructuredDataTransmitter {
/** String -> IStructuredDataTransmitter **/
Hashtable providers = new Hashtable();
IStructuredDataTransmitter currentTransmitter;
public String getName() {
return "GPSServiceProvider";
}
public Vector getAvailableProviders() {
return providers.keys();
}
public void registerProvider(IStructuredDataTransmitter transmitter) {
providers.add(transmitter.getName(), transmitter);
}
public void setCurrentProvider(String provider) {
currentTransmitter = providers.get(provider)
}
public String getTransmitterName() {
return "Structured Data Transmitter Provider";
}
public void pushStructuredData(byte[] data) {
currentTransmitter.pushStructuredData(data);
}
}
There are a number of details glossed over in this brief example. For instance, there is no default provider, and there should be a well structured set of exceptions to handle cases where no services can be provided.
Additionally, it's worth noting that this provider implements the interface that it is providing, which seems kind of odd. Essentially this exists to ensure that any functionality available to the interface is available from the Service provider. Another way to ensure access to all of the interface's contracts would be to simply return the Service from the Provider. That can be powerful, but dangerous as it allows the Service to be mutated, which is sometimes necessary. How exactly the provider delivers the functionality of a service is a design decision that should be made with a particular application in mind.
The final step to be covered is that of the initial registration of a Service Provider for an actual application. This would, in general, happen in the shell in some manner such as
GPRSStructuredDataTransmitter gprs =
new GPRSStructuredDataTransmitter(initializationArguments);
BluetoothStructuredDataTransmitter bt =
new BluetoothStructuredDataTransmitter(bluetoothInitializationArguments);
StructuredDataTransmitterProvider provider = new StructuredDataTransmitterProvider();
provider.registerProvider(gprs);
provider.registerProvider(bt);
provider.setCurrentProvider(gprs.getTransmitterName());
JavaRosaServiceProvider.instance().registerService(provider);
After which we could interact with the different services through the provider.
StructuredDataTransmitterProvider provider =
(StructuredDataTransmitterProvider)JavaRosaServiceProvider.instance().getService("GPSServiceProvider");
provider.pushStructuredData(data);
...
StructuredDataTransmitterProvider provider =
(StructuredDataTransmitterProvider)JavaRosaServiceProvider.instance().getService("GPSServiceProvider");
Vector serviceNames = provider.getAvailableProviders();
...
Present Services to the User and get a choice about which one to use
...
provider.setCurrentProvider(userChoice);
Summary
The creation of a new service requires a large number of design decisions to be made regarding the specificity, use cases, and potential modularity of the data source being provided. This document has provided a general set of guidelines for the different kinds of modularity that can be provided with different Service designs, and how they can be used to obtain data from outside sources in JavaRosa.
