Copyright © 2002 SMB GmbH
Ozone Documentation License, Version 1
This document is free software; you can redistribute it and/or modify it provided that the terms of the GNU Library General Public License as published by the Free Software Foundation version 2 of the License; and the following terms are met.
The Ozone Database Project <ozone@ozone-db.org>
Included in the ozone distribution is code and documentation made available by other copyright holders and under different licenses. All these licenses allow worldwide, royalty free distribution, whether alone or as part of a larger product. License, copyright and disclaimer of this software is included in this directory.
The document is Copyright (C) 1997-2001 by SMB GmbH, Rohrteichstr. 18, 04347 Leipzig, Germany, All rights reserved.
You must give prominent notice with each copy of the work that the document is used in it and that its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License.
The name ozone must not be used to endorse or promote software products derived from this software without prior written permission of SMB.
Software products derived from this document may not be called ozone nor may ozone appear in their names without prior written permission of SMB.
This document is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
Table of Contents
The Ozone Database Project is a java based Object Server that makes it possible to execute persistent Java objects in a transactional environment.
Ozone is an Open Source project distributed under the using the LGPL license (GNU Library General Public License). This basically means you can use it freely in both commercial and non-commercial situations and also build on top of the code and include Ozone in your project without having to pay royalties to anyone.
The Ozone project aims to evolve an object server that allows developers to build pure object-oriented, Java based, persistent applications. I.e. you can create your Java objects and let them run in a transactional database environment without any split between data model and object model. This makes it different from other application server environments e.g. EJB, which assumes that data (often stored in a relational database) is separate from the object (entity bean). EJB's also differentiate between data objects (entity beans) and objects performing business logic (session beans). Ozone objects are just one type but can act like entity beans or session beans if you choose to design them that way but there is no database other than Ozone itself behind the scenes.
Besides Java, Ozone also have an extended XML support making it an excellent XML data repository, especially when the XML manipulation you want to do can be created as Ozone objects inside the server. Ozone includes a fully W3C compliant DOM implementation that allows you to store XML data. You can use any XML tool to provide and access these data. Support classes for Apache Xerces-J and Xalan-J are included. Work is also underway to support the XML:DB api for XML databases.
Ozone started by Falko Braeutigam as a multimedia study project around ten years ago after reading an article about a new, document centric approach to desktop software where the idea was that the programs should not be primary but the data. The author called this "multimedia" because this technology should allow mixing data from different sources and of different format into one document. The text part should be controlled by a text component, the pictures by a picture component. While looking for a software architecture to support this, i.e. something that was able to serve as a container for the logic (components) that manipulates the persistent data he got an idea for a new approach to software development where logic and data is not separate and objects does not disappear when the program stops and where we should not have to build "programs" like before. Today we are calling this components and application servers.
This evolved into a system, which relies on a single-instance architecture where there is only one instance of a database object inside the database server, controlled via Proxy objects. Since the proxy object represent the actual database object it is used by the client as if it was the actual database object.
Two categories of objects can be stored in Ozone:
"Ozone objects". These are special objects which extend OzoneObject, have an interface, are accessed through a proxy, etc. The documentation at the web site has details about this.
Regular Java objects. These can be basically any java object. The only constraint is that they must implement Serializable.
You can divide Ozone objects in to two categories:
Named Ozone objects.
Unnamed Ozone objects.
How to can we then access these objects? You can only retrieve named objects directly:
db.objectForName("object_name");See the Ozone API documentation for more details. However, this named object can have references to other objects stored in the database, both Ozone objects and regular Java objects. You can follow these references and access these objects exactly as you would in a regular application, ignoring the fact that they are stored in a database. Therefore, each database must have at least one named Ozone object in it which you can retrieve by name. It may have references to other objects. Those objects may have references to other objects. As long as you can follow a series of references from a named object to that object, you can access it.
House myHouse = db.objectForName("MyHouse"); Door frontDoor = myHouse.getDoor(); Knob doorKnob = frontDoor.getKnob();myHouse, frontDoor, and doorKnob were all stored in the database. Only myHouse was a named object. myHouse has to be an Ozone object. frontDoor and doorKnob could be Ozone objects or just regular objects
It is common to put too much locic at the client instead of at the server. Let's take an example: Say you have a simple client-server program which is calling 150 calling update methods to write data to the database. This takes you about 12 seconds to do, a very long time that should indicate to you that something is wrong.
What is happening is that each method call results in a roundtrip to the server and a complete transaction begin/prepare/commit cycle - which is probably not intended and slow. By re-writing your 150 "writes" and putting the 150 item loop into a method of a database object is the solution. It's much faster and encloses the entire work in one transaction. Your application will now execute those 150 updates in milliseconds.
Table of Contents
This tutorial is meant to provide a first impression of the Ozone system. It gives a very simple example, which shows the most important features of and the work with ozone. Other, more complex, samples can be found in the 'samples' directory of the Ozone distribution. You may use them to get familiar with the more advanced concepts of the Ozone system or as starting points for your own projects.
To avoid object replication Ozone uses a single-instance architecture. This means that, at all times, there is just one instance of a database object, which lives inside the database server. Such database objects are controlled from the client via Proxy objects.
Sounds slow, doesn't it? It isn't! In fact, it's the same way as EJB works -but easier to program and faster when executing ;) The fact that database objects are controlled via Proxy objects does not mean that each and every method call needs a roundtrip from the client to the server. This is because Proxies can be used to reference a database object from the client and from other database object inside the server. So, most (almost all) database method calls are done inside the server, which is very fast. But back to the step-by-step introduction.
Proxy object represent the actual database object - inside the client applications and inside other database object. A proxy object can be seen as a persistent reference. Proxy classes are automatically generated out of the database classes.
In most cases the Ozone server runs in a separate Java VM. The client applications connect the server via UNIX domain sockets. However, it is also possible to run the Ozone server and the client inside the same Java VM. See the section called “The various types of databases” for a more extensive description. The example of this tutorial works with a separate server.
To run the example of this tutorial you have to install the Ozone system properly and start an Ozone server. Installation is described in the INSTALL file, which can be found in the Ozone root directory.
Both, client and server need to access the database classes and the generated Proxy classes. The most simple way to accomplish this is to start the client and the server from the directory where the classes are located.
In order to work properly the client applications need several *.jar files, which come with the Ozone distribution, in their CLASSPATH. The easiest way to accomplish this is to start client application via the ojvm command instead of just calling java.
In our example we would like to store cars. So we need a class named Car. But we remember that all database objects are controlled by Proxy objects. So the first thing we do is to define the method interface of our Car objects. Only the methods of this interface can be used to access the underlying database object. To mark this interface as an external interface we inherit it from the OzoneRemote interface of Ozone.
import org.ozoneDB.OzoneRemote; public interface Car extends OzoneRemote { public void setName( String name ); /*update*/ public String name(); public void setYearOfConst( int year ); /*update*/ public int age(); }
Please note the /*update*/ comment at the end of the set*() methods. Unfortunately in Java there is no way to determine methods that change the state of an object. But Ozone has to know these methods because such methods are update transactions instead of read transactions. So the programmer has to mark update methods explicitely. There are different ways to do so:
Comment in the interface source code.
Put a /*update*/ comment at the end of each method that changes the state of the object.
Pattern for the names of the update methods.
Name all update methods after a regular expression pattern. Such a pattern can be for example .*_update". When running OPP (the section called “OPP and Proxy objects”) this pattern has to be specified to make OPP able to determine the methods for which it have to generate special code. The Garage interface the section called “The "Garage" interface and "GarageImpl" class” uses patterns.
Ozone Class Descriptor.
An Ozone Class Descriptor (OCD) is an XML, which holds some meta information of the classes. The OPP (see the section called “OPP and Proxy objects” ) uses the class descriptor to determine the lock level of each transaction.
Our database objects will be instances of the CarImpl class. This class has to implement the Car interface. It extends OzoneObject to mark it as a database class. Here comes the code for the CarImpl class:
import org.ozoneDB.OzoneObject; import java.util.*; public class CarImpl extends OzoneObject implements Car { /** Set version of the serialized data to make it compatible with new class versions. */ final static long serialVersionUID = 1L; private String _name; private int _yearOfConst; public CarImpl() { _name = new String( "" ); _yearOfConst = 0; } public String name() { return _name; } public void setName( String name ) { _name = name; } public void setYearOfConst( int year ) { _yearOfConst = year; } public int age() { Calendar cal = Calendar.getInstance(); return cal.get (Calendar.YEAR) - _yearOfConst; } }
Now we can compile the interface and the class. You can use the following command line:
javac Car.java CarImpl.javaIn the last step we create the proxy class for CarImpl. Proxy objects are the substitutes for the database objects in your application. Their job is to pass your method calls for database objects through the connection to the database. They have the same interface as the actual database objects and you can work with them as they were database objects.
To create the proxy class for CarImpl we use the following command line: opp CarImpl
OPP is the Ozone Post Processor. It creates the proxy class for a given database class. In our example it creates a file named Car_Proxy.class.
Note that the Ozone server and the OPP have to have access to the class files of all Ozone related classes of an application. If you are working with packages, you must add the corresponding directories to you CLASSPATH. In this example we do not use packages. If there is the ./ directory in your CLASSPATH you can complete our car example by just starting the Ozone server and the OPP in the directory of this example.
The following "picture" shows all classes and interfaces that are needed for the Proxy System of Ozone. "Higher" classes/interfaces extend "lower" ones.
The classes on the left are server-side classes and the classes on the right are client-side classes. The [remote interface] is the glue that allows to use proxy objects like the actual objects.
########### .....>> # OPP # >>....... . ########### . . ^ . . ^ . --------------- . --------------- *Impl . *Impl_Proxy --------------- . --------------- | \ ~~~~~~~~~~~~~~~~~~ / | | ----- [remote interface] --- | --------------- ~~~~~~~~~~~~~~~~~~ --------------- OzoneObject | OzoneProxy --------------- | --------------- | ~~~~~~~~~~~~~~~~~~ | OzoneRemote ~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~ OzoneCompatible ~~~~~~~~~~~~~~~ Java interfaces : ~~~~~~~~ Java classes : -------- executable : ########
However, the only thing the programmer has to do is to write the code of the *Impl class and put all remote methods that should be accessible from the client side in the remote interface. OPP takes *Impl and remote interface and generates the *Impl_Proxy proxy class.
This section describes all steps that has to be completed to make a database class. It is a brief overview without code or examples. However, following the examples of this tutorial is the best way to learn how to work with Ozone.
Write the implemention.
Create a new class and name it *Impl (for example AutoImpl). This class should extend OzoneObject.
Create the remote interface.
When the implementation of the *Impl class is finished you have to create a Java interface that represents the remote interface of the *Impl class. Use a simple name for the interface (for example Auto).
Mark all update methods.
All methods of the remote interface that change the internal state of the object or that call a method that changes the internal state of the same object (for example this.doSomething()) has to be marked. Add a comment /*update*/ to the end of each line that defines an update method in the remote interface.
Let the *Impl class implement the newly created remote interface.
Let OPP create the proxy class.
The client application will create a car, make some operations on it and finally delete it. Here is the code of the client application:
import org.ozoneDB.*; public class MyApp { private static ExternalDatabase db; public static void main( String[] args ) throws Exception { if (args.length == 0) { System.out.println( "usage: ojvm MyApp create|delete|print" ); System.exit( 1 ); } // create and open a new database connection db = ExternalDatabase.openDatabase( "ozonedb:remote://localhost:3333" ); System.out.println( "Connected ..." ); db.reloadClasses(); if (args[0].equals( "create" )) { createCar(); } else if (args[0].equals( "delete" )) { deleteCar(); } else { printCar(); } db.close(); } public static void createCar() throws Exception { // create a new Car object with the name "my_first_car" // the return value is Car_proxy, which implements the Car-interface Car car = (Car)(db.createObject( CarImpl.class.getName(), 0, "my_first_car" )); car.setName( "gottfried" ); car.setYearOfConst( 1957 ); } public static void printCar() throws Exception { Car car = (Car)(db.objectForName( "my_first_car" )); if (car != null) { System.out.println( "The car " + car.name() + " is " + car.age() + " years old." ); } else { System.out.println( "Object my_first_car not found." ); } } public static void deleteCar() throws Exception { Car car = (Car)(db.objectForName( "my_first_car" )); if (car != null) { db.deleteObject( car ); } else { System.out.println( "Object my_first_car not found." ); } } }Compiling the program ist straight forward: javac MyApp.java But at first we have to start the Ozone server: ozone -d'your_ozone_data_directory' Then we run our small application: ojvm MyApp create
At first the program connects to the database on host "localhost" at port 3333. If everything was okay, we get a Connected output. The next step is to reload our database classes with reloadClasses(). This is necessary if we made some changes in our class code and the Ozone server was not restarted. In this case it holds the old class bytecode in its cache and it would not use our changed version.
Because we used the create option the program creates now a new database object of the type CarImpl and gives it the name my_first_car.
Please note that this name has no conjunction with the _name -member or the name()-method of CarImpl. This a database feature and it helps us to get our car back if we restart our small program.
The next to method calls assign the name of our car with "gottfried" and the year of contruction with 1957. Last but not least we close the connection and our program has finished.
The following command line starts the application: ojvm MyApp print The application connects to the server again and get our car back by using the objectForName-method. Then it prints the name and the age of our car. If everything was okay we get the following output: The car gottfried is 40 years old. Finally we close the connection.
We can delete our car object from the database by calling: ojvm MyApp delete When you have enough played with your first Ozone-application you can shutdown the Ozone-server simply by pressing q (return) in the Ozone-server terminal window or choose the shutdown command from the administration tool.
In the next sections we will extend out example by adding a garage for out cars.
Our cars need a garage. So we need to make a GarageImpl class and the corresponding interface. Note that opposit to the Car interface the update methods of Garage are not marked by an special comment but they are named after a regular expression pattern. In this example the pattern is .*_update. This pattern has to be specified to OPP when generating the proxy class.
import org.ozoneDB.OzoneRemote; import java.util.*; public interface Garage extends OzoneRemote { public void addCar_update( String name, int yearOfConst ) throws Exception; public Car carForName( String name ); public Car removeCar_update( String name ); public Vector oldtimers(); }
We will use a dictionary as the data structure for our garage. This dictionary contains all our cars identified by their name. The next step is clear: we have to write the implementation of our interface and we call this class GarageImpl.
import org.ozoneDB.OzoneObject; import java.util.*; public class GarageImpl extends OzoneObject implements Garage { /** Set version of the serialized data to make it compatible with new class versions. */ final static long serialVersionUID = 1L; private Hashtable cars; public GarageImpl() { cars = new Hashtable(); } public void addCar_update( String name, int yearOfConst ) throws Exception { Car car = (Car)database().createObject( CarImpl.class.getName() ); car.setName( name ); car.setYearOfConst( yearOfConst ); cars.put( car.name(), car ); } public Car carForName( String name ) { return (Car)cars.get( name ); } public Car removeCar_update( String name ) { return (Car)cars.remove( name ); } public Vector oldtimers() { Vector result = new Vector(); for (Enumeration e=cars.elements(); e.hasMoreElements(); ) { Car car = (Car)e.nextElement(); if (car.age() >= 20) { result.add( car ); } } return result; } public void onDelete() throws Exception { for (Enumeration e=cars.elements(); e.hasMoreElements(); ) { Car car = (Car)e.nextElement(); database().deleteObject( car ); } } }
Most of the code above is selfexplaining. We add, get and remove cars identified by their name. It seems to be easy, but remember that the cars-list does not contain objects of the type CarImpl! It contains only proxy objects of the type Car_proxy. Each of these objects stands for a database object of the type CarImpl. Both of these types, CarImpl and Car_proxy, implements the Car-interface and the proxy object has the same behaviour as the corresponding database object. In Ozone you always work with proxy objects. The only exception of this rule is, you are standing in a method of a database object. In this case you have of course direct access to the object via the this-reference. Have a look at the oldtimers() method. The result bag the cars dictionary contain proxy objects and so the call to the age() method of the cars is an RMI call.
Finally we take a look at the onDelete-method. Every OzoneObject has this method and it is invoked when we delete this object from the database. This is similar to the destructor in C++. Our onDelete() should delete all the cars of the garage when we delete the garage itself. For this purpose we need a connection to the database so that we can call deleteObject(). The method database() gives us the right database proxy. Every OzoneObject has this database -method and with this link to the database you can call database methods like createObject() or deleteObject.
Now we have to compile the Garage interface and the GarageImpl class and we have to generate the proxy class. javac GarageImpl opp -p".*_update" GarageImpl Note the -p parameter. This specifies the pattern after which all update methods are named.
Our application does four things (depending from the command line arguments): create a new garage, delete the garage, add a new car and print all oldtimers. Here comes the code:
import java.util.*; import org.ozoneDB.*; public class GarageApp { public static void main( String[] args ) throws Exception { if (args.length == 0) { System.out.println( "usage: ojvm GarageApp create|delete|oldtimers" ); System.exit( 1 ); } ExternalDatabase db = ExternalDatabase.openDatabase( "ozonedb:remote://localhost:3333" ); System.out.println( "Connected ..." ); db.reloadClasses(); if (args[0].equals( "create" )) { db.createObject( GarageImpl.class.getName(), 0, "my_garage" ); } else if (args[0].equals( "delete" )) { Garage garage = (Garage)(db.objectForName( "my_garage" )); if (garage != null) { db.deleteObject( garage ); } } else if (args[0].equals( "add" )) { Garage garage = (Garage)(db.objectForName( "my_garage" )); if (garage != null) { garage.addCar_update( args[1], Integer.parseInt( args[2] ) ); } else { System.out.println( "Garage object not found!" ); } } else if (args[0].equals( "oldtimer" )) { Garage garage = (Garage)(db.objectForName( "my_garage" )); if (garage != null) { System.out.println( "Oldtimers:" ); Vector oldtimers = garage.oldtimers(); for ( int i = 0; i < oldtimers.size(); i++ ) { Car car = (Car)(oldtimers.elementAt( i )); System.out.println( " Car " + car.name() + " age=" + car.age() ); } } else { System.out.println( "Garage object not found!" ); } } else { System.out.println( "Unknown parameter: " + args[0] ); } db.close(); } }The functionality of this program is clear. If we give add as option, the next arguments must be the name and the year of the construction of the car - the same with the other options.
Have a look at the oldtimer section in the main() method: The result list of the oldtimer() method contains proxy objects. And so the calls of the age() and name() are RMI calls. But we call these methods from outside the server, this means the methods calls go through the socket connection. But the code looks straight forward. We have just written the code as expected - and it works. However, iterating over collections of database object outside a database method results in very poor performance and consistency problems. In fact all method calls from outside the Ozone server are transactions. If the method call returns, the transaction is commited, otherwise you get an exception and the transaction is aborted. You need not to explicitly start and commit transactions. This means all actions of a transaction must be grouped in one method. In our example we iterate over a collection of objects. Of course this should be a transaction because we do not want the GarageImpl object to be changed while we are working with it.
With this brief tutorial you have got a first idea how to work with Ozone. You should play around with the examples to get the right feeling for it. It is easy to use Ozone, if you always keep the basic principles in mind. Especially important is the meaning of Proxy objects in Ozone. The rest is programming as usual.
Table of Contents
There are two basic ways of working with an Ozone Object Server:
Ozone is running as a remote application server started separately.
Ozone is used as a local persistence engine for your application and started by your application.
ExternalDatabase represents the basic idea of a remote object server. It is started by org.ozzoneDB.Server which is invoked by the db startup scripts (ozone.bat and ozone respectively).
All samples uses an External database. The important thing to remember is that all classes stored must be in the classpath. Either start the server in the directory where your compiled classes are -such as described in the "simple sample", or create a directory somewhere (eg. an "ozoneDeploy" dir at the same level as your database dir and copy all your Ozone Objects there. Add that directory to your classpath before starting the server.
LocalDatabase is a subclass of ExternalDatabase specialized to work as an in JVM persistance storage for an application. The simplest way to create a LocalDatabase is as follows:
String dbDir = "/usr/local/test/ozoneDB"; LocalDatabase db = new LocalDatabase(); db.create(dbDir);After the db has been created you start it up by calling open():
db.open(dbDir);Then you can use it as usual i.e. db.objectForName(...), db.createObject(...) etc.
Since open() throws an exception if no database exists you might want to query for that first and create a new db only if none is found:
db = new LocalDatabase(); if (!db.exists(dbDir)) { db.create(dbDir); } // Open the database db.open(dbDir);If you want to overrride the debug level setting in config.properties located in the root of the database directory you can set it by doing the following:
// Open the database db.open(dbDir, OzoneDebugLevel.ERROR_STR);
This setting would instruct the logging system to only log things of error severity and up (i.e. ERROR and FATAL). Ozone uses Log4J for its logging system so the levels corresponds to the Log4J levels in the Level class but we have added some debug levels to make development easier and hence our debug level all derive from OzoneDebugLevel instead of directly from Level. In reality it is often more flexible to not hard code this so that the debug level can be easily changed by editing the properties file compared to recompiling in order to change the log level.