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.
| The Soto distribution comes with demos. Instructions pertaining to these
demos are available in the README.txt file in the root of the distribution. |
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.
| AOP toolkits use byte-code manipulation to insert interceptors in classes at compile-time
or at runtime. The layer concept does not imply the use of byte code manipulation. In the case of the
JMX layer for example, MBeans are created dynamically, at runtime, using the Java Reflection API. No
interception is made in this case. Thus, one could say that "AOP implements layer" - as pretentious
as this may sound. |
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.
|
For more on configuring your own implementations, have a look at the Object Definitions section
further below.
|
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.
| If you want to map a static inner class to a name using a def
element, YOU MUST follow Java notation and use the '$' separator charater between the class name and
the inner class name (ex: org.sapia.soto.examples.MyClass$InnerClass). |
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.
| Soto's built-in services are configured with URIs that start with the soto
scheme. For example: soto:properties, soto:jmx, etc.
These URIs are shortcuts, so to speak, that are resolved by a Soto container through a special hack: they are
mapped to the org/sapia/soto/defs package. In the case of the
soto:jmx URI, for example, the org/sapia/soto/defs/jmx.xml
file is loaded - and so on. |
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.
|
Note that any class explicit object definition will have precedence over package mapping.
|
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.
|
Note that class matching follows explicit object definitions and package mapping in the resolution order.
|
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:
| Relative includes are solved relatively to their parent include. |
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.
| The soto:include element takes a single nested XML element
that must correspond to an object definition. |
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.
|
Resource aliasing is very useful when combined with bootstrapping.
|
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).
| The soto:if element takes a single nested XML element
that must correspond to an object definition. |
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".
| The soto:unless element takes a single nested XML element
that must correspond to an object definition. |
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>
|
| The case and otherwise elements
takes a single nested XML element that must correspond to an object definition. |
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).
| An Env instance can be retrieved directly from
a SotoContainer, using the toEnv() method
on the container instance. |
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.
|