This document explains how to write an application using the IberAgents platform.
This guide explains the necessary steps that a developer must take to build an IberAgents application, from the point of view of a user (i.e. someone who does not want to mess with framework internals, just use them).
A developer may choose to use IberAgents from the start, or she may want to adapt an existing application. Both approaches are explained below, with an emphasis on adaptation.
IberAgents components are discussed first, followed by configuration, discovery and persistence features. Next comes an explanation of application deployment. The conclusion contains pointers to other documents of interest.
IberAgents is especially suited for distributed applications, where several nodes (computers where the framework is running) communicate between them. In each node, a set of components is deployed; not all nodes need run all components. E.g. the global registry keeps track of services in any node, and it should run on just one node.
The organization of IberAgents includes bare Components which are only accessible from the local node, Services which are accessible from other nodes, and Agents which are executed while the application is running. See the architecture documentation for more details.
To define a component, the developer can isolate external behaviour in an interface that extends one of those described above. Only the methods included there will be accessible from other components.
The implementation of the component itself is then defined in a class that implements this interface. This separation of interface and implementation, very usual (and mandatory) in other languages such as C++, is sometimes convenient for IberAgents components. An example is shown below.
public interface Foo extends Service
{
void doSomething();
}
public class Bar implements Foo
{
public void doSomething()
{
System.out.println("Hello world!");
}
public boolean ping()
{
return true;
}
}
Note: the method ping() is present in all IberAgents services; it returns the current state of the component, on or off.
Sometimes it is convenient to have just one class for the component. This is particularly so for creating configuration, user and administration components. The separate interface is only necessary when there are several implementations of the same component, as in the persistence layer
The art of componentizing an application is by no means an exact science. However, some guidelines can be highlighted to the developer experienced in pattern modelling.
As a rule of thumb, you usually have one or two components per Java package, that expose its functionality to other components.
IberAgents keeps separate configuration files for each running application (see deployment architecture for more information. When building your own application, you should decide which data will be available for users to modify, and then place this data in your configuration file
Any component that implements the interface Configurable is saying that it expects a configuration object. Its configure(Configuration) method will eventually be called, passing in the correct Configuration object. To illustrate this point we will build a component that is defined in just one class (without an interface).
The first step is thus to implement Configurable in your component:
public class Foo implements Service, Configurable
{
...
}
The component must then implement the method to receive your desired configuration object. It is usually stored as an attribute.
public class Foo implements Service, Configurable
{
private FooConfiguration configuration;
public void configure(Configuration configuration)
{
this.configuration = (FooConfiguration)configuration;
}
...
}
How does the framework know which type of configuration must be sent to this object? This information is stored in the configuration object itself, along with any data as a bean object. The example below shows a configuration with just one piece of data, an integer value:
public class FooConfiguration implements Configuration
{
private int integerValue;
public int getIntegerValue()
{
return this.integerValue;
}
public void setIntegerValue(int integerValue)
{
this.integerValue = integerValue;
}
public String getTargetComponent()
{
return Foo.class.getName();
}
}
Sometimes there is data which must be accessible for several components. The structure to use here is a ConfigurationComponent: a special type of component which is just a container of configuration values. It is very simple to implement one; an example is shown below, containing just one integer value.
public class BarConfiguration extends ConfigurationComponent
{
private int integerValue;
public int getIntegerValue()
{
return this.integerValue;
}
public void setIntegerValue(int integerValue)
{
this.integerValue = integerValue;
}
public boolean ping()
{
return true;
}
}
Access to its data is done as follows (see below for a thorough discussion):
BarConfiguration configuration = (BarConfiguration)Finder.findConfiguration(BarConfiguration.class); int value = configuration.getIntegerValue();
Configuration objects and components are stored into configuration files; a generic one for the framework and another per application. These files can be edited by hand, although it is usually easier to use the included configuration editor.
These files have all the same structure: a container object that holds separate arrays with configurations and configuration components. Each object is stored using IberAgents serialization (see the architecture guide); an extract of the global file platform.xml is shown below.
<element type="com.iberagents.registry.remote.RemoteConfiguration"> <globalRegistryNode type="com.iberagents.bind.node.PlatformNode"> <directory type="string">/iberagents</directory> <host type="string">localhost</host> <port type="int">8004</port> </globalRegistryNode> </element>
A configuration object of type RemoteConfiguration is stored here inside the array of configurations; it includes just a parameter of type PlatformNode, which in turn contains three parameters (directory, host and port). The types of all objects are specified as type attributes: the first two are text strings, while the third is an integer. Values are stored as element contents.
Components are not isolated entities; they need facilities provided by others to work, as described in the architecture guide. An easy mechanism is provided to locate them, either locally or across the network.
The class Finder has two methods to locate components on the local node.
These searches are quite fast, since they use a hashed table for lookup. Each call results in two boolean attribute comparisons and four method calls, including one hash table lookup.
An example is shown below on how to locate two local components named Foo and Bar in package com.fooproject.
Foo foo = (Foo)Finder.findLocal(Foo.class);
Bar bar = (Bar)Finder.findLocal("com.fooproject.Bar");
Sometimes, the developer may want to call a component that may or may not be found in the local node (i.e. a service); there is also the possibility of finding the service on a specific node. When the service resides on a different machine, a proxy object is returned that implements the same interface as the component. Any method call results in a soap message being sent to the remote node, and its answer is then deserialized and returned as an object.
These methods only work with service components; be sure to implement the Service interface on all components that need to be accessed remotely.
In some occassions, the developer will want to call all nodes, and receive all answers at the same time. In this case a com.iberagents.bind.Collector is required. The following method is used.
Collector Finder.createCollector(Component, Class);
This collector object is then used to create a proxy to the service, which can be accessed as a regular service. The collector will asynchronously call all nodes that contain the service, and aggregate the responses. A call to its getAnswers() method returns a hash map with (node, object) pairs corresponding to each response.
An example will make it clearer; we suppose that the service Foo resides on several nodes, and the method findSpec() returns a string with some node information. The following code prints a line with each node.
Collector collector = Finder.createCollector(this, Foo.class);
// create the service proxy
Foo foo = (Foo)collector.createProxy();
// ignore response from this method
foo.findSpec();
// wait until answers have arrived
HashMap map = collector.getAnswers();
// go over all nodes
Iterator iterator = map.keySet.iterator();
while (iterator.hasNext())
{
// the key is the node
Node node = (Node)iterator.next();
// the value is the response
String spec = (String)map.get(node);
System.out.println("node " + node + ": " + spec);
}
The method getAnswers() blocks until responses from all nodes have been collected, or the timeout specified in com.iberagents.bind.send.DelivererConfiguration has been reached. If some nodes throw an exception, it is removed from the map before returning the answers -- so that the number of answers can be smaller than the total number of nodes where the service resides. Timeouts also result in fewer answers.
There are a couple of convenience methods in Finder for easy and fast component location:
Binder Finder.findBinder(); Logger Finder.getLogger();
Obtaining the logger this way has the advantage of not having to perform a lookup in the local registry. (The binder must still be looked up.) In all other respects both methods are identical to calling findLocal() and casting the result.
Whenever the developer wants to store information in a way opaque to the user, the persistence layer should be considered. IberAgents stores bean objects using its own format, either to file or to an underlying database; it can be extended to allow for easy access to external databases.
As always, your architectural concerns can be answered through the architecture guide.
The requirements for persistent objects (implementing com.iberagents.persistence.Persistent) are just to contain a primary key with accessor methods, and to be a bean object. The primary key must be unique to each object of the same class to be stored or retrieved.
Only those bean objects that must be stored or retrieved directly have to implement the interface Persistent; nested beans are managed internally.
Aside from storing, reading and deleting individual objects, a couple of methods provide flexible functionalities:
They are based on the com.iberagents.persistence.Query class. It is created for a particular class, and as constraints are added, more requirements will be placed on objects to retrieve or delete. The kinds of constraints are explained below.
Queries can be easily extended to accept other constraints, should they be found necessary.
Sometimes, a developer may want to have a local cache of persistent objects, so that they are not stored right away -- or they can be retrieved without calling the back-end. The component com.iberagents.persistence.local.LocalPersistence is used to manage com.iberagents.persistence.local.CachedPersistent objects, which define their own latency time before a persistence operation is performed on them (store or read).
If several applications are run on a single node, there is no need to include them all as separate libraries; you can use the deployment features in IberAgents, overviewed in the architecture guide. This way you get automatic application reloading and complete class loader separation, as in some J2EE containers.
It is very easy to build an application, especially using Apache Ant. The configuration file must be called configuration.xml, and be placed in the top directory of the jar file. Classes and XSL files can reside anywhere; they will be found by the Deployer component and loaded.
The following Ant target, taken from the ibertest.jar sample application included with iberagents, compiles and builds the ibertest application jar.
<target name="build" depends="init" description="Compiles and builds the application">
<!-- compile from src into build -->
<javac srcdir="${src}" destdir="${build}">
<classpath id="project.class.path">
<pathElement path="${build}"/>
<pathelement location="${lib}/junit.jar"/>
</classpath>
</javac>
<!-- copy configuration.xml and test xsl -->
<copy file="${files}/configuration.xml" tofile="${build}/configuration.xml"/>
<copy file="${files}/testPage.xsl" tofile="${build}/testPage.xsl"/>
<!-- put everything in lib into the jar -->
<jar jarfile="${dist}/lib/ibertest.jar" basedir="${build}"/>
</target>
After going through this guide, you are probably eager to develop a new application. A step-by-step tutorial that builds the included test application is available; a more advanced tutorial shows how to create a personal agent.
Online documentation for architecture and features is available. The status of the framework can be useful to decide whether IberAgents is suitable for use in a given project, along with security information.
Updated: Dec 3 2004.
© 2004 Ibermática.
Webmaster