Libreto API: For Those Who Know Java
(version 1.0)
Table of Contents
1. OntoBox
1.1. What Is It For?
OntoBox is a data/knowledge storing system with strong management features.
- a database (it is object-oriented)
- a map system (it offers more expressive tools and class hierarchies)
- an ontology system (it is more efficient, and simpler for the user)
- an object database (it is simpler; it can be embedded into knowledge management systems)
- an XML or a proprietary format (OntoBox has its own data storage with a high-level API)
The version of OntoBox described here can be used in various Java applications as an embedded data/knowledge storage. OntoBox provides efficient object-oriented data management and storing.
1.2. How to Use This Document?
This document is intended for the developers who are familiar with Java and understand the basic principles of object-oriented programming.
You can start with section 9 of the document to get acquainted with a code example, and then read the rest.
Sections 2-4 are necessary for understanding the other sections.
Sections 5-7 describe different access levels, we recommend to read these sections together with JavaDoc.
Section 8 is written for experienced programmers acquainted with other sections.
Section 10 is necessary only to those who used to work with the old OntoBox model, and can be skipped by the others.
This document is structured as a brief description of all the basic issues concerning work with OntoBox. The re-reading of the document's parts is useful for the re-examination of these issues on the basis of newly gained experience.
1.3. OntoBox Basic Components
There are three basic components of the system:
- OntoBox API (an object model, a set of programming interfaces) providing access to the other components. The developers work mostly with this component.
- OntoBox Storage is a basic OntoBox API implementation. It provides basic features for (long-term) data storing.
- Libretto is a query language working in OntoBox. The Libretto interpreter works with data via the OntoBox API.
1.4. Access Levels
There are three basic access levels:
- Lower: directly through the OntoBox API (most efficient, but very cumbersome)
- Intermediate: through special helper classes (less efficient, the code is more compact)
- Higher: via Libretto (efficiency differs, the code is compact and elegant)
It is possible to use all these levels in Java applications, separately or mutually. We recommend to start with higher levels, with further deploying lower levels in those parts of the code, where efficiency is the key point.
2. Data structures
2.1 Basic Data and Terminology
OntoBox uses object-oriented data structures, which have some important differences with the familiar Java structures.
A class is a set of objects (like in Java).
A superclass is like that in Java, but with the allowance of multiple inheritance (see 2.3).
A subclass is like in Java.
A type is a set of datatype values / primitive data (like primitive data in Java).
A property resembles properties/fields/attributes in Java. If the values of a property are type values (strings, integers etc.) then such a property is called a t-property. If the values of a property are objects, this property is an o-property. E.g. the analogue of the Java's field String name is a t-property having strings as values. And the analogue of Person person is an o-property, the values of which are objects of the class Person.
The domain of a property is a class, for which the property is defined. This property can be used within the class itself and (by inheritance) all its subclasses.
The range of a property is a class or a type, which contains the values of this property. Types can serve as the ranges of t-properties, and classes as the ranges of o-properties.
Cardinality is the number of values of a property (see 2.6).
An object resembles a Java's object, but objects in OntoBox are always uniquely named (see 2.8). Also they can belong to several classes simultaneously (see 2.4).
An ontology is intuitively close to the notion of a package in Java. An ontology comprises classes, types, properties and objects.
An entity is either a class, a type, a property, or an object. Sometimes (depending on the context) ontologies are also considered as entities.
The name (the full name) of an ontology is its URI.
The name (the full name) of an entity, syntactically, is the concatenation of an ontology name (to which the entity belongs), the symbol '#', and the local (short) name of the entity.
The local (short) name is the segment of the full name, which starts after '#' in the (full) name ('#' is excluded).
The URI of an ontology does not have to correspond to a real resource on the Web. The URI is used to secure the uniqueness of the ontology's name.
For instance, the system entities of OntoBox are placed in the ontology http://ontobox.org/. In particular, the datatype of disk-map strings has the (full) name http://ontobox.org/#dmapString, which means that it belongs to the ontology http://ontobox.org/, and its local name is dmapString.
Below, by default, a 'name' means a full name, if the opposite is not explicitly stated.
2.2. Naming
The recommendations for naming in OntoBox are close to Java's:
- class names start with uppercase letters (as singular nouns): Person, Book;
- property names start with lowercase letters (a singular/plural noun): authors, title, name
- object names start with uppercase letters, or are surrogates (see below): Tom, Book0135
- data type names can start with both uppercase and lowercase letters (a singular noun): string, long
Everywhere in this document, by default, a 'name' means a full name, if the opposite is not explicitly stated.
2.3. Multiple Inheritance
OntoBox supports the multiple inheritance of classes. Its active use is highly recommended.
The collisions of property names over inheritance (when synonymic but different properties are collected within the same class via inheritance) are impossible due to the uniqueness of the property names (see 2.5).
2.4. Placing Objects to Several Classes
An object can be directly declared as belonging to several classes. This saves us from introducing intermediate classes for this form of the multiple inheritance. It is useful when the number of heirs combinations is large.
2.5. Property Uniqueness
A significant distinction from Java is that a property with the same name can belong to no more than one class. That is, whereas in Java a property title can be defined within different classes, in OntoBox the property http://example.com/#title can be defined for a single class only. This solves the collision problem for names, when different properties are gathered in a class or an object from different branches of multiple inheritance.
What to do if we want to use the property names with similar meaning in different classes/hierarchies?
- We can use compound local names. For instance, the pattern "class-property" can be used here. E.g. the title property for books can be bookTitle, and that for albums can be albumTitle.
- We can use the same local names but in different ontologies. For instance, http://book.example.com/#title and http://album.example.com/#title. The full names here are different, so the properties are also different from the system's point of view.
- If entities are similar not only by name, but also by their meaning, then we can define a separate class containing only this property. And then multiple inheritance is used to insert this property to all necessary classes. For instance, the class Titled defines the only property title, and the classes Book and Album are both declared as the heirs of Titled (multiple inheritance in action).
2.6. Multiple Property Values and Cardinality
In Java's classes fields can have only one value. To keep several values, collections are used. Compare: Person author and List<Person> authors. In OntoBox a property can be assigned several values (including zero values). Therefore the property author/authors will be used in the same way in both cases: as an o-property author (or authors) ranged Person.
The values of an object's property are kept in the determined order, and can be duplicated. From this point of view, property values resemble the Java's List (an indexed collection allowing duplications).
The number of property's values is its cardinality. It can be (optionally) upper-bounded (by determining maximum cardinality). By default, maximum cardinality is 'potential' infinity (restricted in practice by the type int and computer resources). But we can bound it, for instance, by 1 (making the property the analogue of Java's Person author).
The cardinality bound checks are triggered by the attempts to add a new value: if the number of values is equal to the maximum cardinality, then adding a new value throws an exception.
2.7. NULL values
As opposed to Java (and databases), OntoBox does not have a NULL value. Based on the multiplicity of property values (see 2.6), the analogue of NULL is the absence of values (zero values). E.g. Person author in Java can be equal to either null, or a certain object of Person. In OntoBox, it corresponds to zero or one value, respectively.
2.8. Object Names
In Java we do not operate with object names, and this differs from OntoBox, where objects must always be named.
The aim of object's names is to uniquely identify objects. In some cases it has sense to assign an object a mnemonic name, e.g. if it is explicitly used as a property value for many objects. But in most cases it is sufficient to entrust OntoBox with the duty of object names generation. It has for this special tools on both the lower level of the API, and the level of Libretto (surrogate names).
2.9. Data Types (T-property Range)
In general (and by default), the datatype values (for t-properties) are handled as strings.
To make data handling more efficient and data access more simple, we can specify datatypes by determining t-property ranges. There are several predefined datatypes in OntoBox, which are based on XML Schema:
- Box.STRING_TYPE (http://www.w3.org/2001/XMLSchema#string) is a string
- Box.INT_TYPE (http://www.w3.org/2001/XMLSchema#int) is an integer (signed, 32 bits)
- Box.LONG_TYPE (http://www.w3.org/2001/XMLSchema#long)is an integer (signed, 64 bits)
- Box.BOOLEAN_TYPE (http://www.w3.org/2001/XMLSchema#boolean)is a boolean (true/false)
- Box.FLOAT_TYPE (http://www.w3.org/2001/XMLSchema#float) is a float
- Box.DATE_TIME_TYPE (http://www.w3.org/2001/XMLSchema#dateTime) is a date/time
If a t-property range is not determined (is equal to null), then it is considered as Box.STRING_TYPE.
All data can be stored as strings, but the explicit range specification provides
- the verification of added data (e.g. if the range is determined as Box.INT_TYPE, then a boolean value is not added, and an exception occurs);
- more efficient storing (data consumes less space in memory and on disk).
2.10. D-maps (Disk-Maps)
For storing large amounts of data used in a passive way disk-map properties (d-maps) can be employed. As opposite to ordinary properties, external storage devices like disks are actively used for storing dmap values. This provides significant memory saving, while keeping large amounts of data. Logically, datamap properties are not distinguishable from ordinary t-properties: both allow long-term data storing, and have similar data access methods. The only differences are in the access speed and storage space.
Only t-properties can be d-maps. For this, it is necessary to select one of the following types as the range of the t-property:
- Box.DMAP_STRING_TYPE (http://ontobox.org/#dmapString). Such properties can keep long strings/texts.
- Box.DMAP_BINARY_TYPE (http://ontobox.org/#dmapBinary). Such properties can keep binary data. It is possible to process this binary data as strings in the Base64 format.
Note that ordinary databases have similar types. The counterpart of Box.DMAP_STRING_TYPE is CLOB (Character Large Object), and the counterpart of Box.DMAP_BINARY_TYPE is BLOB (Binary Large Object).
2.11. Inverse properties
OntoBox has a mechanism (indexation) providing the fast retrieval of all objects, a property's value of which equals a certain value. This mechanism supports an easy implementation of property inversions.
For instance, if a tree structure is constructed, it is often necessary to 'walk' up and down along the tree branches. In Java we have to introduce for this two fields at each node: parent (which points to the parent node) and children (which contains the list of pointers to the child nodes). The complication of such an implementation is in necessity to synchronize these fields against each other.
OntoBox offers another option: to define only one property and to use instead of the other the inversion of the first property. E.g. if the order of nodes is important, then we define the property children, and use the inversion of children for obtaining the node's parents (that is, those nodes the property children of which contains the current node). If the order of nodes is not important, we can define parent and use instead of children the inversion of parent.
The efficiency of inverted properties handling is comparable with that of direct properties (in the basic OntoBox Storage implementation).
The difference of inverted properties from ordinary (direct) ones is that the value order in inverted properties is not defined and the duplications are impossible (that is, the values of an inverted property form a set).
2.12. Annotations
All OntoBox entities can be annotated, that is linked to a set of key/value pairs. Annotations are useful for adding auxiliary information about entities.
Both keys and values must be strings. Keys must have the syntax of an URI (like the full names of entities).
2.13. Temporary Objects
The system ontology http://ontobox.org/ of OntoBox contains the predefined class Box.TEMP_CLASS (http://ontobox.org/#Temporary), such that if an object belongs to this class (directly or via inheritance) it can be deleted automatically, if it is NOT the value of a property.
At present the removal of such objects is secured only on closing OntoBox. In other situations the removal is not guaranteed.
In addition, if a temporary object is the value of its own property (forms a value loop) then its behaviour is not regulated (it can be deleted, and can be not). On the other hand, it is guaranteed that if a temporary object is the value of an o-property of another object it will not be deleted.
2.14. Map-objects
In OntoBox it is possible to create objects with a simplified mechanism of property creation (see http://ontobox.org/libretto/libretto-en.html#21). Such objects are called map-objects. They are used basically for prompt information gathering (grouping) in Libretto expressions (that is, at the higher access level). At the intermediate access level the usage of map-objects is less important, but possible via the corresponding helper. At the lower level map-objects are not distinguishable from ordinary objects.
Technically, map-objects belong to the class MapHelper.MAP_CLASS (http://ontobox.org/map#Map) and can contain both ordinary properties and special properties (defined in the ontology http://ontobox.org/map). Special properties are ordinary properties but have a special domain (in the same ontology http://ontobox.org/map), and a special short name, which starts with either MapHelper.OPROP_PREFIX, or MapHelper.TPROP_PREFIX.
3. OntoBox Storage
3.1. A Basic OntoBox Implementation
OntoBox is an abstract programming interface. For real work we need to implement it. A basic implementation of OntoBox, OntoBox Storage, is included in the download package.
OntoBox Storage provides long-term data storing. At the same time all handled data is kept in the memory. This puts stronger demand on the memory capacity, but significantly improves the efficiency. To save the memory, disk-map properties can be employed (see 2.10), but they are significantly slower than ordinary properties.
OntoBox Storage supports transactions (rollbacks are also possible) and provides the safe level of isolation of transactions from each other.
Data handling in OntoBox Storage is possible only in the exclusive usage mode (it is impossible to open an OntoBox Storage data folder from different processes), see 3.2 for details on the storing file structure.
In what follows we consider OntoBox Storage as the basic implementation of OntoBox.
3.2. File Structure
A folder is indicated as a data root for OntoBox Storage. While working, the following files are created in this folder:
- dump.data contains stable data
- lock is a lock file (for prohibiting simultaneous access)
- trace.data is a data change journal (during the work)
After closing OntoBox Storage, only the file dump.data must stay in the root folder. If the files lock and/or trace.data are also found, then the last session have finished incorrectly. The system is ready for such situations and uses all possibilities to restore data automatically taking the information from the data change journal up to the transaction. But there is a probability that (in case of uncompleted transactions) some data will be lost (depending on the concrete OS and the Java implementation).
Besides these files the root folder contains a sub-folder ofiledmap, in which the values of disk-map properties are saved. This folder can exist even in cases when disk-map properties are not used.
4. How to Use OntoBox: a General Scheme
4.1. Packages
The basic packages are:
- org.ontobox.box contains the basic interfaces of OntoBox
- org.ontobox.base contains auxiliary classes for those who implement OntoBox API
- org.ontobox.box.exception contains exceptions
- org.ontobox.box.event determines events for changes listening
- org.ontobox.box.helper contains the helpers for the intermediate access level
- org.ontobox.box.query contains classes for higher level access (Libretto)
- org.ontobox.storage is the implementation of OntoBox Storage
- org.ontobox.exchange is data exchange (export/import)
4.2. JAR Files
Basic
- ontobox-storage.jar contains the basic OntoBox and OntoBox Storage interfaces and classes
- ontobox-libretto.jar contains the interpreter of Libretto
- teacode-common.jar is necessary for OntoBox functioning
Efficiency
- pcj.jar is the library for primitive collection management. OntoBox can work without this library, but significantly slower.
Export/Import
- jdom10.jar is necessary in case of export to a dialect of OWL/X (its usage is not recommended)
- owlapi-bin.jar is necessary in case of export through OWLAPI (only for export to external systems)
- htmlcleaner2_1.jar is necessary in case of import from HTML
4.3. Basic Interfaces
- Box describes general work with OntoBox.
- BoxWorker describes data retrieval and transaction support in OntoBox
- BoxWriter describes data creation, modification and removal in OntoBox
4.4. Box Opening/Closing
To open an OntoBox, it is sufficient to create an object of a class implementing the interface Box. In particular, in OntoBox Storage an object of StorageBox is created. So the code can be like that:
Box box = new StorageBox();
The argument dir of the constructor StorageBox(File dir) is a folder, in which the basic files of the current OntoBox Storage base are to be created (see 3.2 for the file structure), if they do not exist.
File dir = new File("fb2-lib");
Box box = new StorageBox(dir);
Data is saved in the folder between the sessions. Reopening the same folder through a new StorageBox provides the access to the stored data.
If the constructor StorageBox() without arguments is used, OntoBox Storage works in the 'memory-only' mode.
Box box = new StorageBox();
This is the mode, in which the ontobase is created in the memory at the moment, when the StorageBox object is generated, and is removed from the memory on its closing. Disk-map property values are also kept in the memory. This mode is useful for debugging or simple handling a small amount of data imported, for instance, from an MVX file or similar.
The normal usage of OntoBox Storage is in the file mode (in which the folder is transferred to the constructor as its argument).
It is impossible to open StorageBox several times simultaneously with the same external data (StorageBox(File dir)), even within the same JVM. The next attempt to open it causes an exception.
A Box must be closed, when the work is completed (at least, on application exit). Otherwise the application has to be enforced to exit (some non-deamon threads remain). If after a seeming application exit, the application continues to work, there is a high probability that a Box is not closed. The method close() is used for it:
Box box = ... box.close();
When a Box is used in a single block/section of the code, it is recommended to open and close it in accordance with the following pattern:
Box box = new StorageBox(...);
try
{
... // work with the box
}
finally
{
box.close();
}
4.5. BoxWorker and BoxWriter
BoxWorker is the interface, which provides transaction management and data retrieval methods.
BoxWriter is the interface connected to BoxWorker, which provides lower-level methods for data creation, modification and deletion. BoxWriter is accessible from BoxWorker via the method write():
BoxWorker worker = ...; BoxWriter writer = worker.write();
The closing of a BoxWorker is related to transactions, see 4.9.
4.6. Entity Identifiers
In the methods of BoxWorker/BoxWriter, integers (int) are used as entity identifiers. This provides the better efficiency of data handling. But it must be taken into account that integers are associated with entities only within each Box instance. When the Box is closed, these identifiers are not applicable, because the links between numbers and entities may be broken.
For long-term entity identification (when the Boxis closed), the full names of entities must be used.
4.7. Thread Safety
A Box instance is thread-safe. Its methods can be accessed from different threads (including concurrent threads).
BoxWorker and BoxWriter ARE NOT thread-safe. Their usage is allowed only in a single thread. Concurrent usage is not allowed (can bring very strange and hardly located errors).
4.8. Exceptions and Errors
AlreadyExistsException an attempt to create an existing entity, or an entity with an occupied name.
NotFoundException an attempt to access a non-existing entity (e.g., a name not found)
IllegalNameException an incorrect name
DomainException an error connected to a property domain type violation (e.g. if a property does not belong to an indicated class or object)
RangeException an error connected to a property range type violation (e.g. when a value of a property does not correspond to an indicated range)
UnsupportedOperationException an operation is not supported
IllegalArgumentException and IllegalStateException are almost similar (and hardly distinguishable), but the first exception is rather linked to incorrect parameters in method invokings, whereas the second one corresponds to parameter misuse (that is, the parameters are correct, but the system state does not allow their use).
DeleteException is similar to IllegalStateException but occurs on deletion (when, for instance, existing ontological links do not allow an entity to be deleted)
Other errors are handled by the ordinary RuntimeException.
4.9. Transactions
All actions are performed within transactions. Transactions provide the isolation and atomicity of an action sequence. Each transaction sees only its own changes until the commit, after which the changed data is available to all. An unsuccessful transaction can be rolled back, for instance, in case of an exception.
There are two types of transactions: normal and read-only. Their difference is that the attempts to change data within read-only transactions are interrupted with errors (exceptions). On the other hand, read-only transactions are more light-weight and efficient for concurrent handling in OntoBox Storage.
From the code viewpoint, transactions are related to a BoxWorker. To create a transaction, the methods Box.work() (for a normal transaction) or Box.workRO() (for a read-only transaction) should be invoked.
Having been created, a BoxWorker is immediately available for data reading (directly through the BoxWorker methods) or for data changing (through a BoxWriter available via the method write() of the interface BoxWorker).
When the work is completed commit() can be invoked in order to make data changes permanent. The commit can be applied also to read-only transactions (actually it does nothing in this case, but do not cause errors).
Data changes can be forceably cancelled by rollback().
When a BoxWorker is closed by close() the rollback is performed for uncommitted changes.
The following general pattern is recommended:
Box box = ...;
BoxWorker worker = box.work(); // or box.workRO() for read-only
try
{
...;
worker.commit(); // make changes permanent
}
finally
{
worker.close(); // performs rollback, if there was no commit, e.g.,
// in case of an exception, and closes the transaction
}
5. Low Level Access
5.1. Entity Types
For the identification of entity types, the enumeration enum Entity is defined, which contains the values:
- ONTOLOGY for an ontology
- TYPE for a datatype
- ONTCLASS for a class
- OPROPERTY for an o-property
- TPROPERTY for a t-property
- ONTOBJECT for an object
The metods Entity BoxWorker.entity(int id) and Entity BoxWorker.entity(String fullName) verify the entity's type by its identifier and full name, respectively. If the entity does not exist, null is returned.
BoxWorker worker = ...;
int id = ...;
Entity entity = worker.entity(id);
if (entity == Entity.ONTCLASS) // check if it is a class
{
...
}
5.2. Names and Identifiers
The BoxWorker's methods Integer id(String fullName), int resolve(String fullName) or int resolve(String fullName, Entity entity) get the identifiers (see 4.6) by full names.
The methods differ by their response on entity non-existence. In this case the method id returns null. The resolve methods throw the exception NotFoundException.
Besides, the method resolve with two arguments checks the entity's type. E.g. if we want to get a class by its full name:
BoxWorker worker = ...; String fn = "http://example.com/#Class"; int id = worker.resolve(fn, Entity.ONTCLASS);
If an entity searched by name does not correspond to the indicated type, an exception is thrown.
For better error diagnosis, we recommend to use the methods resolve, if possible.
The method String name(int id) of BoxWorker gets the name by the identifier. If the identifier does not exist, null is returned.
String BoxWorker.name(int ont, String local) gets the full name by the ontology and the local name. The first argument is the ontology's identifier. The second is the local name. This method does not need the existing entity with this name in this ontology. So it can be used, for instance, for the creation of full names before the generation of entities.
int BoxWorker.ontology(int) gets the entity's ontology by its identifier (can throw the exception, in case of non-existence).
int BoxWorker.local(int) gets the entity's local name by its identifier (can throw the similar exception).
The existence of an entity can be checked by its full name as follows:
1. If the entity's identifier is needed, the method resolve can be used. If the entity does not exist the exception is thrown.
2. To check the existence of an entity with a certain name (e.g. to check if a name is occupied) we can employ either id:
boolean exists = worker.id(name) != null;
or entity:
boolean exists = worker.entity(name) != null;
3. If it is necessary to check not only existence but also the entity's type, then we use entity:
boolean objectExists = worker.entity(name) == Entity.ONTOBJECT;
Surrogate names are also can be used. The method String newName(int) of the interface BoxWriter creates them. As input this method gets the ontology's identifier, in which the surrogate name is created, and returns a new full name. If a generated surrogate name becomes useless, nothing has to be done, because the name generator does not change data in the OntoBox base.
int ont = worker.resolve("http://example.com/");
String name = worker.write().newName(ont);
5.3. Direct/non-Direct Methods
The names of some methods contain the word 'Direct'. This means that the method does not take into account the inheritance hierarchy, and works with direct (explicitly defined) links. The methods without 'Direct' in their names take into aÑ_count the inheritance (if it has impact on the result).
This is the list of the 'Direct' methods:
objectsDirect returns the list of objects without inheritance
classesDirect returns the list of direct (super)classes without inheritance
subclassesDirect returns the list of subclusses without inheritance
tpropsDirect returns the list of t-properties without inheritance
opropsDirect returns the list of o-properties without inheritance
For instance, the method classes returns all classes to which the object belongs through inheritance hierarchy, whereas the method classesDirect returns only those classes, in which the object is included explicitly.
Normally there is no need to use the 'Direct' methods in programs, if there is no sophisticated work with the data structure.
5.4. General Data Retrieval
The methods of BoxWorker can work with the entities of various types:
objects returns the list of objects belonging to the input ontology or the input class
objectsDirect returns the list of objects directly belonging to the input class (not through inheritance)
classes returns the list of classes defined in the input ontology, or superclasses of the input class, or the classes to which the input object belongs
classesDirect returns the list of direct classes defined in the input ontology (the direct classes of an ontology are the classes, which are defined in the ontology and do not have superclasses), the list of direct superclasses of the input class, or the list of classses to which the input object belongs (not through inheritance)
subclasses returns the list of subclasses of the input class
subclassesDirect returns the list of direct subclasses of the input class (not through inheritance)
types returns the list of types declared in the input ontology
tprops returns the list of t-properties defined in the input ontology, class or object
tpropsDirect returns the list of t-properties directly defined in the input class (not through inheritance)
oprops returns the list of all o-properties defined in the input ontology, class, or object
opropsDirect returns the list of o-properties directly defined in the input class (not through inheritance)
The lists above are the arrays of entity identifiers (int []).
The methods domain and range determine the domain and the range of the input property, respectively. They work with both t-properties and o-properties and are distinguished by the entity type returned by range. If the domain or the range is not defined, then the corresponding method returns null.
5.5. Property's Values Retrieval
There are several methods aimed at getting values of t-properties. Each method is implemented in two versions: a version for getting all values of an object's t-property (its arguments are the object's identifier and the identifier of the t-property), and a version for getting all t-property's values of all objects (its only argument is the t-property identifier).
Besides, these methods return different data types, depending on their names:
strings and strings return the values as strings String [] (applicable to each t-property's range)
ints and ints return the values as integers int[]
longs and longs return values as long integers long[]
dateTimes and dateTimes return values as dates Date[]
booleans и booleans return values as boolean values boolean[]
The behaviour of these methods (except strings) depends on the range of the t-property.
Two methods owners are established for work with the inversions of t-properties. The first owners gets as input a string and returns all objects in which at least one value of at least one t-property coincides with the input string. The second owners acts on a specified t-property (that is, this t-property is inverted). As input it gets the identifier of this t-property and the value string, which must coincide.
To get the values of an o-property the method objects is defined with two arguments: the object's identifier, and the identifier of the o-property.
The inversion of o-properties is implemented by the two methods owners. The first owners takes as input an object's identifier and returns all objects, at least one value of at least one o-property of which coincides with the input object. The second owners works with a specified o-property (that is, this o-property is inverted). It takes as input the identifier of this o-property and the identifier of the checked object.
5.6. Structure Creation/Editing
The creation and editing of the data structure is performed through the interface BoxWriter.
To create entities, the methods new???? are introduced. They take as input the full name of the entity to be created:
newOntology
newClass
newOProperty
newTProperty
newType
If the name is occupied, an exception is thrown.
The methods setDomain, setRange set the domain and range of a property. Both the domain and range can be null. Determined domains and ranges can not be edited/changed. The method newOProperty generates properties with undefined ranges and domains (null).
For changing class hierarchies the methods addSubclass and removeSubclass are used. These methods throw an exception, if the modified segment of the hierarchy is used by objects or properties.
The method rename is used for entity renaming. It works with any entity (including ontologies). As a new name a full name of the entity is used (or URI in case of an ontology). Only the local names of entities can be changed, moving to another ontology by this method is impossible.
The method delete is used for deleting entities. This method brings errors, if the deleted entity is connected somehow with others (e.g. the object is used as a value of some o-property, or an ontology contains entities, etc.)
5.7. Objects Creation/Editing
Objects are generated in two steps: first, an object is created, second it is linked to some class (or classes). Then we can set the values of the object's properties.
An object generation is performed by the method newObject.
The method addObjectClass links objects to classes. For removing an object from a class the method removeObjectClass is used. An object can belong to several classes.
The methods add???? are used for adding values to properties. These methods are implemented in two versions: object-property-value (the value is added at the end of the value list) and object-property-index-value (the value is added at the indicated position of the value list). These methods differ by types: addString, addInt etc.
The method removeValue, deletes values. As arguments it takes an object, a property and the position of the deleted value.
Several values can be deleted simultaneously. The method removeValues can delete all values of a certain property (the arguments are an object and the property). Also the method removeValues can delete all values of all properties of an object (the argument is an object).
The method delete is used for deleting objects. An object can not be deleted by this method, if it is used as a value of some o-property.
5.8. Annotations
The method annames of the interface BoxWorker returns the list of annotation names defined for a certain entity (see 2.12).
To get the values of the entity annotations, the method anno of the interface BoxWorker is used. If an annotation is not defined, the method returns null.
The method annotate of the interface BoxWriter is used for adding annotations. It is also used for their deletion. In this case its value must be null.
5.9. Cardinality
On the lower level OntoBox does not have special methods for handling cardinality. Cardinality data is saved in property annotations. Maximum cardinality is saved in the annotation named Box.MAX_CARD (http://ontobox.org/#maxcard), and minimum is saved in Box.MIN_CARD (http://ontobox.org/#mincard).
6. Intermediate Access Level (Helpers)
6.1. Naming
The class NameHelper defines the methods working with entity names, and is useful for user interface development.
The method String getShortOntologyName(String uri) returns some 'mnemonic' ontology name aimed at the presentation to the user. This name can be used only for this purpose, it is impossible to identify the ontology by this name.
The method boolean isSystemOntology(String uri) checks if the ontology name is a system name. It is not recommended to show the ontologies with system names to the user without unavoidable necessity.
The method generatePrefixes is useful for applications including the Libretto console for manual inputting of Libretto queries. This method automatically generates name prefixes for existing ontologies.
6.2. Helpers for Structure Development
The class WHelper defines the method ClassAddition ontclass(final BoxWorker worker, final String className). This method creates a class with the indicated full name. Its peculiarity is that the returned object of the class ClassAddition allows method chaining for the class development: its positioning within the hierarchy (the method ClassAddition subclassof(final String name)), properties introduction (the methods tprop, tprop, oprop, tpropDMapBinary, tpropDMapString).
6.3. Helpers for Object Generation
The class WHelper defines the methods for object creation similar to the method ontclass above: ObjectAddition object(final BoxWorker worker, final int ont, final String className) and ObjectAddition object(final BoxWorker worker, final String name, final String className). The only difference is that it returns an object of the class ObjectAddition, which allows method chaining for object data handling.
6.4. Object Data Helpers
The class WHelper provides a technique for object data editing, similar to that for object generation. For this the method ObjectAddition object(final BoxWorker worker, final int object) is used. This method returns an object of the class ObjectAddition, which can be used for the chaining of object editing methods.
For data reading the class RHelper is introduced. It provides obtaining single values (e.g. via the method String stringValue(BoxWorker worker, int object, int tprop)), verification of whether an object belongs to a class (the method boolean isInstanceOf(BoxWorker worker, int obj, int cl)) etc.
6.5. Binary Diskmap Properties
The BinaryHelper defines helper methods working with binary data:
- base64: byte[] fromBase64(String) and String toBase64(byte[])
- hex (hexadecimal text representation): byte[] fromHex(String) and String toHex(byte[])
6.6. Deletion
The class DeleteHelper defines the method deleteAllData for the entire deletion of all data (all entities of all ontologies and ontologies themselves, except the system ontologies).
The entire deletion is performed within transactions, so it can be rolled back.
6.7. Sorting
The class Sorter provides quite efficient methods for object sorting by a specified parameter.
The methods sort and sortIgnoreCase perform simple sorting by some t-property. Enum SortMode sets the direction of sorting.
For more advanced sorting the method sortTyped is used. As the last parameter it gets an object of the interface Sorter.ValueGenerator. This object must return a certain value by which sorting is performed. For instance, for simple sorting by the string value of a t-property we can have:
BoxWorker worker = ...;
int tprop = ...;new Sorter.ValueGenerator()
{
public Comparable value(int objectId)
{
return RHelper.stringValue(worker, objectId, tprop);
}
}
6.8. Search
The class Searcher provides a simple search method with gradual criteria refinement.
To start the search by the first criterion, one of the following methods can be invoked:
- SearchContext findByString(BoxWorker worker, String prop, String value) is the search for string equality (for a t-property)
- SearchContext findByInteger(BoxWorker worker, String prop, int value) is the search for integer equality (for a t-property)
- SearchContext findByLong(BoxWorker worker, String prop, long value) is the search for long integer equality (for a t-property)
- SearchContext findByObject(BoxWorker worker, String prop, int value) is the search for object equality (for an o-property)
- SearchContext findBySubstring(BoxWorker worker, String value) is the search by a substring (for a t-property)
Having got the result of the first criterion (an object of the class SearchContext), we can either get the resulting list of objects (by the method int[] all()), or only the first object (Integer single()), or continue the search by chaining new criteria with the methods:
- SearchContext andBySubstring(String value)
- SearchContext orBySubstring(String value)
- SearchContext andByString(String prop, String value)
- SearchContext andByInteger(String prop, int value)
- SearchContext andByObject(String prop, int value)
6.9. Map-objects
The class MapHelper provides methods working with map-objects (see 2.14).
A map-object can be generated from scratch by the method int create(BoxWorker worker) or int create(BoxWorker worker, int ontology), or an existing object can be declared as map by the method int makeMap(BoxWorker worker, int object).
The method isMap(BoxWorker worker, int object) verifies if the object is a map-object.
The method int tkey(BoxWorker worker, int map, String key) is used for obtaining a t-property corresponding to the indicated key of the indicated object. If the key exists, the corresponding t-property will be returned. If this key does not exist, the corresponding t-property will be created.
The method int okey(BoxWorker worker, int map, String key) is similar to tkey, but is applied to o-properties.
The simultaneous creation for a map-object of synonymic keys corresponding to a t-property and an o-property, respectively, is forbidden. For different map-objects it is allowed.
The values are assigned to keys by the ordinary BoxWorker methods applied to the properties corresponding to these keys.
The methods boolean isTkey(BoxWorker worker, int map, String key) and boolean isOkey(BoxWorker worker, int map, String key) verify if there exists a property corresponding to the indicated key in the indicated map-object.
The methods String[] getTKeys(BoxWorker worker, int map) and String[] getOKeys(BoxWorker worker, int map) return the existing keys of this map-object.
The method String name(BoxWorker worker, int prop) is used for getting the key of the indicated property. If the property does not correspond to any key, null is returned.
The method void removeKey(BoxWorker worker, int map, String key) deletes the key and all the linked values of the object.
6.10. Temporary Objects
The class TempHelper defines the methods working with temporary objects (see 2.13).
The method boolean makeTemporary(BoxWorker worker, int object) declares the object as temporary. This method is allowed for already temporary objects. The method returns true if the object was not temporary before the method's application and false otherwise.
The method boolean isTemporary(BoxWorker worker, int object) verifies if the object is temporary.
6.11. Values Collection (Values)
Sometimes we need to manipulate with the collections of values, which can be represented by the interface Values. Values provides handling collections as data of various types: either as a single value (the first value or null if the collection is empty), or as a collection (List).
| Value | Value type | Key word | The method for collection | The method for a single value |
| Object (VMap) | VMap | Map | maps | map |
| Object (identifier) | Integer | Object | objects | object |
| String | String | String | strings | string |
| Long | Long | Long | longs | longV |
| Integer | Integer | Integer | integers | integer |
| Date/Time | Date | DateTime | dateTimes | dateTime |
| Boolean | Boolean | Boolean | booleans | booleanV |
The method isObject verifies whether it is possible to get values as an ontology object (Map or object).
The methods string and strings are determined for all types of collections.
The methods of Values are allowed only if the BoxWorker, within which this Values was obtained, is not closed yet. Otherwise, the behaviour of these methods is undetermined.
6.12. Objects as Java Maps
For better convenience, an access to an object (and its properties) as an ordinary Java Map is provided. For this, the class VMap is introduced. Its constructor obtains a BoxWorker, and an object identifier, or its full name. VMap implements Map<String, Values>, and thus can be used as an ordinary Map. The full names of properties and the field names of Map-objects play the role of keys. The result is a collection of values of the corresponding property represented as Values.
For the sake of simplicity, VMap provides methods which call corresponding methods of Values: m????(key) of VMap if equivalent to get(key).????(), for instance:
VMap map = ...; String key = ...;String v1 = map.get(key).string(); String v2 = map.mString(key);int s1 = map.get(key).size(); int s2 = map.mSize(key);
The methods of VMap are allowed only if the BoxWorker, which has generated this VMap, is still not closed. If it is closed, the behaviour of these methods is undetermined.
6.13. Working with XML
The class XMLHelper includes the methods working with XML documents represented in the form of a special ontology and a certain structure of classes and properties. These basic entities are defined in the ontology XMLHelper.XML_URI. But objects representing XML documents are defined in other ontologies (in those indicated by the user).
The import of an XML document is provided by the two methods: importXML, importXML, which differ by the source, from which the XML document must be imported. Both return an object, representing the Document XML.
For the XML export the three methods are provided: exportXML, exportXML, exportXML.
Besides, the methods for HTML import are also provided with further conversion into XHTML representation saved in an indicated XML ontology. The following methods are provided: importHTML, importHTML, importHTML. These methods are implemented with the usage of the library htmlcleaner (see 4.2).
7. Higher Access Level (a Query Language)
7.1. Libretto
Libretto is a query language, which supports retrieval, creation and editing data in OntoBox. It is described in a separate document.
7.2. Context
QContext is a global context of Libretto. It contains the definitions of namespaces and functions. An object of this class can exist independently and without Box/BoxWorker/BoxWriter. QContext is thread-safe, and a single QContext can be used in all parts of the application. In practice, it has sense to create a QContext and a Box together, with the immediate introduction of necessary namespaces.
QContext ctx = new QContext();
ctx.setDefaultPrefix("http://example.com/");
ctx.setPrefix("r", "http://example.com/r/");
Note that the variables of the query language are not saved in the global context (by default), so in concurrent queries different variables of the same name can be used (assigned).
On the other hand, the functions defined in Libretto are available in all executed queries. In order to avoid errors, it is not recommended to define new functions in concurrently executed queries.
In the QContext the method unshortenName is defined, which serves for unfolding prefixed names (that is, short names with prefixes in the Libretto style) in accordance with defined namespaces.
7.3. Dynamic Queries
Queries in Libretto can be both static and dynamic. Static queries are Libretto expressions represented by strings (String). Dynamic queries resemble PreparedStatement JDBC. They are defined by Libretto expressions, in which the question marks '?' are inserted instead of concrete values (see here for more details). These '?' are to be substituted by concrete values dynamically.
The class Query is used for dynamic queries representation. The method createQuery of the class QContext creates a Query. For parameter substitution the methods of Query starting with the word 'set' are used, e.g. setString. Their first argument is the number of the occurrence of '?' and the second is a substituting value. The enumeration of the occurrences of '?' is from left to right.
QContext ctx = ...;
Query q = ctx.createQuery("Person[name=? and title=?];");
List<string> names = ...;
List<string> titles = ...;
int size = ...;
for (int t = 0; t < size; t++)
{
q.setString(0, names[t]);
q.setString(1, titles[t]);
// invoke q
...
}
</string></string>
Symbols '?' can occur in those positions of queries, where occurrences of variables are allowed.
Note that there are two methods substituting objects instead of '?': setObject и setObj. Both take strings as parameters, which are interpreted as object names. But in the first method they are interpreted as full names, whereas in the second one they are names in the Libretto style (local names with an optional prefix separated by ':').
7.4. Query Execution and Getting Results
For query execution, BoxWorker contains methods, the names of which start with the symbol 'q'. All these methods take as the first argument a global context QContext, within which the query is to be executed. The second argument is either String for static queries, or Query for dynamic ones. The methods differ by the values they return (their types).
A special method q works with Libretto query sequences, and returns List<Values>. Values is the result of each Libretto query of the sequence (the intermediate access level). And List<Values> is the list of all results of all queries of this query sequence.
The method q (having two versions) is the only way to execute a Libretto query sequence and obtain the results for all queries this sequence contains. All the other methods q???? return only the result of the last query of the sequence.
The methods q???? are divided into two basic groups: the methods returning a value sequence, and the methods returning a single value. The methods of the second type return the first value of the resulting sequence, if it is not empty, or null, otherwise. The methods of the first type always return a sequence (List<...>). It is empty, if the execution fails to find data.
The table of the methods q????
| Value | Value type | Method's keyword | Sequence method | Single value method |
| Any via Values | Any via Values | Value | qValues | - |
| Object (identifier) | Integer | Object | qObjects | qObject |
| Object (VMap) | VMap | Map | qMaps | qMap |
| String | String | String | qStrings | qString |
| Date/Time | Date | DateTime | qDateTimes | qDateTime |
QContext ctx = ...;
Query q = ctx.createQuery("Person[name=? and title=?];");BoxWorker worker = ...;List<string> names = ...;
List<string> titles = ...;
int size = ...;
List<integer> results = new ArrayList<integer>();
for (int t = 0; t < size; t++)
{
q.setString(0, names[t]);
q.setString(1, titles[t]);
Integer object = worker.qObject(ctx, q);
results.add(object);
}
</integer></integer></string></string>
Note that although these methods are defined in BoxWorker, using them we can execute those Libretto queries, which change, create and delete data. If the result of a query is not important, it is easier to apply the method q.
8. Miscellanea
8.1. Data Export/Import
For data exchange the format MVX is used. This is a ZIP-archive with an XML file, in which ontological data is stored.
Technically, the export/import is performed by the class MVX (the package org.ontobox.exchange)
The export is performed by the method exportFile:
BoxWorker worker = ...;
File outFile = new File("out.mvx");
MVX.exportFile(outFile, worker);
The import is performed by the importFile:
BoxWorker worker = ....;
File inFile = new File("in.mvx");
MVX.importFile(inFile, worker);
Data deletion is not performed prior to the import, so the collisions with imported names are possible.
The prerequisite clean-up can be done, for instance, using DeleteHelper.deleteAllData. If to fulfil deletion and import within the same transaction, then the rollback to the initial state is possible (with un-deleting data), e.g. in case of errors.
8.2. Performance and Security
For maximum performance OntoBox Storage needs the library pcj.jar, which provides the fast implementations of the collection of primitives. OntoBox Storage can work without this library (using the standard collections of java.util), but the performance will be much lower, and the memory consumption is higher. By using logs, it is possible to check whether pcj.jar is used or not. If pcj.jar is used, then the log contains:
INFO: Using PCJ open map/set collections
If this library is not used, there is another message:
INFO: Using java.util collections
Besides, the (non-critical) messages like "WARNING: java.lang.NoClassDefFoundError" are possible, which indicate on this or that collection handling class.
The general strategy of work with OntoBox should be based on maximally small transactions. The creation of a transaction is a very fast action from the point of view of OntoBox Storage. The transaction support mechanism works most efficiently, when it has a big number of small transactions. Do not keep long-term uncompleted (by the commit or rollback) transactions, close them (using the method close) or perform commit/rollback. Then the performance and concurrent work will be most efficient.
Besides, the maximum possible usage of read-only transactions is welcomed. They are more efficient, and allow better concurrency support. Read-only transactions are more safe too: in case of attempts to modify data an exception occurs, and data stays untouched.
On the higher level, it is recommended to maximally use Libretto dynamic queries (that is, queries, in which parameters are specified by symbols '?'), with further parameter instantiating by the methods of the class Query. It is advisable to re-use the same Query and multiply execute it with different parameters. Generating queries 'manually' by string concatenation should be allowed only in cases of absolute necessity, because this style often brings errors and makes the code vulnerable. If this style has to be used, please do not forget of escaping strings.
8.3. OntoBox in Web Applications
We recommend to keep the object of a Box implementing class (e.g., StorageBox), in the context of the whole web application, for instance, as an attribute of ServletContext or in a functionally similar place of the framework. It is important to have a single Box for the whole application, which is available from the relevant code segments. And it is important to close the Box while quitting the application. But do not open/close a Box without real necessity, because this is a relatively heavy operation.
While handling a web query, it is recommended to create a new BoxWorker. This operation is very fast. Moreover, in some cases it has sense to open and close the chain of independent BoxWorkers while handling the same query. That is, a BoxWorker must include as few actions as possible, which must be performed in an atomic style. And do not forget to perform commit, and close each BoxWorker by close. The less time each BoxWorker works, the better the performance of the whole application.
Moreover, try to maximize the usage of read-only BoxWorkers (created by the method Box.workRO) in those segments, where data does not change. This significantly improves concurrent queries handling, which is important in web applications.
Do not keep/use the objects of classes Values and VMap, after quitting corresponding queries. The behaviour of these objects is determined only for an opened BoxWorker, and the long-term keeping of BoxWorkers is not recommended (it can hamper the performance).
8.4. Changes Tracking
For the tracking of data changes in OntoBox, the class BoxListenerManager is used. In it, the method addActionListener is implemented, which adds listeners. The BoxListenerManager can be obtained by the method getListenerManager().
A listener is an object implementing BoxEventListener. The method process is invoked when changes occur. Its argument is the list of events describing these changes.
The events are transferred asynchronously within one thread. At the same time, the events strictly correspond to the data changes order in OntoBox. In the basic implementation OntoBox Storage events are transferred after committing the transaction. The delays with event handling in a listener do not influence the performance of OntoBox Storage, but has an impact on delays with other listeners, and increase the memory consumption. Thus, it is recommended to make the listener code efficient enough.
If various listeners are added constantly, please do not to forget to close them (by the method removeActionListener), as soon as they are not needed.
8.5. Implementation of Your Own OntoBox
For the implementation of your own OntoBox, special abstract classes are defined in the package org.ontobox.box.base. In them, it is sufficient to implement abstract methods, and the base code automatically provides type checking.
For a correct implementation of OntoBox the support of identifiers (integers), which uniquely define all entities (see 4.6), is necessary.
A good and 'full-fledged' implementation of OntoBox must support changes listening (see 8.4).
9. An Example
9.1. An Example Scenario
The sequence of actions in an OntoBox depends on the aims and type of the developed application. As an example, the code is demonstrated for a standalone application, which solves a simple task comprising the following steps:
- The structure creation: ontologies, classes and properties. This step is necessary, if the structure has not been created yet.
- The creation of simple data (objects). Objects must be created, if they do not exist.
- Data retrieval.
The verification whether necessary structures and objects exist or not, in order to avoid double creation, can be performed by several methods, but here it is sufficient to verify the existence of the ontology (by the method entity of the class BoxWorker).
The creation step includes:
- The creation of the ontology http://person.example.com/ (the constant ONTOLOGY)
- The creation of the class http://person.example.com/#Person (the constant PERSON_CLASS)
- The creation of the t-property http://person.example.com/#name with http://person.example.com/#Person as the domain, and strings as the range, and with maximum cardinality bounded by 1. (the constant PERSON_NAME)
- The creation of the t-property http://person.example.com/#age, the domain of which is http://person.example.com/#Person, the range is an integer (32 bits), and the maximum cardinality is bounded by 1. (the constant PERSON_AGE)
Each step of the demonstration is implemented on all three access levels. It uses the variable BoxWorker worker.
9.2. Structure Creation
The lower level:
final BoxWriter writer = worker.write(); writer.newOntology(ONTOLOGY); final int person = writer.newClass(PERSON_CLASS); final int name = writer.newTProperty(PERSON_NAME); writer.setDomain(name, person); writer.setRange(name, worker.resolve(Box.STRING_TYPE)); writer.annotate(name, Box.MAX_CARD, "1"); final int age = writer.newTProperty(PERSON_AGE); writer.setDomain(age, person); writer.setRange(age, worker.resolve(Box.INT_TYPE)); writer.annotate(age, Box.MAX_CARD, "1");
The intermediate level (helpers):
final BoxWriter writer = worker.write();
writer.newOntology(ONTOLOGY);
WHelper.ontclass(worker, PERSON_CLASS)
.tprop(PERSON_NAME, Box.STRING_TYPE, 1)
.tprop(PERSON_AGE, Box.INT_TYPE, 1);
The higher level (the query language Libretto):
worker.q(new QContext(), "ontology . \"http://person.example.com/\";" +
"class Person { name[,1] { xsd:string }, age[,1] { xsd:int } };");
9.3. Data Creation
Suppose that we need to create objects in loop. names here is a String[], and ages is an int[] with the same number of arguments. New object names are surrogate.
The lower level:
final BoxWriter writer = worker.write();
final int ont = worker.id(ONTOLOGY);
final int person = worker.id(PERSON_CLASS);
final int name = worker.id(PERSON_NAME);
final int age = worker.id(PERSON_AGE);
final int size = names.length;
for (int t = 0; t < size; t++)
{
final int obj = writer.newObject(writer.newName(ont)); // surrogate name
writer.addObjectClass(obj, person);
writer.addString(obj, name, names[t]);
writer.addInt(obj, age, ages[t]);
}
The intermediate level (helpers):
final int ont = worker.id(ONTOLOGY);
final int size = names.length;
for (int t = 0; t < size; t++)
{
WHelper.object(worker, ont, PERSON_CLASS)
.addString(PERSON_NAME, names[t])
.addInt(PERSON_AGE, ages[t]);
}
The higher level (the query language Libretto):
QContext ctx = new QContext();
ctx.setDefaultPrefix(ONTOLOGY);
Query q = ctx.createQuery("Person &_ { name := ?, age := ? };");
final int size = names.length;
for (int t = 0; t < size; t++)
{
q.setString(0, names[t]);
q.setInt(1, ages[t]);
worker.q(ctx, q);
}
9.4. Data Retrieval
Here we solve the task of printing the object name and t-property values. The list of objects must be sorted by the values of a t-property converted to lower case (by PERSON_NAME).
The lower level:
private static class PersonName
{
public int person;
public String name;
private PersonName(int person, String name)
{
this.person = person;
this.name = name;
}
}
...
final int name = worker.resolve(PERSON_NAME, Entity.TPROPERTY);
final int age = worker.resolve(PERSON_AGE, Entity.TPROPERTY);
List<personname> persons = new ArrayList<personname>();
for (int obj : worker.objects(worker.resolve(PERSON_CLASS)))
{
persons.add(new PersonName(obj, worker.strings(obj, name)[0])); // may be exception
}
Collections.sort(persons, new Comparator<personname>()
{
public int compare(PersonName pn1, PersonName pn2)
{
return pn1.name.toLowerCase().compareTo(pn2.name.toLowerCase());
}
});
for (PersonName person : persons)
{
System.out.println("Object: "+worker.name(person.person));
System.out.println("Name: "+person.name);
System.out.println("Age: "+worker.ints(person.person, age)[0]); // may be exception
}
</personname></personname></personname>
The intermediate level (helpers), version 1:
int[] persons = worker.objects(worker.resolve(PERSON_CLASS));
persons = Sorter.sortIgnoreCase(worker, persons, SortMode.ASC,
worker.resolve(PERSON_NAME, Entity.TPROPERTY));
for (int person : persons)
{
System.out.println("Object: "+worker.name(person));
System.out.println("Name: "+ RHelper.stringValue(worker, person, PERSON_NAME));
System.out.println("Age: "+ RHelper.intValue(worker, person, PERSON_AGE));
}
The intermediate level (helpers), version 2:
int[] persons = worker.objects(worker.resolve(PERSON_CLASS));
persons = Sorter.sortIgnoreCase(worker, persons, SortMode.ASC,
worker.resolve(PERSON_NAME, Entity.TPROPERTY));
for (int person : persons)
{
final VMap v = new VMap(worker, person);
System.out.println("Object: "+v.name());
System.out.println("Name: "+ v.mString(PERSON_NAME));
System.out.println("Age: "+ v.mInteger(PERSON_AGE));
}
The higher level (Libretto):
QContext ctx = new QContext();
ctx.setDefaultPrefix(ONTOLOGY);
final List<vmap> persons = worker.qMaps(ctx, "Person<<name>>");
for (VMap v : persons)
{
System.out.println("Object: "+v.name());
System.out.println("Name: "+ v.mString(PERSON_NAME));
System.out.println("Age: "+ v.mInteger(PERSON_AGE));
}
</name></vmap>
9.5. The Complete Example Code
By editing the initial code, it is possible to apply the lower, intermediate and higher levels for the structure and data creation (createLow, createMid, createHigh in main). For inputting all three levels are used.
import org.ontobox.box.*;
import org.ontobox.box.helper.*;
import org.ontobox.box.query.*;
import org.ontobox.storage.StorageBox;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;public class Example
{
private static String ONTOLOGY = "http://person.example.com/";
private static String PERSON_CLASS = ONTOLOGY+"#Person";
private static String PERSON_NAME = ONTOLOGY+"#name";
private static String PERSON_AGE = ONTOLOGY+"#age";
private static String[] names = { "Basil", "Peter", "Nick", "abc"};
private static int[] ages = { 23, 20, 30, 99};
private static void createLow(BoxWorker worker)
{
createStructureLow(worker);
createDataLow(worker);
}
private static void createMid(BoxWorker worker)
{
createStructureMid(worker);
createDataMid(worker);
}
private static void createHigh(BoxWorker worker)
{
createStructureHigh(worker);
createDataHigh(worker);
}
private static void createStructureLow(BoxWorker worker)
{
final BoxWriter writer = worker.write();
writer.newOntology(ONTOLOGY);
final int person = writer.newClass(PERSON_CLASS);
final int name = writer.newTProperty(PERSON_NAME);
writer.setDomain(name, person);
writer.setRange(name, worker.resolve(Box.STRING_TYPE));
writer.annotate(name, Box.MAX_CARD, "1");
final int age = writer.newTProperty(PERSON_AGE);
writer.setDomain(age, person);
writer.setRange(age, worker.resolve(Box.INT_TYPE));
writer.annotate(age, Box.MAX_CARD, "1");
}
private static void createStructureMid(BoxWorker worker)
{
final BoxWriter writer = worker.write();
writer.newOntology(ONTOLOGY);
WHelper.ontclass(worker, PERSON_CLASS)
.tprop(PERSON_NAME, Box.STRING_TYPE, 1)
.tprop(PERSON_AGE, Box.INT_TYPE, 1);
}
private static void createStructureHigh(BoxWorker worker)
{
worker.q(new QContext(), "ontology . \"http://person.example.com/\";" +
"class Person { name[,1] { xsd:string }, age[,1] { xsd:int } };");
}
private static void createDataLow(BoxWorker worker)
{
final BoxWriter writer = worker.write();
final int ont = worker.id(ONTOLOGY);
final int person = worker.id(PERSON_CLASS);
final int name = worker.id(PERSON_NAME);
final int age = worker.id(PERSON_AGE);
final int size = names.length;
for (int t = 0; t < size; t++)
{
final int obj = writer.newObject(writer.newName(ont)); // surrogate name
writer.addObjectClass(obj, person);
writer.addString(obj, name, names[t]);
writer.addInt(obj, age, ages[t]);
}
}
private static void createDataMid(BoxWorker worker)
{
final int ont = worker.id(ONTOLOGY);
final int size = names.length;
for (int t = 0; t < size; t++)
{
WHelper.object(worker, ont, PERSON_CLASS)
.addString(PERSON_NAME, names[t])
.addInt(PERSON_AGE, ages[t]);
}
}
private static void createDataHigh(BoxWorker worker)
{
QContext ctx = new QContext();
ctx.setDefaultPrefix(ONTOLOGY);
Query q = ctx.createQuery("Person &_ { name := ?, age := ? };");
final int size = names.length;
for (int t = 0; t < size; t++)
{
q.setString(0, names[t]);
q.setInt(1, ages[t]);
worker.q(ctx, q);
}
}
private static class PersonName
{
public int person;
public String name;
private PersonName(int person, String name)
{
this.person = person;
this.name = name;
}
}
private static void printDataLow(BoxWorker worker)
{
final int name = worker.resolve(PERSON_NAME, Entity.TPROPERTY);
final int age = worker.resolve(PERSON_AGE, Entity.TPROPERTY);
List<personname> persons = new ArrayList<personname>();
for (int obj : worker.objects(worker.resolve(PERSON_CLASS)))
{
persons.add(new PersonName(obj, worker.strings(obj, name)[0])); // may be exception
}
Collections.sort(persons, new Comparator<personname>()
{
public int compare(PersonName pn1, PersonName pn2)
{
return pn1.name.toLowerCase().compareTo(pn2.name.toLowerCase());
}
});
System.out.println("Person list (Low)");
for (PersonName person : persons)
{
System.out.println("Object: "+worker.name(person.person));
System.out.println("Name: "+person.name);
System.out.println("Age: "+worker.ints(person.person, age)[0]); // may be exception
}
}
private static void printDataMid(BoxWorker worker)
{
int[] persons = worker.objects(worker.resolve(PERSON_CLASS));
persons = Sorter.sortIgnoreCase(worker, persons, SortMode.ASC, worker.resolve(PERSON_NAME, Entity.TPROPERTY));
System.out.println("Person list (Mid-1)");
for (int person : persons)
{
System.out.println("Object: "+worker.name(person));
System.out.println("Name: "+ RHelper.stringValue(worker, person, PERSON_NAME));
System.out.println("Age: "+ RHelper.intValue(worker, person, PERSON_AGE));
}
System.out.println("Person list (Mid-2)");
for (int person : persons)
{
final VMap v = new VMap(worker, person);
System.out.println("Object: "+v.name());
System.out.println("Name: "+ v.mString(PERSON_NAME));
System.out.println("Age: "+ v.mInteger(PERSON_AGE));
}
}
private static void printDataHigh(BoxWorker worker)
{
QContext ctx = new QContext();
ctx.setDefaultPrefix(ONTOLOGY);
System.out.println("Person list (High)");
final List<vmap> persons = worker.qMaps(ctx, "Person<<name>>");
for (VMap v : persons)
{
System.out.println("Object: "+v.name());
System.out.println("Name: "+ v.mString(PERSON_NAME));
System.out.println("Age: "+ v.mInteger(PERSON_AGE));
}
}
public static void main(String[] args)
{
final Box box = new StorageBox();
//final Box box = new StorageBox(new File("ontobase"));
try
{
BoxWorker worker = box.work();
try
{
if (worker.entity(ONTOLOGY) != Entity.ONTOLOGY)
{
createLow(worker);
//createMid(worker);
//createHigh(worker);
}
worker.commit();
}
finally
{
worker.close();
}
worker = box.workRO();
try
{
printDataLow(worker);
printDataMid(worker);
printDataHigh(worker);
}
finally
{
worker.close();
}
}
finally
{
box.close();
}
}
}
</name></vmap></personname></personname></personname>
10. Migrating From the Old Object Model
10.1. Old model
The usage of the old model (the package org.meta2project.model and its sub-packages) is not recommended, though allowed in the current version (in future versions this package will be removed).
10.2. Connections/Sessions
BoxWorker is the analogue of the old model's Connection.
Box is the analogue of the old model's Session.
10.3. Adapters
The transition from Connection to BoxWorker:
Connection con = ...; BoxWorker worker = con.getWorker();
The transition from BoxWorker to Connection:
BoxWorker worker = ...; Connection con = new BoxConnection(worker);
The corresponding Connection and BoxWorker are linked. The closing of the one automatically causes the closing of the other.
10.4. Closing and Transactions
The closing operation and transaction handling (commit/rollback) are different for BoxWorker and Connection.
In the old model the commit and rollback handlers are combined. The following scenario is used:
Session ses = ...;
Connection con = ses.openTransaction();
try
{
....
....
con.close(); // commit of changes, if success
}
catch (Exception e)
{
con.closeRollback(); // changes rollback, if error
}
This approach is inconvenient in practice, and in some situations can bring unclosed connections.
In the new model these handlers are separated.
Box box = ...;
BoxWorker worker = box.work();
try
{
...;
worker.commit(); // commit of changes, if success
}
finally
{
worker.close(); // close, and 'uncommitted' changes are rolled back
}
The branch
catch (Exception e)
{
worker.rollback();
}
also can be added, but it is unnecessary, because, when closed, all uncommitted changes are automatically rolled back. Do not forget to perform commit.
It is important that while handling a BoxWorker, it is possible to perform commit and rollback without closing.
... worker.commit(); ... worker.commit(); ... worker.commit();
Such a code is correct.
10.5. Method Names
In the old model the long and word-saturated names of methods are used. In the new model these names are maximally short: the word 'get' is omitted, as well as some secondary words.
There is an important difference in the names of methods, the result of which depends on the class hierarchy. In the old model the methods having the shorter names (without the word 'All') work without taking into account the hierarchy, and those with the longer names (including 'All') employ the hierarchy. In the new model the situation is opposite: shorter name (without 'Direct') work with the hierarchy, and the longer ones (with 'Direct') work without it.
The example of getting super-classes of a class:
In the old model OntClass.getAllSuperClasses returns all super-classes with inheritance. In the new model its analogue is BoxWorker.classes.
In the old model OntClass.getSuperClasses returns the direct super-classes. In the new model its analogue is BoxWorker.classesDirect.
These changes has been made after the estimation of method usage frequencies. The methods working with the hierarchy are invoked much more frequently and, thus, should have shorter names.
10.6. Entities
The objects of various classes play the role of entity identifiers in the old model, whereas in the new model integers play the same role.
The full names are also can be used as identifiers in the new model, by which one can obtain the corresponding integer identifiers.
10.7. Entity Type Verification
The entity type verification, which in the old model is normally implemented by Connection.getEntity(String fullName) with further 'instanceof' checking, is implemented in the new model by BoxWorker.entity(String fullName) with further comparison with enum Entity.
