Soto 3
-
Overview
-
Features
- Architecture
- Learning by Example
-
Advanced Issues
-
Attributes
- Implementation Details
-
File Includes
-
Resource Handlers
-
Resource Aliasing
-
Bootstrapping
-
Variable Interpolation
-
Post-Injection
- Conditional Instantiation
-
Accessing the Environment
-
Accessing Naming Information
- Type Coercion
- Best Practices
-
Attributes
-
Conclusion
Overview
Soto stands for "service oriented technology"; it will offer various tools/frameworks intented to provide building blocks for service-oriented architectures. Soto's core is a lightweigth container framework allowing to "wire" services through a convenient XML format. Soto in addition supports a "layered" logic approach: different "logic domains" (known as "layers") can be mapped on top of service instances, sparing the developer from aggregating various types of logic into unmanageable blobs - this is similar to what AOP attempts to solve, but at a higher-level: the "layer" concept needs not being implemented with byte code manipulation, advices, pointcuts, and the likes.
Features
More concretely, here are Soto's main features:
- Service implementations are configured through XML, and instantiated using the Confix API: this spares developers from manipulating DOM-like structures to access a service's configuration. The latter is rather directly assigned to service instances using Java Reflection.
- Service implementations are "dynamically" associated: if a service needs another service to accomplish its work, this association is materialized through the XML configuration - no lookup through some naming service or "component manager".
- Embeddable: a so-called "soto container" can be instantiated directly by the developer; it can be used in a servlet, in an EJB, in a main method, etc. Hence the "lightweight" container...
- Comes with built-in "layers": one that allows services to be automatically published as MMeans, through simple configuration; another that implements a simple AOP framework - others are planned.
- Supports file "includes": configuration can be split into multiple configuration files that can be "included" in other ones. This is very essential in cases where configuration becomes "large" and difficult to manage in text editors.
- Pays special attention to configuration management.
- Sports nifty features such as conditional instantiation, URI aliases, resource resolving...
Architecture
What's in a Service?
From the Soto framework point of view, a service is simply a class that implements the Service interface. The interface goes as follows:
public interface Service {
public void init() throws Exception;
public void start() throws Exception;
public void dispose();
}
The above signature implies that services are initialized, then started, and eventually destroyed. This is indeed the simple life-cycle that Soto imposes.
A service can be anything you wish it to be; ideally, it should perform some specialized unit of work; multiple services are associated to form an application.
What's in a Layer?
A layer is meant to separate the core, specialized task that a service performs from the generic behavior/characteristics/features that a given application might "need" the service to offer/implement. Layers have two goals: code reuse and elegance. The latter means that service developers are freed from reimplementing features that do not belong specifically to a given service, but that the latter might offer through a plug-in scheme, without fuss, as simply as possible, preferably through configuration. To use AOP's terminology, we could say that layers address "cross-cutting concerns". The difference though is that layers need not being implemented through AOP - although they could. Layers are destined to work "on top" of services, without interfering with the latter's activities.
In Soto, layers implement the Layer interface. The interface goes as follows:
public interface Layer {
public void init(ServiceMetaData meta) throws Exception;
public void dispose();
}
A layer instance is associated to a service instance (as the init() method suggests); this association is done through Soto's XML configuration format - as will be shown further below. The above methods are called after their service counterparts are called.
Learning by Example
Implementing a Service
The following code shows a simple service implementation that displays a message to stdout:
package org.sapia.soto.examples;
import org.sapia.soto.Service;
public class MasterService implements Service {
private String _msg;
public MasterService() {
}
public void init() throws Exception {
System.out.println("initializing " + getClass());
}
public void start() throws Exception {
System.out.println("starting " + getClass());
}
public void dispose() {}
public void setMessage(String msg){
_msg = msg;
}
public void doSomething(){
System.out.println("This is a message: " + _msg);
}
}
Life-Cycle
Upon startup, the service init() and start() methods are successively called; it is in the init() method that a service should perform pre-startup checks (insuring that required instance members are not null, etc.). The container that holds the services (the SotoContainer, as we'll see further below) indeed proceeds to the following:
- calls init() on service instances - once a service's init() method is called, all its layers (if any) have their init() method also called. The init() procedure is aborted as soon as a service or layer throws an exception; otherwise the next step occurs.
- The container waits for its own start() method to be called; once this happens, each service is also started.
Configuration
The above service is configured as follows:
<soto:app
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<soto:namespace prefix="sample">
<def class="org.sapia.soto.examples.MasterService"
name="master" />
</soto:namespace>
<soto:service id="master">
<sample:master
message="This is a simple useless service" />
</soto:service>
The root element of a Soto configuration file is the soto:app - we will discuss the namespace declarations later on.
Soto uses the Confix API to instantiate objects from a XML representation. To make a long story short, Confix spares developers from having to traverse a DOM-like configuration; the latter is rather direclty assigned to the instantiated objects - through setter/adder methods on these objects (see the Confix documentation for more details).
Then, in order for objects to be created dynamically by the configuration engine, their classes must be declared with def elements, categorized by namespace. This is the same pattern that is used by the Vlad validation framework. Such a categorization allows for different object definitions to cohabitate without risking name collisions; it also allows to group object definitions by domain, or by contributor/vendor, etc.
This definition-per-namespace scheme is materialized through the following:
<soto:namespace prefix="sample">
<def class="org.sapia.soto.examples.MasterService"
name="master" />
</soto:namespace>
The soto:namespace element takes a mandatory prefix attribute. This prefix must later be used when creating instances of our object definitions; if you look at the def element, you see that our service implementation appears. In fact, an object definition holds the name of the class from which to instantiate objects, and a "name", which is a logical identifier allowing to refer to a single definition in a given namespace.
Once object definitions have been configured in such a way, they can later on be referred to with the prefix:name notation; upon encountering such a combination, Confix (Soto's underlying configuration engine) will create an instance of the class specified by the corresponding definition.
For the XML parser to understand our prefix:name elements, the prefix part as to be mapped to an XML namespace. Hence such namespaces at the top of the configuration file.
The following excerpt shows how to create an instance of our service:
<soto:service id="master">
<sample:master
message="This is a simple useless service" />
</soto:service>
An important point to note is that all services are configured within a soto:service element. The element can take an optional id attribute, that allows to refer to the service later on in the configuration - to give you a hint: this is how service association is done.
Then, the sample:master element is our actual service "instantiation"; upon encountering this element, the configuration engine creates a MasterService instance. Then, following Confix' processing rule, the message attribute is mapped to the setMessage() method in our service; the attribute's value will be passed to the method.
Of course, many services can be instantiated this way, and associated to each other, to form a whole application - as we'll see later on.
As a service is created, it is internally kept by the container (with which the configuration was loaded), for the application's whole duration.
POJO Support
As of version 3.0, Soto supports POJOs, meaning that services do not need implementing the Service interface. The POJOs obey the same life-cycle as "standard" Soto services, provided that some special information is given at configuration time. To make a long story short: you are responsible for indicating to Soto which are the init, start and dispose methods of your POJOs. The following illustrates how this is done:
<soto:service id="somePojo">
<initMethod name="initialize" />
<startMethod name="startup" />
<disposeMethod name="dispose" />
<sample:pojo />
</soto:service>
Of course, not all tags need being specified.
Loading and Starting the Application
An instance of SotoContainer is used to load into memory the services that make up a given Soto application, start the application (i.e.: its services), and stop it. The example below shows how to load our single-service application:
SotoContainer container = new SotoContainer();
container.load(new File("path_to_xml_config"));
container.start();
That's it; we have created our first (although not very useful) Soto application.
Service Association/Composition
It is much recommended that an application be made of multiple, specialized services, rather then use a single service instance. The multi-instance scenario suggests that some instances will rely on the capabilities of others to fulfill their task; this further means, in concrete terms, that a given component A, requiring the service of a given component B, be provided with the latter at some point. In EJB, association between components is done through the JNDI: in our case, component A would be provided with the JNDI name of component B.
A "naming service" indirection (such as JNDI) is suitable for distributed components, but can be tedious to use when developing an application whose components will reside in the same memory space. With Soto, a given service can directly be provided with an instance of another service dynamically; such an association is specified through configuration. Again, Confix comes to the rescue and dynamically assigns service instances to one another. The following, an updated version of our application's configuration file, shows how to link a service with another:
<soto:app
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<soto:namespace prefix="sample">
<def class="org.sapia.soto.examples.MasterService"
name="master" />
<def class="org.sapia.soto.examples.SecondaryService"
name="secondary" />
</soto:namespace>
<soto:service id="master">
<sample:master
message="This is a simple useless service" />
</soto:service>
<soto:service id="secondary">
<sample:secondary>
<someService>
<soto:serviceRef id="master"/>
</someService>
</sample:secondary>
</soto:service>
<soto:app>
To better understand what is happening here, the code of our "secondary" service is given below:
package org.sapia.soto.examples;
import org.sapia.soto.Service;
public class SecondaryService implements Service {
private MasterService _svc;
public SecondaryService() {}
public void setSomeService(MasterService svc) {
_svc = svc;
}
public MasterService getSomeService() {
return _svc;
}
public void init() throws Exception {
System.out.println("initializing " + getClass());
}
public void start() throws Exception {
System.out.println("starting " + getClass());
}
public void dispose() {}
public void doSomethingElse(){
System.out.println("Calling master service...");
_svc.doSomething();
}
}
As you can see in the configuration, our new service is also configured in a soto:service element. The most interesting part is the soto:serviceRef element; see how it is encapsulated within a someService element? Now have a look at the SecondaryService; see the setSomeService() method? See the type of the parameter that this method takes? Now look back at the configuration; see the soto:serviceRef element's id attribute, and how the value of this attribute indeed maps to the identifier of a MasterService instance?
There you have it: a service can be associated to another through a soto:serviceRef. Note that instead of a setSomeService() method, we could have add a addSomeService() one; and then, the following would have been "legal":
<soto:app
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<soto:namespace prefix="sample">
<def class="org.sapia.soto.examples.MasterService"
name="master" />
<def class="org.sapia.soto.examples.SecondaryService"
name="secondary" />
</soto:namespace>
<soto:service id="secondary">
<sample:secondary>
<someService>
<soto:service>
<sample:master
message="This is the first one" />
</soto:service>
</someService>
<someService>
<soto:service>
<sample:master
message="This is the second one" />
</soto:service>
</someService>
</sample:secondary>
</soto:service>
<soto:app>
So, not only can we associate services, but we can also aggregate them. In addition, note that we did not use service references in this case, but rather created specific instances of the MasterService class. Service instances that are created in this way are considered "local" or "private" service instances: they cannot be referred to - by using service references.
Constructor Dependency Injection
Soto supports instantiating objects through their constructor. In this case, the soto:new element is used, as follows:
<soto:app xmlns:soto="sapia:soto">
<soto:service id="someService">
<soto:new class="org.sapia.soto.example.IOC3Service">
<arg>test</arg>
<arg><soto:int value="10" /></arg>
<arg type="int">100</arg>
<property>test</property>
</soto:new>
</soto:service>
</soto:app>
The above demonstrates usage of the soto:new element, which takes as an attribute the name of the class for which an instance should be created. In addition, the element supports passing in constructor arguments, through arg elements. The arg element is configured as follows:
- it takes an optional type attribute, which specifies the type of the argument expected by the constructor. The type must correspond to the full-qualified name of a class, or the name of a primitive(boolean, byte, short, int, long, float, double);
- it takes a nested element that should evaluate to an object of the expected type.
The example shows that the type of the argument does not always have to be specified. In such a case, Soto attempts guessing the type from the value that is passed in. For example, the soto:int converts its value to a Java integer, which makes it easy for Soto to guess the type of the argument. If no type is specified, and the value passed in does not evaluate to a "precise" type, then the value is presumed to be an instance of java.lang.String.
JNDI
Soto services (implementing the Service interface) need not exclusively be associated with other Soto services. Through the configuration, any object can be associated to a Soto service.
To benefit from seamless integration with existing J2EE components deployed in the context of J2EE app servers, Soto supports assiociating objects looked up from the JNDI to Soto services. The example below illustrates this:
<soto:service id="myService">
<sample:customService>
<datasource>
<soto:jndiRef name="java:comp/env/jdbc/someDatasource" />
</datasource>
</sample:customService>
</soto:service>
<soto:app>
In the above case, the jndiRef tag implementation internally calls new InitialContext() and then performs the lookup; but we can also specify properties that will be passed to the InitialContext constructor:
<soto:service id="myService">
<sample:customService>
<datasource>
<soto:jndiRef name="java:comp/env/jdbc/someDatasource">
<property name="java.naming.factory.initial"
value="org.acme.jndi.MyInitialContextFactory" />
</soto:jndiRef>
</datasource>
</sample:customService>
</soto:service>
<soto:app>
In the above case, the tag implementation internally creates a java.util.Properties object that is initialized with the properties that we have configured; then, the tag calls new InitialContext(properties) and performs the lookup.
Layers
We've discussed layers a bit: they are intended to add transparent functionality to services, without interfering with their activity. For example, imagine that you have this service that you'd like to administer remotely; then you start thinking: "Hey, actually, what if I could just take any service and publish it as a JMX Mbean?". Instead of hardwiring the remote administration code into your service, you'd rather have pluggable MBean behavior, on demand.
In Soto's terminology, such "pluggable", reusable behavior that can be added to existing services is dubbed a "layer". As we have seen, Soto comes with a JMX layer and an AOP layer; a layer is just an implementation of the Layer interface that is intented to "decorate" service instances at runtime, adding additional characteristics/functionality to these services.
The XML below shows how a JMX layer is added to our configuration:
<soto:app
xmlns:soto="sapia:soto"
xmlns:jmx="soto:jmx"
xmlns:sample="soto:sample">
<soto:namespace prefix="sample">
<def class="org.sapia.soto.examples.MasterService"
name="master" />
<def class="org.sapia.soto.examples.SecondaryService"
name="secondary" />
</soto:namespace>
<soto:service id="master">
<sample:master
message="This is a simple useless service" />
</soto:service>
<soto:service id="secondary">
<!-- service instance -->
<sample:secondary>
<someService>
<soto:serviceRef id="master"/>
</someService>
</sample:secondary>
<!-- applying JMX layer -->
<jmx:mbean description="A Service that does not do much">
<operations>
<include name="doSomething*"
description="This operation does something." />
<exclude name="init"/>
<exclude name="start"/>
</operations>
</jmx:mbeam>
</soto:service>
<soto:app>
As can be seen, the layer is "applied" to the service through configuration; upon encountering the jmx:mbean combination, the underlying configuration engine instantiates the appropriate layer instance. In this case, it is the layer that generates a MBean for our service. As can be seen, the layer supports a convenient notation that allows us to:
- Determine which setters/getters and methods of our service are "published" as MBean attributes and operations.
- Provide additional information (mainly, a description) to the available attributes/operations.
In our updated configuration, no object definition corresponds to the jmx:mbean element. That is simply because the "jmx" namespace is part of Soto's default namespaces. - we will see further below where that default configuration resides.
Implementing your own layer is easy: write a class that implements the Layer interface, create an object definition for it in your configuration file, and you're up.
The life-cycle of a layer is related to the one of its service:
- The init() method is called after the service's own init().
- The start() method is called after the service's own start().
- The dispose() method is called before the service's own dispose().
One could think of many uses for layers: what about a "transaction" layer?; or a "web service" layer (that transparently publishes your service as a web service). Your imagination is your only limit.
Advanced Issues
Attributes
Soto services can be configured with "attributes". Attributes provide runtime metadata about services that can be used to help further differentiate service instances. For example, multiple service instance could implement a given common interface in a different manner. From the application's perspective, it might be necessary to be able to differentiate among those different service instances depending on the intended usage. Using attributes to help the application choose the proper service instance could be an efficient mechanism. One configures service attributes like so:
<soto:app
<soto:service id="secondary">
<!-- attributes -->
<attribute name="acme:type" value="jdbc" >
<sample:databaseService />
</soto:service>
</soto:app>
In the above example, an arbitrary attribute is configured to provide information about the service's role or type. In this case, we identify that the service instance is of "jdbc" type. Imagine a bit that we could have another service, abstracting another type of repository (for example an XML one). We could have both service classes implement a common repository interface, and discriminate among them by providing, as attributes, information about the type of repository they represent.
This is of course not the end of the story; on their own, attributes do not help much. Used in combination with lookups (see next section), they show all their power.
Each attribute takes a name that is composed of a prefix and an actual "local" name. These are not related to XML prefixes and namespaces, although the intent is analogous: this notation helps prevent name collisions. At runtime, an attribute configuration is "transformed" into an Attribute instance. Such an instance is encapsulate in a ServiceMetaData.
Implementation Details
Threading
Care must be taken when implementing services; as in the servlet model, services can potentially be accessed by multiple simulteaneous threads. Therefore, state modifications should be synchronized - the same applies to layers.
Looking up Services
Once the start() method on the SotoContainer has been called, the lookup() methods can be used to retrieve specific service instances.
By Identifier
The SotoContainer provides a method to lookup that allows finding services through their identifiers (configured as part of their corresponding soto:service tag), as the example below illustrates:
// 'container' is a SotoContainer instance // 'aServiceIdentifier' is a String SomeService svc = (SomeService)container.lookup(aServiceIdentifier);
By Type
At times, it might be more convenient to lookup a given service based on a given interface that it is expected to implement. For example, a service could implement the javax.sql.DataSource interface, and your application might want such a datasource. Thus, the SotoContainer class allows making lookup based on an interface:
DataSource svc = (DataSource)container.lookup(DataSource.class);
In this case, the container traverses all services (in the order in which they were added to it) and returns the one that implements the given interface. If more than one service implements the interface, an exception is thrown. Note that this method uses Java reflection and traverses all services at each invocation, so it is suboptimal to call it every time you need a service that implements a given interface. In such cases, you should make a single lookup at initialization time, and keep the instance that you have acquired from then on.
By Attributes
Soto allows looking up services based on their configured attributes. In such a case, you use an AttributeServiceSelector as follows:
AttributeServiceSelector selector =
new AttributeServiceSelector();
selector
.addAttribute(
new Attribute().setName("attribute1")
)
.addAttribute(
new Attribute().setName("attribute2")
.setValue("value2")
);
List result = services.lookup(selector, false);
Note that the lookup method in the above example allows specifying if one wishes to acquire ServiceMetadata (by passing true) or Service instances (by passing false).
With a Custom Selector
Soto provides an easy way to extend its lookup mechanism: you can implement ServiceSelectors. A ServiceSelector only needs implementing a single accept() method, that takes a ServiceMetadata as a parameter. That metadata encapsulates a Service, as well as its configuration information (such as its identifier and attributes, if any). A service selector can instrospect the metada and/or the corresponding service to determine if it should "accept" it. If the selector's accept() call returns true, then the service or metadata is made part of the result that is eventually returned to the caller:
List result = container.lookup(new MySelector(), false);
Object Definitions
The XML elements that map XML element names to Java class names are dubbed "object definitions". You can specify your own definitions in one of two ways:
- By loading definitions into the Soto container from a definition file.
- By embedding soto:namespace elements in your Soto configuration.
- By mapping the XML prefix that you use to an URI that actually corresponds to a real definition file.
Loading Definitions into the Container
A file containing object definitions can be loaded into a SotoContainer through one of its load() methods. The contents of such a file is as follows:
<soto:defs
xmlns:soto="sapia:soto">
<soto:namespace prefix="myapp">
<def class="org.acmeapps.someapp.TransactionService"
name="transactions" />
</soto:namespace>
</soto:defs>
Object definition files have a soto:defs element as a root; such an element can take one to many soto:namespace elements. Each such element can take one to many def, corresponding to an actual object definition: it maps the name of a Java class to the name of an XML element.
An application can load such definitions using the different load() methods on a SotoContainer. The latter supports chained invocations, and multiple files can be loaded sequentially:
container.load("org/dummy/myapp/someDefinitions.xml")
.load(new File("appConfig.xml"))
.start();
As is demonstrated above, object definitions must be loaded before the application configuration. Multiple application configurations can be chained.
The prefix attribute of a namespace element corresponds to the XML prefix that must be used when instantiation objects corresponding to given definitions. In such cases, the prefix:elementName combination must be used; it tells the Soto container which object should be instantiated. Going back to the example above, we would instantiate an object corresponding to the given object definition as follows:
<soto:app
xmlns:soto="sapia:soto" xmlns:myapp="www.acmeapps.org/someapp">
...
<soto:service>
<myapp:transactions />
</soto:service>
...
</soto:app>
Embedding Object Definitions into a Soto Configuration
As a convenience, Soto allows embedding soto:namespace elements into configuration files. Thus, the following would be perfectly "legal":
<soto:app
xmlns:soto="sapia:soto" xmlns:myapp="www.acmeapps.org/someapp">
<soto:namespace prefix="myapp">
<def class="org.acmeapps.someapp.TransactionService"
name="transactions" />
</soto:namespace>
<soto:service>
<myapp:transactions />
</soto:service>
</soto:app>
URI-based Object Definitions
Both loading object definitions explicitely or declaring them as part of your configuration file is cumbersome; in this case, the application must be "aware" of the definitions. A more convenient approach is to have the container load object definitions using the XML URI to which they are bound. As an example, look at the XML namespace declarations of our modified configuration:
<soto:app
xmlns:soto="sapia:soto" xmlns:myapp="resource:/org/acmeapps/someapp">
<soto:namespace prefix="myapp">
<def class="org.acmeapps.someapp.TransactionService"
name="transactions" />
</soto:namespace>
<soto:service>
<myapp:transactions />
</soto:service>
</soto:app>
You can see that the URI of our XML namespace declaration has changed: this URI corresponds to an actual file in the classpath. One thing to know is that for each namespace URIs the container encounters, it attempts loading an object definition file (such the one we have explained previously); it internally keeps a cache of the URIs that have been searched, so that it does not attempt loading the same file twice. In the above case, no file is specified at the end of the URI; a file name is optional, since the container attempts loading a defs.xml file at that URI, by default. This is to say that for the above to be consistent, a file having the path resource:/org/acmeapps/someapp/defs.xml must exist in the classpath. If a file containing object definitions is named otherwise, it should be explicitely provided as part of the URI.
If the container cannot load a file for a given URI, it simply ignores it and goes on. Therefore, make sure you are using URIs that correspond to the actual files you want to load.
URI-based loading of object definitions is much more powerful than the other methods, since developpers are shielded from having to load object definitions explicitely - they only need knowing about your tags and the configuration these tags "accept".
Package Mapping
Object definitions can make use of Soto's package mapping feature:
<soto:app
xmlns:soto="sapia:soto" xmlns:myapp="resource:/org/acmeapps/someapp">
<soto:namespace prefix="myapp">
<package>org.acmeapps.someapp</package>
</soto:namespace>
<soto:service>
<myapp:transactionService />
</soto:service>
</soto:app>
In the above case, upon encountering an element name with the myapp prefix, Soto will attempt resolving the transactionService local name to the fully-qualified name of a class, using the package mapping(s) in the corresponding namespace. Indeed, the soto:namespace element can take one to many package nested elements, of which each should correspond to a Java package. In the above example, Soto will try converting the transactionService local name part to the org.acmeapps.someapp.TransactionService class, if it can be found.
Thus, the local name should correspond to the "short class name" (name of the class without the package). As a convenience, Soto will automatically capitalize the first letter of the local name if needed.
Class Matching
As an additional feature, Soto can match "any" element name to a given specific class:
<soto:app
xmlns:soto="sapia:soto" xmlns:myapp="resource:/org/acmeapps/someapp">
<soto:namespace prefix="myapp">
<def class="org.acmeapps.someapp.xml.XMLTag"
name="*" />
</soto:namespace>
</soto:app>
In the above case, the "*" character that sits in place of a "real" name in the def element indicates that the corresponding class should be used when attempting to find a match for elements with the myapp XML prefix. Meaning that for any element with the myapp prefix, an instance of XMLTag will be created. This feature can be useful in conjunction with the use of the XmlAware interface.
Instantiation/Initialization/Start Order
Services are instantiated when their corresponding XML element is encountered; the init() method is called as soon as a service is instantiated. The start() method is called sequentially on each service once the container's own start() method is called.
Once a service's init() method has been called, its layer instances (if any) also have their init() method called.
Since objects are loaded as their corresponding XML is encountered, services that depend on other services should be configured after the latter. More precisely, this means that service references should refer to already configured services. Of course, this model proscribes circular dependencies, which are not a good practice anyway.
Once a service's init() method has been called (and that, subsequently, all its layers have been initialized), the service is assumed to be in "ready" state; it is important for service implementations to respect this contract: indeed, services that use other services are "counting" on the latter to be ready when assigned to them.
So, then, why a start() method? The start() method is only provided in order for services to "publish" themselves, making their interface available for "external" use. What does that mean? Well, for example, let's say that you implement a RMI server; you would bind your service to a registry in the start() method - but that would not spare your service instance from needing to be available for "internal" use at init() time.
File Includes
Configuration files can grow rapidly. For ease of management, Soto provides an "include" mechanism that allows a configuration to be made of multiple configuration files. You start with one file (the "root" file), and then include other files through a special tag in that root file. Of course, a file can include other files and so on. The following sample shows a file that includes another file:
<soto:service id="secondary"
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<sample:secondary>
<someService>
<soto:serviceRef id="master"/>
</someService>
</sample:secondary>
<soto:include uri="nestedInclude.xml"/>
</soto:service>
The file includes another file, whose path is resolved relatively to the parent (the including file). Indeed, look at the value of the uri attribute; it does not have any protocol scheme, such as in the following:
<soto:service id="secondary"
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<sample:secondary>
<someService>
<soto:serviceRef id="master"/>
</someService>
</sample:secondary>
<soto:include
uri="resource:/com/acme/myapp/nestedInclude.xml"/>
<!-- this could also be valid; Soto supports
accessing system properties.
<soto:include
uri="${user.dir}/config/nestedInclude.xml"/>
-->
</soto:service>
This time, the path is absolute: it is resolved using the full path that is given. In this case, the path corresponds to a resource in the classpath. And now, what if that included file resource itself had includes? Depending on the uri, these includes would be resolved a) relatively to their parent; or b) in an absolute manner. For example, for the above resource, all relative includes would be resolved with resource:/com/acme/myapp/ as a base path. The same rule would apply if the URI scheme would be http, file, etc. - see next section for the supported schemes. In general, the following is true:
There is, of course, one exception: you will notice that one of SotoContainer's load() methods takes an InputStream as an argument. If the configuration corresponding to this stream has relative includes, how are the latter resolved? Well, clearly, there is no way to extract path information from that stream; thus, in such a case, relative includes are not resolved relatively. What Soto does internally is to create a File object with the given path, and attempts to load that file's content. If the latter does not exist, an exception is thrown. So if you want to use relative includes in your initial config file, load that file using a load() method that takes the path to a classpath resource or a File instance has an argument.
It is also possible to pass parameters in the context of an include; the included file will be rendered with these parameters. See the variable interpolation section for more details.
Resource Handlers
As illustrated above, Soto has built-in support for specifying file resources in various "locations", through "well-known" URIs. Soto supports the file, resource and http protocols natively. The "file" and "http" protocols are probably not new to you. But what about the "resource" protocol? Well, the class that handles this protocol is the ClasspathResourceHandler class, that implements the ResourceHandler interface. Implementations thereof have a simple contract: they must retrieve a Resource, given a path.
Custom resource handlers can be registered with the ResourceHandlerChain that a SotoContainer keeps internally. The registerResourceHandler() method on the SotoContainer appends a given resource handler that the chain - see the javadoc for more details.
In addition, objects instantiated through Soto that implement the EnvAware are passed an Env instance that can be used to resolve URIs.
Resource Aliasing
Resource aliasing is a useful feature that allows redirecting URIs to other ones. Imagine, for example, that a prepackaged application expects a configuration file under a given URI; under some conditions, you might want the application to use the content of another file (this could be the case for example when working under different deployment environments). Heres how we use the soto:resourceAlias to redirect all URIs that match a given pattern to a different target URI:
<soto:resourceAlias uri="**/my.properties"
redirect="resource:/org/acmeapps/dev/{1}/my.properties" />
The above redirects all URIs that match the given pattern (specified by the uri attribute) to the URI specified by the redirect attribute. The following should be noted:
- the pattern accepted by the uri attribute takes optional * or **, matching a single token or multiple tokens of the processed URIs.
- the redirect attribute takes one to many special "pattern result" sections, each taking a number corresponding to the position of the matched token(s) in the original URI. Note that positions start at 1, with 0 corresponding to the original URI.
Resource aliases are kept in the container and traversed sequentially every time a URI is resolved. Thus, this implies a certain performance overhead. Note that resource aliasing is single-pass only: URIs are aliased once (already aliased URIs are not further aliased). This limitation was meant to reduce overhead and avoid infinite loops.
Bootstrapping
Upon being loaded, a Soto container will look for a soto.bootstrap property passed through one of its load() methods that takes a Map as an argument (the property will be searched in that Map), or as a Java system property.
The value of the property is expected to hold a list of URIs, separated by semicolons. The container will load the configurations corresponding to these URIs prior to loading anything else. This can be useful when wishing to preload services without the application "knowing" it, or in conjunction with resource aliases (you could configure resource aliases in the bootstrap configuration, therefore redirecting URIs for the rest of the application).
Variable Interpolation
If you look carefully, the SotoContainer class has load() methods that also take a Map instance as an argument. This instance can hold arbitratry name/value bindings that can be recuperated in the configuration through the ${name} notation. To resolve these variables, Soto a) looks up in the given Map and, if no value is found there, falls back to the system properties. If still no value is found, the ${name} - as configured - an exception is thrown.
Interpolation values can also be specified within soto:include elements. This allows to reuse a single included file that this configured with variables, and pass the values for these variable on a per-include basis. The example below demonstrates this:
<soto:service id="secondary"
xmlns:soto="sapia:soto"
xmlns:jmx="soto:jmx"
xmlns:sample="soto:sample">
<sample:secondary>
<someService>
<soto:serviceRef id="master"/>
</someService>
</sample:secondary>
<soto:include uri="nestedInclude.xml">
<param name="foo" value="bar" />
<param name="sna" value="fu" />
</soto:include>
</soto:service>
Parameters defined at a given include scope will only exist for the given include - and any nested one. If no value is found at the nested levels for the expected variables, the parent scopes are searched, up to the system properties.
Post-Injection
As you know by now, Soto allows dynamically calling methods of objects at configuration time in order to set dependencies. It might be required, in addition, to call methods on objects after the instantiation-time injection has been done. For example, a given Soto service might have been already instantiated and "injected", and yet some more dependencies may need to be injected later on. This case can be seen when including (through Soto's include mechanism) generic, predefined configuration files, that may need additional configuration, depending on the context.
To take a more concrete example, imagine the following Hibernate service configuration:
<hb8:hibernate id="petDB" xmlns:hb8="soto:hibernate"> <class>org.acmepetstore.Dog</class> <class>org.acmepetstore.Cat</class> </hb8:hibernate>
Now imagine that the above is saved in a file that is meant to be included in applications that share the Petstore data model, and corresponding Hibernate service. Yet, depending on the context, some applications may wish to add their own class mappings upon including that file. Here's how this is done:
<soto:app xmlns:soto="sapia:soto">
<soto:service id="hibernate">
<soto:inject>
<object>
<soto:include uri="resource:/org/acmepetstore/hibernateService.xml" />
</object>
<class>org.acmepetstore.Shark</class>
<class>org.acmepetstore.Dolphin</class>
</soto:inject>
</soto:service>
</soto:app>
In order to perform "post-injection", the soto:inject element is used. This element takes the following:
- First, an object element is specified, that is meant to hold another element evaluating to the target object that will be injected with additional properties.
- Second, a list of arbitrary elements, expected by the target object.
The soto:inject element in turn "returns" the target object - it thus acts as some sort of temporary proxy, from a configuration point of view.
In addition, the element may be configured directly under the soto:app element. For example, the sample below has the same effect has the previous one:
<soto:app xmlns:soto="sapia:soto">
<soto:service id="hibernate">
<soto:inject>
<object>
<soto:include uri="resource:/org/acmepetstore/hibernateService.xml" />
</object>
</soto:inject>
</soto:service>
<soto:inject>
<object><soto:serviceRef id="hibernate" /></object>
<class>org.acmepetstore.Shark</class>
<class>org.acmepetstore.Dolphin</class>
</soto:inject>
</soto:app>
Note in the above how the soto:serviceRef element is used to set the target object.
Conditional Instantiation
Imagine that you want to instantiate objects in your configuration only if certain conditions are verified. Soto allows you to do this: objects can be created (or not created at all) according to runtime parameters, using soto:if or soto:choose elements.
Remember that you can pass runtime parameters to a Soto configuration and eventually recuperate the values corresponding to these parameters using the ${param_name} notation. Soto further allows you to perform tests on the values of these parameters, through elements that implement conditional branching.
If
The soto:if performs an "if" test on a given runtime parameter. The XML nested within the "if" is "executed" only if the specified condition is true; otherwise, the end result is as if no XML had been specified (and thus amounts to a "noop"). The example below demonstrates the use of the "if":
<soto:service id="secondary">
<sample:secondary>
<someService>
<soto:if param="set.master.service"
equals="true">
<sample:master/>
</soto:if>
</someService>
</sample:secondary>
</soto:service>
If no equals attribute is specified, then the "if" evaluates to true if the specified runtime parameter exists (i.e.: a value is present for it).
Unless
The soto:unless is the opposite of the soto:if. It in fact performs an "if not" test on a given runtime parameter. The example below demonstrates the use of "unless":
<soto:service id="secondary">
<sample:secondary>
<someService>
<soto:unless param="set.master.service"
equals="false">
<sample:master/>
</soto:unless>
</someService>
</sample:secondary>
</soto:service>
The above means: "unless the set.master.service property is specified and its value is false, execute the nested XML".
Choose
The soto:choose is pretty similar to the "if", except that it can take multiple conditions; the first one that matches will see its nested XML being executed. If nothing matches and no otherwise is specified, then no nested XML is executed and the corresonding configuration amounts to a "noop". The example below demonstrates how to use the "choose". As you can see, a "choose" is one-to-many "case" elements (that follow the syntax of the "if"), followed by an optional "otherwise" element. The example is pretty useless, but you can probably imagine more interesting possibilities:
<soto:service id="secondary">
<sample:secondary>
<someService>
<soto:choose>
<when param="set.master.service"
equals="1">
<sample:master message="1" />
</when>
<when param="set.master.service"
equals="2">
<sample:master message="2" />
</when>
<otherwise>
<sample:master message="3" />
</otherwise>
</soto:choose>
</someService>
</sample:secondary>
</soto:service>
Accessing the Environment
Quite often, it is convenient to have access to container-related state/behavior from within service instance. All objects that are instantiated through object definitions (not only services) can potentially have access to container functionality by implementing the EnvAware interface. The interface specifies a single setEnv() method, that takes an Env instance as an argument. The example below gives an example - the Env instance is used to resolve a configuration URL:
public class MyService implements EnvAware, Service{
private Env _env;
private String _configUrl;
public void setEnv(Env env){
_env = env;
}
public void setConfigUrl(String urlStr){
_configUrl = urlStr;
}
public void init() throws Exception{
InputStream is = _env.resolveStream(_configUrl);
...
}
...
}
An Env instance gives access to very useful functionality, not only for retrieving resources (as shown above), but also for looking up services - various lookup methods are provided. One common pattern is to implement an EnvAware instance that will lookup a required service based on its type:
...
public void setEnv(Env env) throws Exception{
datasource = env.lookup(DataSource.class);
}
...
The above can spare extra configuration (associating the above to a DataSource would indeed normally be done through configuration). Of course, this approach holds true only if a single instance of the specified interface exists within the container (if that is not the case, the lookup() method throws an exception).
Accessing Naming Information
In some cases, it might be usefull for instances to have access to their XML naming information (such as specified in the corresponding object definitions). All objects that are instantiated through object definitions (not only services) can have access to their corresponding XML naming information by implementing the XmlAware interface. The interface specifies a single setNameInfo() method, that takes the local name, namespace URI and namespace prefix of the corresponding object definition.
Type Coercion
Basics
When assigning configuration to objects, Soto tries to coerce the data in the configuration to the type that is specified in the corresponding accessor on the configured object, meaning: if a "maxPoolSize" XML attribute is found to be corresponding to a setMaxPoolSize(int) method, that attribute's value is converted to an int. But what if, for an obscure reason, the method's signature is setMaxPoolSize(Object)? This use-case is rather absurd, but it can make sense other times. When that occurs, Soto provides special tags corresponding to Java's primitive types (and to the java.util.Date class as well) that can be used to convert strings to the appropriate types. These tags are:
- soto:boolean
- soto:short
- soto:int
- soto:long
- soto:float
- soto:double
- soto:date
In addition, Soto supports more exotic types:
- soto:class
- soto:file
- soto:resource
- soto:map
- soto:list
- soto:array
- soto:properties
- soto:uri
To better illustrate usage of these tags, let us give you a concrete example; suppose that we have the following classes:
public class SomePool extends Parametrizable{
private int _maxPoolSize;
public void init(){
Param p = super.getParam("maxPoolSize");
_maxPoolSize = ((Integer)p.getValue()).intValue();
}
}
public class Config{
private List params = new ArrayList();
public Param createParam(){
Param p = new Param();
params.add(p);
return p;
}
public List getParams(){
return params;
}
}
public class Param{
private String name;
private Object value;
public void setName(String name){
this.name = name;
}
public void setValue(Object value){
this.value = value;
}
public String getName(){
return name;
}
public Object getValue(){
return value;
}
}
This use case makes sense: the pool instance expects a maxPoolSize of type int. How would we handle this in configuration? Well, as follows:
<soto:app
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<soto:service id="somePool">
<sample:somePool>
<config>
<param name="maxPoolSize">
<value>
<soto:int value="10" />
</value>
</param>
</config>
</sample:somePool>
</soto:service>
</soto:app>
As you can see, it quite straightforward, and nothing stops you from implementing your own tags that perform custom-coercion - look at the source for inspiration. All Soto tags accept a value as an attribute (or as an nested XML child element). The soto:date tag in addition supports a pattern, whose value should correspond to a date parsing pattern as specified in the SimpleDateFormat class. The resulting date is thus parsed according to the given pattern, such as in the following:
<soto:app
xmlns:soto="sapia:soto"
xmlns:sample="soto:sample">
<soto:service id="someService">
<sample:someService>
<config>
<param name="nextShutDownDate">
<value>
<soto:date value="2005/31/12" pattern="yyyy/MM/dd" />
</value>
</param>
</config>
</sample:someService>
</soto:service>
</soto:app>
In addition to having elements for specifying primitive or common values, Soto has some other elements for more "complex" types, as the examples below demonstrate:
Classes
Configuring a java.lang.Class:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:class name="com.acme.app.SomeClass" />
...
</soto:app>
URIs
Configuring a java.net.URI:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:uri value="file:config/application.xml" />
...
</soto:app>
Resources
Configuring a org.sapia.soto.util.Resource (resolved through Soto's resource-resolving mechanism):
<soto:app
xmlns:soto="sapia:soto">
...
<soto:resource uri="resource:config/application.xml" />
...
</soto:app>
Lists
Configuring a java.util.List:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:list>
<soto:int value="1" />
<soto:int value="2" />
<soto:int value="3" />
</soto:list>
...
</soto:app>
Maps
Configuring a java.util.Map:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:map>
<entry key="first">
<soto:int value="1" />
</entry>
<entry key="second">
<soto:int value="2" />
</entry>
<entry key="third">
<soto:int value="3" />
</entry>
</soto:map>
...
</soto:app>
Arrays
Configuring an array of java.lang.Strings:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:array type="java.lang.String">
<soto:string value="string_1" />
<soto:string value="string_2" />
<soto:string value="string_3" />
</soto:array>
...
</soto:app>
Properties
Configuring java.util.Properties:
<soto:app
xmlns:soto="sapia:soto">
...
<!-- one explicitly, through XML -->
<soto:properties>
<property name="first" value="one" />
<property name="second" value="two" />
</soto:properties>
<!-- through a Soto URI -->
<soto:properties
uri="resource:/com/acme/app/someProperties.xml" />
...
</soto:app>
Files
Configuring java.io.Files:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:file name="someDir" create="true" />
...
</soto:app>
Constants
Configuring constants:
<soto:app
xmlns:soto="sapia:soto">
...
<soto:constant name="TYPE" class="java.lang.Integer" />
...
</soto:app>
Best Practices
Use Interfaces
Needless to say, specify service behavior through interfaces. This makes replacing an implementation with another seamless, and offers a lot of other advantages that you are certainly aware of.
Decompose
Have your services perform specific, targeted tasks. This favors team development, makes code so much easier to maintain, and offers a lot of other advantages that you are certainly aware of ;-).
Conclusion
Use Soto as an embeddable application framework. The reflection-bases configuration handling approach spares you from tedious manipulations, and the layer concept allows you to separate your core business logic from "generic" features.