Elmo has three main levels of operation: ElmoManagerFactory, ElmoManager, and Entity. Normally, only one ElmoManagerFactory would exist for each domain model used with the repository and only one ElmoManager for each repository connection. An Entity would exist for each individual used within a connection. ElmoManagerFactory and ElmoManager are interfaces to the classes SesameManagerFactory and SesameManager. This is shown in the following class diagram.
The ElmoManagerFactory can create ElmoManagers. When ElmoManagers are created, they operate on a unique connection to the repository. Through this connection all the properties of created entities are read and updated.
Entities are referenced by a qualified-name (QName). Each qualified-name must include a namespaceURI and a localPart that is unique within the namespace. An example of a namespaceURI is "http://www.example.com/rdf/2008/data/". This namespaceURI will be referenced as "NS" throughout the examples in this document.
An ElmoModule can be created using its default constructor and must be created before instantiating an SesameManagerFactory. ElmoModule provides methods to register roles, literals, and factories, it is also used to load RDF datasets, set the operating context, and scan jars. These methods set the scope of the manager factory when instantiated.
Role registration is described in the Section called Registering Concepts & Behaviours. Literal registration is described in the Section called Literals. Factories are described in the Section called Factory Classes. Dataset can be loaded into and replace a context. Each ElmoModule can include a primary context, which will contain all new statements added.
The SesameManagerFactory is created with a module and a repository. An existing repository can be passed to the manager factory or the factory can create (and shutdown) a repository itself. A repository can be created using a repository id to a remote sesame server (passing a URL), a local data configuration directory (passing a File dataDir), or an application id, used to locate the data directory. If no repository or repository id is provided an in-memory repository will be created with no persisted storage.
Elmo's entity manager interface provides common methods to designate, merge, find, rename, and remove entities. It can be created using the SesameManagerFactory class or accessed by framework specific notation. For command oriented applications, a new ElmoManager should be created for each command (or request). The overhead for creating a new ElmoManager is low and resources are shared by managers created in the same ElmoManagerFactory.
The manager provides a method to designate (or create) entities by a qualified-name (QName). Blank or anonymous entities can be created by using the overloaded designate method and will operate without a qualified-name. The designate method is called with a Java interface (concept), and the ElmoManager will create a new entity with the subject type of the given concept. Calling designate with a QName that is already used will cause the subject to take on multiple roles to satisfy each designate request. If the entity already implements the concept given, it is equivalent to calling find and casting the result.
Take an example of a company where an employee is an engineer, but also does sales. In Elmo this can be modelled as shown below where a subject can implement both a SalesRep and an Engineer at the same time, by calling designate with the same QName. This technique can be used to integrate and extend domain-models without modifying their source code.
assert SalesRep.class.isInterface(); assert Engineer.class.isInterface(); ElmoModule module = new ElmoModule(); module.addRole(Engineer.class); module.addRole(SalesRep.class); factory = new SesameManagerFactory(module); manager = factory.createElmoManager(); QName id = new QName(NS, "E340076"); manager.designate(SalesRep.class, id); Object john = manager.designate(Engineer.class, id); assert john instanceof SalesRep; assert john instanceof Engineer; |
Elmo supports a form of persistence ignorance. When a property is assigned to an object, created outside of the manager and directly implements a recorded concept, it will be merged into the repository. If the object has the method getQName, it will be merged with the QName returned. If it does not have this method or the result is null, it will be merged with a newly designated anonymous resource. Merging can also be done explicitly by using the merge method on the manager. This method will merge the object into the repository, returning the managed entity. Note that the managed entity will not extend the class, but only implement all the recorded concepts.
By default the interface java.util.List is registered as an interface for rdfs:Container. This means that any object created outside of the manager, which implements java.util.List, will be merged into the repository as a rdfs:Container when it is assigned to a property or merged explicitly.
The manager has a find method to directly load entities by a QName. Entities can also be retrieved by type or query, through findAll and createQuery methods respectively. In addition, the manager provides a method to remove the entity from the pool (RDF repository). This method will remove all references to the entity and its properties. If an entity includes another entity as its property value it must be removed separately, this includes anonymous entity, such as container or list entities.
The entities do not persist any values internally and all information is stored in the underlying repository connection. In the example below we can see that the entities john and jonny are separate Objects, but both retrieve their values from the same repository. The ElmoManager only persists concept properties with @rdf annotation on the getter method.
ElmoModule module = new ElmoModule();
module.addRole(Employee.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();
QName id = new QName(NS, "E340076");
Employee john = manager.designate(Employee.class, id);
Employee jonny = manager.designate(Employee.class, id);
assert john.equals(jonny);
assert john != jonny;
john.setName("John");
assert jonny.getName().equals("John"); |
Entities are not serializable and are only valid during the life of the ElmoManager (usually just a transaction). The Entity interface, implemented by all entities returned from the manager, provides the getQName method to allow entities to be restored at a later time by another manager. Anonymous entities return null from their getQName method and cannot be restored using the find method. Shown below is the QName property of the Entity interface.
factory = new SesameManagerFactory(new ElmoModule()); manager = factory.createElmoManager(); QName id = new QName(NS, "E340076"); Entity john = manager.find(id); assertEquals(id, john.getQName()); // the subject john has the URI of // "http://www.example.com/rdf/2008/data/E340076" |
The ElmoManager can retrieve both SPARQL and SeRQL tuple queries of entity resources through the createQuery method of the ElmoManager. These results are then converted into Objects or arrays of Objects in an iterator. Only one query language can be configured for an ElmoManager at a time using the setQueryLanguage method of the SesameManagerFactory.
ElmoModule module = new ElmoModule();
module.addRole(Employee.class);
factory = new SesameManagerFactory(module);
factory.setQueryLanguage(QueryLanguage.SERQL);
manager = factory.createElmoManager();
Employee john = manager.designate(Employee.class);
john.setName("John");
String queryStr = "SELECT emp FROM "
+ " {emp} <http://www.example.com/rdf/2008/model#name> {name}";
ElmoQuery<?> query = manager.createQuery(queryStr);
query.setString("name", "John");
for (Object emp : query) {
assert ((Employee) emp).getName().equals("John");
} |
In Elmo, a role is defined as a concept or behaviour. A concept, in Elmo, is a JavaBean interface with an @rdf annotations describing the subject type of the interface and the predicates of each mapped Bean property. Elmo ships with a collection of popular ontologies defined as Elmo concepts. They can be found in the elmo-concepts module.
Every @rdf annotated concept property must be a single or a set of entities and/or literals (sometimes referred to as value objects). These correspond to the RDF structure, where everything is a resource or a literal and properties have multiple values or a single value (where value is an entity or literal). Elmo uses Sesame, an RDF repository, for the persistence layer and all @rdf annotated concept properties are mapped to an RDF triple, consisting of subject, predicate, and value. RDF repositories are semi-structured. You can use any URI (or URL) you want, but it must be unique to that property or role and every persistent concept property must have a predicate. For more information about Sesame and RDF please consult the Sesame User Guide or FAQ.
The @rdf and @inverseOf annotations are placed on getter methods of Bean interfaces. They are placed on methods that have no parameters, a return type, and start with "get" or if a boolean "is". A setter method is not required, but if present it will also be implemented. These methods will be implemented to map to the RDF values in the repository. The return type can be any registered concept, literal, or java.util.Set of the above. When a setter method is not present the property is read-only. @inverseOf annotation is used to indicate inverse relations as show below. The @rdf annotation takes precedence over the @inverseOf annotation, if both are specified, and only the first URI is used for reading the values from the repository.
@rdf("http://www.example.com/rdf/2008/model#subteam")
public Set<Team> getSubteams();
public void setSubteams(Set<Team> subteams);
@inverseOf("http://www.example.com/rdf/2008/model#subteam")
public Team getParentTeam(); |
The localized annotation can be used on properties that return a String or a Set of Strings. When this annotation is present the Locale of the ElmoManager is associated with assigned values, and when retrieved the closest set of values for the Locale of the ElmoManager are returned.
If inferencing is enabled in the factory, through setInferencingEnabled, and the getter method of a concept property contains additional URI values, all changes to the property will also modify each listed predicate, and inverseOf predicates will be modified with the subject and value reversed.
public interface Team {
@rdf("http://www.example.com/rdf/2008/model#members")
public Set<Employee> getMemebers(); // read-only
@rdf({"http://www.example.com/rdf/2008/model#teamLead",
"http://www.example.com/rdf/2008/model#members"})
@inverseOf({"http://www.example.com/rdf/2008/model#leaderOf"})
public Employee getTeamLead();
public void setTeamLead(Employee value);
@rdf({"http://www.example.com/rdf/2008/model#salesRep",
"http://www.example.com/rdf/2008/model#members"})
public SalesRep getSalesRep();
public void setSalesRep(SalesRep value);
@rdf({"http://www.example.com/rdf/2008/model#engineer",
"http://www.example.com/rdf/2008/model#members"})
public Engineer getEngineer();
public void setEngineer(Engineer value);
} |
In the example above (with inferencing enabled) the members property will be updated as the properties teamLead, salesRep, and engineer are modified. The property leaderOf of Employee will also be updated with their Team as the teamLead value is changed.
The annotation oneOf can be placed on the setter method to assert the URI (value) or label values are correct. This check is only performed when assertion is enabled, which can be enabled with the vm argument "-ea".
@rdf("http://www.example.com/rdf/2008/model#gender")
public String getGender();
@oneOf(label={"M", "F"})
public void setGender(String value); |
Role annotations are placed on concepts or behaviours to statically associate them with the subject types or individuals that they should be composed with.
The @rdf annotation, in addition to being used on properties, is also used on the class/interface to associate it with a primary subject type. On class/interface declarations, alias subject types can be indicated with additional annotation values. The role will be included for any subject of any type in @rdf.
@rdf("http://purl.org/rss/1.0/channel")
public interface Channel {
@rdf("http://purl.org/rss/1.0/link")
public Set<String> getLink();
public void setLink(Set<String> link); |
Above we have defined a Channel concept that should be used with every resource of the type "http://purl.org/rss/1.0/channel". It has one property named link which maps to the RDF predicate "http://purl.org/rss/1.0/link".
To indicate restrictions of what types this role cannot be created with, the disjointWith annotation can be used. This is only checked when assertions are enabled and is only read from concepts passed on the create method of the ElmoManager.
@rdf({"http://www.example.com/rdf/2008/model#Customer",
"http://www.example.com/rdf/2008/model#Client"})
@disjointWith({Employee.class})
public interface Customer {
...
} |
These annotations are an alternative to directly mapping roles to an RDF subject type. They provide a way to assign roles based on other roles of the entity or based on their qualified-name. In this way role specific behaviours can be defined based on a combination of roles, the absence of a role, or on a set of individuals.
The intersectionOf annotation takes a list of roles, that if a entity implements all of the given roles, it will also implements the declared role. The complementOf annotation takes a single role as argument; if any entity does not implement the given role, it will implement the declared role. The oneOf takes a list of qualified-names; if any entity is created with one of the qualified-names it will implement the declared role.
By registering a public interface or class with a subject type, Elmo will dynamically include this role with all resources of the defined type. Registering concepts is required in order for them to be used with the designate method. The composition of roles can facilitate decentralized design and development for an application or a suite of applications.
Roles are statically loaded from "META-INF/org.openrdf.elmo.roles" files. These files may be included in any number of jars or paths in the Java class-path. The format of these files is the full class name optionally assigned to one or more URI subject type. If the @rdf class annotation is present its value will also be included. If no mapping is provided, the role will be applied to the role annotations of its interfaces. Here is a sample of the contents of the file.
org.openrdf.concepts.foaf.Person org.openrdf.concepts.foaf.Agent org.openrdf.concepts.dc.DcResource |
If this file does not contain any classes, but does exist in a directory or jar in the class-path, the entire directory or jar will be scanned for classes with a role annotation or classes that extend or implement a annotated class and automatically register them. Each role will be logged at the DEBUG level when being registered. Roles can also be registered at run-time using the addRole methods of the ElmoModule.
ElmoModule = new ElmoModule();
module.addRole(Engineer.class,
"http://www.example.com/rdf/2008/model#Engineer");
// URI type of SalesRep is retrieved from the @rdf annotation
module.addRole(SalesRep.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager(); |
A behaviour, also called a mixin, is a concrete or abstract class that implements one or more methods for an Entity. Behaviours provide functionality other then RDF property mapping. They simplify code by capturing design patterns into reusable classes. Behaviours are created with a factory, or a constructor with an interface implemented by the entity. Behaviours are registered in the same way that concepts are registered, which is described in the Section called Registering Concepts & Behaviours.
Behaviour factories are also registered in the ElmoModule. Factories use the same role mapping as concepts and behaviours. They are used to create a behaviour for used in an entity. An example of using a behaviour factory can be found in the Section called Dependency Injection.
Behaviours without a corresponding factory must have a default constructor or a constructor with a single parameter. This parameter can be any interface that the entity will implement, including the Entity interface itself. Behaviours have access to all properties and other behaviours within the entity through abstract methods or the constructor argument.
Behaviours can be used to implement methods for the entity's interfaces. Below is the EmployeeSalarySupport behaviour. Here the class implements the paySalary method for an Employee. It splits the salary amount into two bank accounts based on the properties of the employee.
public abstract class EmployeeSalarySupport implements Employee {
public void paySalary(double amount) {
double invest = getInvestmentPercent() * amount;
BankAccount fund = getInvestmentAccount();
BankAccount checking = getSalaryDepositAccount();
if (invest >= amount) {
fund.deposit(amount);
} else if (invest > 0) {
fund.deposit(invest);
checking.deposit(amount - invest);
} else {
checking.deposit(amount);
}
}
} |
Interceptors are behaviours that have the @intercepts annotation on one or more of their methods. They are used to maintain a 1:1 mapping between design concept and implementation for cross cutting concerns. The annotation @intercepts must be placed on methods that take the same parameters as the method(s) intercepted or an InvocationContext as the parameter.
The @intercepts annotation uses pattern matching to filter which methods of the entity should be intercepted. By default every method of the entity with the same method name will be intercepted. The annotation provides six inclusive ways to select which Methods to intercept.
is an regex match on the entire Method name, if not provided the method name must have the same name as the method being interepted.
checks the number of parameters.
ensures the parameterTypes of the method are equivalent to the classes given.
checks for equivalent or superclass of the Method's return type.
checks if this class has this method.
is the name of a static method in the interceptor class that takes a Method argument and returns a boolean indicating if this method should be intercepted.
Below is an example of using an interceptor.
@rdf("http://www.example.com/rdf/2008/Message")
public class EmailValidator {
@intercept(method="set.*Email.*", parameters={String.class}, returns=Void.class)
public Object setEmail(InvocationContext ctx) throws Exception {
String email = (String) ctx.getParameters()[0];
if (email.endsWith("@example.com"))
return ctx.proceed();
throw new IllegalArgumentException("Only internal emails are supported");
}
}
ElmoModule module = new ElmoModule();
// if no URI and no @rdf, applies to all entities
module.addRole(EmailValidator.class);
module.addRole(Message.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();
Message message = manager.designate(Message.class);
message.setFromEmailAddress("john@example.com"); // okay
message.setToEmailAddress("jonny@invalid-example.com"); // throws exc |
Factory classes are classes that create behaviour instances. They are registered using the method addFactory in the ElmoModule or in the file "META-INF/org.openrdf.elmo.factories", using the same format as for roles. All factory classes must use the @factory annotation on their class and each of their factory methods. The factory class must have either a static getInstance method or a public default constructor.
The factory classes are mapped in the org.openrdf.elmo.factories file or mapped with role annotations. The role annotations may be placed on the factory class or on the return type of the factory methods or on an interface implemented by the return type. The return type of each factory method must also be registered for, or be an interface of, the entity being constructed. If the factory method has a parameter it must be assignable from the entity being constructed.
See the Section called Dependency Injection for an example.
Any mapped @rdf property value, that is not an entity, is either a java.util.Set or a literal object (java.util.List is an entity). A literal Object must be Serializable, have a String constructor, or a static valueOf method, if not directly supported by the LiteralManager. Elmo ships supporting most common Java literal Objects. Elmo also includes mapping to primitive XML-Schema data-types that correspond to a standard Java Object.
Elmo, like Java5 collections, uses run-time type checking on object retrieval when casting. Elmo uses the data-type of literals exclusively to look up the Java Object that should be instantiated and does not convert the Object to conform to a property type. If the data-type of the literal in the repository does not correspond to the properly type of the concept a ClassCastException will occur at run-time when the literal is retrieved. Any data that is imported into the repository must contain the correct data-types for every literal, according to the org.openrdf.elmo.literals file. Note that no data-type is used for java.lang.String class.
Elmo does has some flexibility when retrieving literals from the repository. For example the xsd:positiveInteger and xsd:integer, both resolve to java.math.BigInteger. However, in the repository, literals of data-type xsd:positiveInteger do not equal literals of xsd:integer, therefore if a collection contains "1"^^xsd:positiveInteger it may not contain "1"^^xsd:integer. This will cause the Set#contains(Object) to fail when searching for a match of java.math.BigInteger("1") as it will be searching for "1"^^xsd:integer.
This data-type mapping can be extended by including a file "META-INF/org.openrdf.elmo.literals". This file may be included in any number of jars or paths in the Java class-path. The format of the file is the full class name of the type assigned to the URI of the data-type that should be used when saving it to the repository.
javax.xml.datatype.XMLGregorianCalendar = http://www.w3.org/2001/XMLSchema#dateTime java.util.Locale = http://www.w3.org/2001/XMLSchema#language |
By default Elmo uses XMLGregorianCalendar as the Java Object for all XML-Schema time or date related data-types. When adding an XMLGregorianCalendar to the repository it will use the xml schema type of the object when storing. Other date objects are supported, but by default will use the data-type "java:" + full class name of the Object.
Elmo properties can use the collection type java.util.Set and java.util.List for property values. Properties with this type can be assigned instances from the Java collections framework, that implement one of these two interfaces. When removing entities from the repository, any property with a type of java.util.List will need to remove the list explicitly before removing the entity itself. This is not the case for java.util.Set, as it is removed along with the entity.
Through the adaptor SesameContainer, entities of type rdfs:Container, including rdf:Seq, rdf:Alt, and rdf:Bag will implement java.util.List. When other java.util.List objects are merged into the repository, through a setter method or explicitly with the merge method, they will have the rdf:type rdfs:Container and when loaded again they will use this adaptor.
The adaptor SesameList implements java.util.List for resources of type rdf:List. Often RDF file formats provide a short hand format for rdf:List resources. These short hands may not include the required rdf:type statement for the list and therefore may not be imported properly into the repository for Elmo to read. In addition, rdf:Lists are implemented as a chain of rdf:Lists, so when removing them from the repository each must be removed explicitly, or cleared before being removed. Because of these three limitations for rdf:List resources, it is recommended to use one of the above rdfs:Containers as an alternative to rdf:List.
Another RDF collection is a set of statements that share a common subject and predicate. These statements are contained in the class ElmoProperty, which implements java.util.Set for the statement's objects. This collection is used for every @rdf property that returns a java.util.Set. Therefore collection properties can exist for java.util.Set, java.util.List, or one of the previously listed RDF concepts (rdf:Seq, rdf:Bag, rdf:Alt).
An alternative to creating static concepts is the DynaClass/DynaBean interface from commons-beanutils. The DynaBeanSupport behaviour provides the DynaBean interface for any resource. It uses the RDFS / OWL ontology in the repository to resolve none-prefixed property names. When using none-prefixed property name, they will either return a single value or a set of values depending on the ontology specification of the property. When using prefixed properties, however, the ontology is not used and all properties return a set. Property can be prefixed with a namespace prefix from the repository or the entire predicate URI. This behaviour is not active by default. Here is an example of how this behaviour can be activated and used.
ElmoModule module = new ElmoModule();
module.addRole(DynaClassSupport.class);
module.addRole(DynaBeanSupport.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();
QName id = new QName(NS, "E340076");
QName type = new QName("http://www.example.com/rdf/2008/model#","User");
DynaClass userClass = (DynaClass) manager.find(type);
DynaBean entity = manager.rename(userClass.newInstance(), id);
entity.set("userName", "john");
assertEquals("john", entity.get("userName")); |
| [1] | ElmoManager Class Diagram |
| [2] | Entity Class Composition |