This initial version of the TSpaces programmer's guide will describe the client interface only.
It also contains a section on how to start the server. If you are interested in more information on the TSpaces Server implementation, you should read the Server overview in the User Guide document.
This guide applies to TSpaces Version 2.1.2
In this section we briefly discuss the basic objects that are used to implement a TSpaces client application.
A Field object is the most basic component of the Tuplespace data structure hierarchy. A Field object contains:
A Tuple object is an ordered sequence of Fields.
The SuperTuple class is where the interesting functionality of tuples is implemented, but it's an abstract class, so clients should create either Tuple or SubclassableTuple objects.
A template tuple is a Tuple that is used for matching. One or more of the Fields in a template may be "wildcards" and consist of only the class type with no value.
A TupleSpace is a shared named collection (bag) of Tuples. The basic operations supported by the collection are to add a Tuple to the space (write), and to remove one from the space (take). The collection is stored and administered across a network on one or more "T Space Servers". Several threads on the same or different machines can be accessing the space simultaneously. Some will be adding tuples to the space and some removing them. A client thread accesses the collection/space using the methods of an instance of this class. For each different TupleSpace a client wishes to access, it requires a separate instance of this class, even if they are managed by the same server. The details of how the operations are completed are hidden from the user. All they need is the name of the space, and the name of a server that manages that space.
A TSpaces server contains many Tuplespaces. It is up to the application writer to decide whether to use one or many tuplespaces for a particular application. However, it is important to know that it's really hard to run out of spaces, so there's no reason to try to fit too many different tuple types into a single space.
The basic primitive operations supported by the space are:
The are also numerous other more specialized commands and there is an ability to define your own specialized commands.
The concepts of TupleSpaces was first developed as part of the Linda System at Yale University in the 1980s. JavaSpaces from Sun MicroSystems is also based on Linda and an excellent book JavaSpaces Principles, Patterns, and Practice has been written that covers the techniques of using "Spaces" for distributed programming. The first chapter from the book is available online
Although the examples in the book use the JavaSpaces implementation, it is trivial to write the same examples using the TSpaces implementation of Tuplespace. The HelloWorld example described later is an example of how to convert a JavaSpaces application to TSpaces.
Lets take a look at a some very simple code that will read and write data using TSpaces. The following are just fragments of the code, for the complete source, look at the source for Example1.java
String host = "tserver1.company.com";
...
TupleSpace ts = new TupleSpace("Example1",host);
The above will contact the TSpaces server that is running on the "tserver1" system
and connect to the TupleSpace that is named "Example1".
If the "Example1" TupleSpace does not
exist, it will be created at this time.
Tuple t1 = new Tuple("Key1","Data1");
ts.write(t1);
Tuple t2 = new Tuple("Key2",new Integer(999),"Data2");
ts.write(t2);
Tuple t3 = new Tuple("Key2","Data3");
ts.write(t3);
The above will write 3 Tuples to the TupleSpace allocated previously. Note that
the 3 Tuples do not really need to all have the same type of Fields. The 2nd Tuple
has 3 Fields while the others have 2 Fields.
Also, the contents of the Tuple could have been directly coded in the write command. or we could have done this the long way and directly allocated the Fields of the tuple. The following are all equivalent:
Field f1 = new Field("Key1");
Field f2 = new Field("Data1");
Tuple t1 = new Tuple();
t1.add(f1);
t1.add(f2);
ts.write(t1);
-or-
Tuple t1 = new Tuple("Key1","Data1");
ts.write(t1);
-or-
ts.write("Key1","Data1");
Now we will read one of the above Tuples using the first field as a key and then extract the contents of the second field.
Tuple template = new Tuple("Key2", new Field(String.class));
Tuple tuple = ts.read(template);
String data = (String)tuple.getField(1).getValue();
The read
command will read the matching Tuple from the TupleSpace
database and return it to the caller.
The parameter passed to read is a template Tuple.
A template Tuple is a Tuple that is used to select matching Tuples.
It will contain 0 or more Fields that are either Actual Fields or Formal Fields.
The Tuple that is returned must exactly match the format of the template Tuple. So in the example above, the Tuple("key2","Data3") will be returned. The Tuple("key2",new Integer(999),"data2") will not be returned because it does not match the format of the template.
The take command is similar to read but it will remove the matching Tuple from the TupleSpace database and return it to the caller.
The example above uses the simple TSpaces class names ( i. e. Tuple). The compiler needs to be able to translate this to the fully qualified name (i. e. com/ibm/tspaces/Tuple) so you need to add an import statement:
import com.ibm.tspaces.*;
In the section on how to compile we will describe how to point the compiler
and runtime environment to the proper TSpaces library.
The example above ignored exceptions that can be thrown by TSpaces.
However, almost all of the TupleSpace methods can throw TupleSpaceException
and therefore one needs to wrap the TSpaces code in Try/Catch blocks.
For example:
try {
TupleSpace ts = new TupleSpace("Example1",host);
ts.write("Key1","Data1");
ts.write("Key2",new Integer(999),"Data2");
ts.write("Key2","Data3");
} catch(TupleSpaceException tse) {
System.out.println("TupleSpace Exception: " + tse.getMessage());
tse.printStackTrace();
}
Some of the examples that follow, may not show the try/catch in order to make the
example easier to follow, but be assured, you must have either try/catch blocks or
declare that the method throws TupleSpaceExecption.
When an application issues a WaitToRead or WaitToTake call, and the data is not yet there on the server, the application blocks on the call until an answer is returned. When a tuple arrives on the server that matches the Read or Take query, it is sent to the client and the application resumes.
As an example, assume that in a client running on machine1, you had the following code:
try {
TupleSpace ts = new TupleSpace("Example",host);
SuperTuple answer = ts.waitToTake("3",new Field(String.class));
...
} catch(TupleSpaceException tse) {
System.out.println("TupleSpace Exception: " + tse.getMessage());
}
And assume that in a client running on machine2, you had the following code:
try {
TupleSpace ts = new TupleSpace("Example",host);
ts.write("1","Data1");
Thread.sleep(5000);
ts.write("2","Data2");
Thread.sleep(5000);
ts.write("3","Data3");
} catch(TupleSpaceException tse) {
System.out.println("TupleSpace Exception: " + tse.getMessage());
}
Assuming that the applications started at the same time, the application on machine1
would issue the waitToTake and then be blocked. However, about 10 seconds later
as soon as the matching Tuple arrived at the server, the server would send it to machine1
which would unblock and process the matching Tuple.
The default for blocking commands is to wait forever. However one can specify an optional timeout parameter to specify that if the operation has not be satisfied when the time limit is exceeded, then the waiting operation should be terminated and the null should be returned. So if we wanted to only wait for 60 seconds, then the code above would now look like the following:
try {
TupleSpace ts = new TupleSpace("Example",host);
Tuple template = new Tuple( "3",new Field(String.class));
SuperTuple answer = ts.waitToTake(template,60*1000));
if (answer == null) {
System.out.println("Operation timed out");
...
} catch(TupleSpaceException tse) {
System.out.println("TupleSpace Exception: " + tse.getMessage());
}
The examples above used the Tuple class to contain the Fields that are written to or read from a TupleSpace. The Tuple class is a subclass of SuperTuple which is an abstract class and can not be instantiated. The Tuple class is declared final and cannot be subclassed. An alternative to Tuple is SubclassableTuple which can be subclassed.
Some of the advantages and disadvantages of defining your own SubclassableTuple are:
template = new SCTuple(key)
instead of
template = new Tuple(key,new Field(String.class));
data = sctuple.getData()
instead of
data = (String)tuple.getField(1).getValue()
It is important to note that since a SubclassableTuple is not a subclass of Tuple, one does not have to be concerned that a Tuple template will accidentally match a user defined SubclassableTuple.
Let's take a look at an example that is similar to example1 but uses SubclassableTuple. The following are just fragments of the code, for the complete source, look at the source for Example2.java
Example2Tuple mytuple;
mytuple = new Example2Tuple("Key1","Data1");
ts.write(mytuple);
mytuple = new Example2Tuple("Key2","Data3");
ts.write(mytuple);
The Example2Tuple is defined as shown below.
class Example2Tuple
extends SubclassableTuple
implements Serializable {
public Example2Tuple(String key,String data) {
super(key,data);
}
public Example2Tuple(String key) {
// build template if only one operand
super(key,new Field(String.class));
}
public String
getData() throws TupleSpaceException {
return (String)this.getField(1).getValue();
}
Now we will read one of the above Tuples using the first field as a key and then extract the contents of the second field.
Example2Tuple template = new Example2Tuple("Key2"));
mytuple = (Example2Tuple)ts.take(template);
String data = mytuple.getData();
If you compare this to Example1, you will see that the interface to TupleSpace has been simplified. In fact, a subclass of SubclassableTuple can be made into a JavaBean and make it very easy to use in a Java IDE.
No Programmer's Guide would be complete without a "Hello World" example. In this example, we will give yet another example of TSpaces coding and also show how one would convert a JavaSpaces program to TSpaces. The first chapter from the excellent book, JavaSpaces Principles, Patterns, and Practice is available online This chapter contains a HelloWorld example and uses it to introduce JavaSpace programming. We will use a somewhat different example and show both the JavaSpaces and TSpaces example. For our example, we will show simple message passing between 2 programs. One program will send a message to the "World" program and then wait for a reply. The "World" program will wait for a message and when it gets it, it will send a reply back to the message's sender.
It is fairly easy to modify a JavaSpaces application to run under TSpaces. Both systems have a common heritage but there are some terminology differences. Instead of defining Tuples, with JavaSpaces, you define classes that implement the Entry class. These Entry objects then have Public instance variables that correspond to the TSpaces Field objects. One can fairly easily convert a JavaSpaces class that implements Entry into a TSpaces SubclassableTuple class. For this example, the Message class is the Entry object that we need to convert. The JavaSpaces version is:
import net.jini.core.entry.Entry;
import java.io.Serializable;
public class Message implements Entry {
public String to;
public String from;
public Serializable message;
public Message() {
}
public Message(String to) {
this.to = to;
this.from = null;
this.message = null;
}
public Message(String to, String from, Serializable message) {
this.to = to;
this.from = from;
this.message = message;
}
}
The equivalent TSpaces Message class is:
import com.ibm.tspaces.*;
import java.io.Serializable;
public class Message extends SubclassableTuple {
public Message() throws TupleSpaceException {
super(new Field(String.class),new Field(String.class),new Field(Serializable.class));
}
public Message(String to) throws TupleSpaceException {
super(to,new Field(String.class), new Field(Serializable.class));
}
public Message(String to,String from, Serializable message) throws TupleSpaceException {
super(to, from, message);
}
//
// get/set methods for Destination, From and Message follow
//
public String getDestination() throws TupleSpaceException{
return (String)getField(0).getValue();
}
public void setDestination(String to) throws TupleSpaceException{
getField(0).setValue(to);
}
public String getFrom() throws TupleSpaceException{
return (String)getField(1).getValue();
}
public void setFrom(String to) throws TupleSpaceException{
getField(1).setValue(to);
}
public Serializable getMessage() throws TupleSpaceException{
return getField(2).getValue();
}
public void setMessage(Serializable message) throws TupleSpaceException{
getField(2).setValue(message);
}
}
The major changes are:
The class Helloworld then implements both halves of the message passing logic. It uses a parameter on the command line to identify itself and then sees if it has a message waiting. If so, it replies to it. If not, it sends a message to "World" and then waits for a reply.
The HelloWorld class for JavaSpaces is:
import jsbook.util.SpaceAccessor;
import net.jini.core.lease.Lease;
import net.jini.space.JavaSpace;
public class HelloWorld {
public static void main(String[] args) {
String myName = "World";
boolean needReply = false;
if ( args.length > 0)
myName = args[0];
try {
JavaSpace space = SpaceAccessor.getSpace();
Message template = new Message(myName);
Message msg = (Message)space.takeIfExists(template,null,long.MAX_VALUE);
if ( msg == null) {
msg = new Message("World", myName,"Hello World");
needReply = true;
} else {
msg = new Message(msg.from,myName,"Hi");
}
space.write(msg, null, Lease.FOREVER);
if (needReply) { // Wait for a reply
msg = (Message)space.take(template,null,long.MAX_VALUE);
}
} catch (Exception e) {
e.printStackTrace();
}
} //main
} // class
The HelloWorld class for TSpaces is:
import com.ibm.tspaces.*;
public class HelloWorld {
public static void main(String[] args) {
String myName = "World";
boolean needReply = false;
if ( args.length > 0)
myName = args[0];
try {
TupleSpace space = new TupleSpace();
Message template = new Message(myName);
Message msg = (Message)space.take(template);
if ( msg == null) {
msg = new Message("World", myName,"Hello World");
needReply = true;
} else {
msg = new Message(msg.getFrom(),myName,"Hi");
}
space.write(msg);
if (needReply) { // Wait for a reply
msg = (Message)space.waitToTake(template);
}
} catch (Exception e) {
System.out.println(e);
}
} //main
} //class
As you can see, other than import statements, there are only minor changes required.
You can also just use the Tuple class to replace the need for the Message class. For many applications, this is simpler than defining a SubclassableTuple. We have included below the code for HelloWorld using only the Tuple class to define the Tuples that are written to the space.
package com.ibm.tspaces.examples.helloworld;
import com.ibm.tspaces.*;
import java.io.Serializable;
public class HelloWorldT {
public static void main(String[] args) {
String myName = "World";
boolean needReply = false;
if ( args.length > 0)
myName = args[0];
try {
TupleSpace space = new TupleSpace();
Tuple template = new Tuple(myName,
new Field(String.class),new Field(Serializable.class));
Tuple msg = space.take(template);
System.out.println(msg);
if ( msg == null) {
msg = new Tuple("World", myName,"Hello World");
needReply = true;
} else {
msg = new Tuple((String)msg.getField(1).getValue(),myName,"Hi");
}
space.write(msg);
if (needReply) { // Wait for a reply
msg = space.waitToTake(template);
System.out.println(msg);
}
} catch (Exception e) {
System.out.println(e);
}
}
}
There are other differences between JavaSpaces and TSpaces not shown in the example above.
The read and take methods return a single matching tuple. If more than one matches, then it will return the first matching tuple. However there are TupleSpace methods that will handle multiple matches.
Here is some sample code that makes use of countN and scan.
Tuple template = new Tuple("Key2",new Field(String.class));
int count = ts.countN(template);
if (count > 0) {
Tuple tupleSet = ts.scan(template);
if ( tupleSet != null) {
for( Enumeration e = tupleSet.fields(); e.hasMoreElements(); ) {
Field f = (Field)e.nextElement();
Tuple tuple = (Tuple)f.getValue();
data = (String)tuple.getField(1).getValue();
...
}
}
}
First we create a template Tuple that will be used to select the Tuples from the server. The next line issues countN to get the count of matches. If we have 1 or more matches, then we issue the scan request to the tupleSpace using the template from earlier. This returns a tuple "tupleset" that represents the set of matching tuples. So "tupleset" contains one or more fields; the value of each field is one of the tuples returned by the scan. So the next step is to use an Enumeration to extract each of the returned Tuples. The fields() method returns an enumeration that can be followed to access each individual Tuple. Once the individual Tuple is accessed, the getField(int).getValue() method is used to access the data in the Tuple.
One of the most useful features of TSpaces is the ability to register with the server to be informed when specific types of events occur. For example, you might want to be informed whenever a Tuple that matches a specific template is written to the Server by any client. With TSpaces this is done by using the TupleSpace.eventRegister() method to indicate the type of event you are interested in and specifying the Callback object that you want to have handle the event.
The following are just fragments of the code, for the complete source, look at Example3.java to see the code in context. Also, the whiteboard sample program is an example of a real application that makes use of eventRegister to support multiuser communication.
Tuple template = new Tuple( "Key2", new Field(String.class) ); ExampleeCallback callback = new Example3Callback(); boolean newThread = true; // default is false int seqNum = ts.eventRegister(TupleSpace.WRITE, template, callback, newThread ); ... ts.eventDeRegister(seqNum);
The above sets up a template Tuple that describes the format of Tuples that we are interested in. The eventRegister() method tells the server to watch for a Tuple being written to the TupleSpace that matches the template Tuple. When this occurs, it should invoke the call() method for the Example3Callback object. The setting of newThread to true indicates that a new Thread should be started to process the callback.
When you no longer want to be informed of Tuple events, you can remove the event registration with the eventDeRegister() method.
class Example3Callback implements Callback {
public boolean
call(String eventName,String tsName,int seqNum,SuperTuple tuple,boolean isException) {
if (! isException) {
... process the tuple passed to this method.
String data = (String)tuple.getField(1).getValue();
} else {
... handle exception
}
return false;
} // call()
}
The above defines a class that implements the TupleSpace Callback interface. The Callback interface requires that it implements the call method with the following parameters:
If the newThread variable in the above eventRegister call had been false, then the method would have been invoked out of the server communication thread should do something really quick and return, that is it should not take up lots of time. Typically, it might just verify that isException is not true, and then queue up the tuple and wake up some other thread that will do the real processing.
Normally this method would return false. It would return true only if this is the last call this callback class is expecting.
Warning:
You cannot issue TupleSpace operations, (e. g. ts.write ) inside the call method
unless the newThread option is set true. Otherwise
this will cause the client thread that handles communication with the
server to hang.
Warning:
Although the Tuple that caused the event is passed to you in the callback,
that does not mean that you have "control" of the Tuple. It may have also
been passed to other clients. Depending on your purpose for getting the event,
you may want to do a "take" of the Tuple to ensure ownership of the Tuple.
The intent is that the "V" version number will change only for a very major change. The "L" Level number will change when new major new features are added and/or incompatible changes are made. The "M" Modification number will change when a new level of compatible code is distributed to fix problems and possibly add minor new functions.
Tuple active = TupleSpace.status(Host,Port);
//
if ( active == null || active.getField(0).getValue().equals("NotRunning"))
System.out.println("Server not running on " + Host+":"+Port );
else
System.out.println("Server Version "+active.getField(1)+" is active on " +Host+":"+Port );
The delete(Tuple template) method will delete the Tuples that match the specified template Tuple.
The deleteAll() method will delete all of the Tuples in the space. This is equivalent to the following:
Tuple all = new Tuple(); ts.delete(all);The Tuple with no Fields acts as a wildcard that matches all Tuple objects.
While we are on the subject of wildcards, if you want to have a wildcard for one of the Fields that will match any object, then you would use the Serializable.class for the Formal class.
ts.write("key","a string");
ts.write("key",new Integer(99));
Tuple template = new Tuple("key",new Field(Serializable.class));
Tuple result = ts.scan(template);
The above scan would pick up both Tuples because the Formal Field
matches any Field value that could have been written in a Tuple.
The multiWrite(Tuple tupleArray) method is an efficient way to write a large number of Tuples to the server. For example, during initialization, you may want to load the server with an initial set of data.
Tuple multi = new Tuple();
for (int i=0; i<10;i++) {
Tuple nextTuple = new Tuple("Test6",new Integer(i),"Tuple# "+i);
multi.add(new Field(nextTuple)); // add Field to Tuple
}
TupleID[] ids = ts.multiWrite(multi);
The above code creates the "multi" Tuple and then adds 10 Fields to it, where
each Field is a Tuple that is to be written to the TupleSpace.
The multiWrite method returns a array of TupleIDs, one for each of
the individual Tuples.
The update(uniqueId,updatedTuple) method will update a specific Tuple in the space.
Tuple newTuple = new Tuple("Data1");
TupleID id = ts.write(newTuple);
Tuple updatedTuple = new Tuple("UpdatedData1");
id = ts.update(id,updatedTuple);
The above code writes a Tuple to a space. The write() method returns a TupleID
that represents the uniqueId of this Tuple.
This TupleID is then used to update the original
Tuple. The TupleID can also be obtained by calling the getTupleID() method
for a tuple that was read from the Space. The TupleID for a Tuple is persistent and is
valid across restarts of the Server.
There is also a multiUpdate command that is similar to MultiWrite. Given an instance of TupleID, one can also use the readTupleById(tupleid) and the deleteTupleById(tupleid) commands to read or delete specific Tuples.
TupleSpace.cleanup();
The cleanup() method is a Class method that will close all of the active TupleSpaces. It should only be used when you have finished with all TupleSpace activity. The default tuplespace action is to use the Object finalize method to take care of the cleanup. Use of finalize may be cause an applet security violation so you can turn this behavior off by coding:
TupleSpace.appletEnvironment(true); TupleSpace ts = new TupleSpace(...);
Another area where you need to use cleanup() is to have your client be able to recover and continue when a server goes down and then is restarted or when the network connection to the server is broken for a period of time. What you need to do on the client when the server crashes or goes down is to recognise that it has gone down by intercepting the exception in the event register callback or by catching the exception on the next Tuplespace operation. Then you need to make a call to cleanup() to cleanup the existing connection and then reissue the "new TupleSpace(space,server)" So it might look something like this.
TupleSpace ts = new TupleSpace(space,host);
while (true) {
try {
while ( ts == null) {
sleep(5000);
ts = new TupleSpace(space,host);
}
Tuple result = ts.take(template);
} catch(TupleSpaceException e) {
System.out.println(e);
if (e.getMessage().indexOf("Server crashed")) {
TupleSpace.cleanup(ts.getHost(),ts.getPort());
ts = null;
}
}
}
or in a callback.
public boolean
call( String event,String space,int seq,
SuperTuple tuple,boolean isException) {
if ( isException) {
ts = null;
return true;
} else {
...
ConfigTuple config = new ConfigTuple();
config.setOption(ConfigTuple.PERSISTENCE, Boolean.FALSE );
config.setOption(ConfigTuple.FIFO, Boolean.TRUE );
TupleSpace ts = new TupleSpace(MyTsName,host,TupleSpace.DEFAULTPORT, config);
Tuple myTuple = new Tuple("Hello","World);
myTuple.setExpire(5*60*1000); // expire in 5 minutes
ts.write(myTuple);
SuperTuple result = ts.read(myTuple); // will return myTuple
Thread.sleep(5*60*1000);
result = ts.read(myTuple); // will return null
Note that when the tuple expires, it is not reported to any eventRegister callbacks.
Our examples up to this point have all used Tuples with basic Java types like String and Integer. However, subject to certain restrictions, any Java object regardless if defined by the Java Language or by the user can be placed in a Tuple and written to and read from TSpaces.
The restrictions are:
The following are just fragments of the code, for the complete source, look at Example4.java to see the code in context.
Example4Obj obj = new Example4Obj("User Object 1");
Field f2 = new Field(obj);
ts.write("Key1",f2);
obj = new Example4Obj("User Object 2");
f2.setValue(obj);
ts.write("Key2",f2);
Tuple template = new Tuple("Key2",new Field(Example4Obj.class));
Tuple mytuple = ts.take(template);
Example4Obj objreturned = (Example4Obj)mytuple.getField(1).getValue();
The above looks much like the previous example except that previously, the 2nd Field was
a String.
Now we will look at the definition of the class:
class Example4Obj implements Serializable, Comparable {
static final long serialVersionUID = -2475757193974756987L;
String userdata;
public String
getData() {
return userdata;
}
public boolean
equals( Object other ) {
if ( (other == null) || ! (other instanceof Example4Obj) )
return false;
if ( userdata == null )
return ( ((Example4Obj)other).getData() == null);
return userdata.equals(((Example4Obj)other).getData());
} // end equals()
public int
compareTo( Object other ) {
if ( (other == null) || ! (other instanceof Example4Obj) )
return +1;
if ( userdata == null )
return ( -1);
return userdata.compareTo(((Example4Obj)other).getData());
} // end compareTo()
}
The things to note in this definition are:
In many cases, it is not feasible to ensure that the class definition for a client defined object be available in the TSpaces server. The best way to bypass this problem is to serialize the object into a byte array prior to sending it to the server and just send the server a field that is defined as byte[]. The FieldPS object is designed to do this Field Pre Serialization. FieldPS is a subclass of Field and acts like a Field object except when handed an Object, it automatically serializes it into a byte[] object and stores that. When requested to return the value of the Field, it automatically returns the deserialized value.
Here is an example of the use of FieldPS. The following are just fragments of the code, for the complete source, look at Example4a.java to see the code in context.
Example4aObj obj = new Example4aObj("User Object 1");
FieldPS f2 = new FieldPS(obj); // <---changed
ts.write("Key1",f2);
obj = new Example4aObj("User Object 2");
f2.setValue(obj);
ts.write("Key2",f2);
Tuple template = new Tuple("Key2",new FieldPS(Example4Obj.class));
Tuple mytuple = ts.take(template);
Example4Obj objreturned = (Example4Obj)mytuple.getField(1).getValue();
Note that only the statement that defines the 2nd Field has changed. All of the
rest of the code is identical. It is important to also note that you use
FieldPS when you define the template to retrieve the tuple. Also, any
attempt to do matching on the FilePS field will probably fail since 2 equal objects
may very likely have different serialized representations.
But you can have one of the other
Tuple Fields be the value that you want to match on.
For example, if you wanted to match on the "userdata" field in the Example4Obj object,
you could add another
Field to the Tuple that would contain the value of userdata;
ts.write("Key1", obj.getData(), new FieldPS(obj));
In what has been discussed so far, client application programs create their own version of a space and invoke various operations like read, write, blocking read, blocking write etc. In general, a client program consists of a set of operations performed on one or more spaces. It is not hard to think of applications where all operations must appear as one atomic unit to anyone interested in the actions performed by the application. This suggests a transaction model.
Each operation unless specified to be part of a transaction is considered to be a stand-alone transaction. To execute operations as a transaction, the client application creates an instance of a Transaction class and calls the beginTrans() method of the Transaction class on it. A TupleSpace object can be added to the Transaction object using the addSpace() method of the Transaction class. Any operation performed on a TupleSpace instance that has been added to a Transaction object, belongs to the transaction corresponding to that Transaction object, if performed after the start of the transaction. The commitTrans() method of the Transaction class is called to make the effect of operations in the transaction visible to other transactions. The abortTrans() method can be called to undo all of the operations that belong to the current transaction.
The following is an example of how a client program can make use of the transaction model. In the example, either all 3 of the writes will succeed or none of them will succeed. You can also look at Example7.java to see a more complete example of how the Transaction support is used.
Transaction trans = new Transaction();
TupleSpace ts = new TupleSpace("testing");
trans.addSpace(ts);
trans.beginTrans();
// start of transaction
ts.write("email", "to", "dick@sun");
ts.write("email", "text", "Hello");
ts.write("email", "from", "jane@ibm");
trans.commitTrans();
// end of transaction
A client application can identify the userid that it is associated with. This is done either by:
Access control is on a TupleSpace operation level; each operation defined on a TupleSpace has an associated list of AccessAttributes that must be satisfied by any client trying to execute that operation. For example, the take operation requires either the general Read and Write AccessAttributes or the more specific Take AccessAttribute.
The access levels for a TupleSpace are granted to a Principal based on an Access Control List (ACL) for that space. A Principal is any defined User or Group. The Acl contains entries that define a Principal and the permissions that the Principal is granted.
Like most systems, groups are just groupings of users and users can be members of different groups. Permissions that are granted to a group are passed to all the groups and users that are members.
The access rights for a TupleSpace are granted when the TupleSpace is created. If not specified in the TupleSpace constructor, then a default Acl is assigned to the space when it is initially created.
Additional information on setting up access control at the server can be found in Setting up Access Control for the Server section of this document
Let's take a look at an example that is similar to Example1 but makes use of AccessControl. The following are just fragments of the code, for the complete source, look at the source for Example5.java
First we create a Tuple that describes the access permissions that we want to grant to this TupleSpace.
String owner = MyUserid; // some code to fill in the current userid
AclEntry ae1 = AclFactory.createAclEntry("Users",P_READ);
AclEntry ae2 = AclFactory.createAclEntry("anonymous",P_READ);
ae2.setNegativePermissions();
AclEntry ae3 = AclFactory.createAclEntry(owner,new Permission[] {P_READ,P_WRITE});
Acl myacl = AclFactory.createAcl("ExampleTS",ownere,
new AclEntry[] {ae1,ae2,ae3});
Tuple permission = new Tuple((Serializable)myacl);
The above code creates an Access Control List (Acl) that will give:
TupleSpace ts = new TupleSpace("ExampleTS,
host,TupleSpace.DEFAULTPORT,null,
permission,
MyUserid,MyPassword);
The above TupleSpace constructor now has more parameters than what we have seen in earlier
examples. The 5th parameter("permission") is the Tuple that we built above.
The 6th and 7th paramters are the userid and password for the user that is running this
application. This information would have been obtained by prompting the user
for it.
As a result of the above, your userid will be able to read and write to the TupleSpace and all other users except "anonymous" will be able to read from the space. The user "anonymous", which is the default userid when no user is specified, will not have any access to the space.
An alternative method for specifying the userid and password information is the following:
TupleSpace.setUserName(Myuserid);
TupleSpace.setPassword(MyPassword);
boolean exists = TupleSpace.exists("ExampleTS",host);
if (exists) {
ts = new TupleSpace("ExampleTS",host);
} else {
ts = new TupleSpace("ExampleTS",host,TupleSpace.DEFAULTPORT,null,permission);
}
This method is preferred when the exists() method is used because
the authorization must be set prior to invoking the exists method.
111
There is also an XMLQuery, but this is described in the XML section. In the set of sample programs that are provided with tspaces, there is the SuperHeros sample that is a good example of using the query facilities of TSpaces.
Tuple tup = new Tuple( "tuple1", "3" ); Tuple result = ts.scan( new MatchQuery( tup ) );This code will return all tuples that exactly match the given tuple. The result is the same as if template match was used. (i.e. the following would be equivalent to the above.)
Tuple result = ts.scan("tuple1","3");
Tuple result = ts.scan(new IndexQuery( "foo", new Integer(8) ));This code will return all tuples with the Integer value 8 in the field named "foo".
Alternatively, the Object passed to the IndexQuery can be a Range. The Range constructor takes two parameters (upper and lower bound). If neither upper nor lower bound is null, it's a normal exclusive range (i.e. everything strictly greater than the lower bound and strictly less than the upper bound). If either bound is null, it's an open-ended range (e.g. lower bound null will return everything less than the upper bound):
Range myRange = new Range(null, new Integer(8)); Tuple result = ts.scan(new IndexQuery( "foo", myRange ));This example will return all tuples with a value less than 8 in the field named "foo".
The IndexQuery requires the use of named Field objects.
For Example:
Field f = new Field("keyfld",new Integer(345765))
will define a Field with the name of "keyfld". All Tuples that
contain a "keyfld" Field will be indexed.
ts.scan(new AndQuery(new OrQuery(new IndexQuery("index1", new Range(new Integer(3), new Integer(7))),
new IndexQuery("index2", new Range(new Integer(6), new Integer(9)))),
new MatchQuery(new Tuple(new Field(Integer.class), new Field(String.class) )))
This query will return all tuples with structure matching
(Integer,string) and having either a value (strictly) between 3
and 7 in the "index1" field, or a value strictly between 6 and 9 in
the "index2" field. Note that order may be important here. Currently the Query
is not optimized such that the "smallest" query is always applied first.
Restrictions:
Let's take a look at an example Applet. The following are just fragments of the code, for the complete source, look at the source for AppletTst1.java
TupleSpace.appletEnvironment(true);
URL url = getCodeBase();
String urlhost = url.getHost();
TupleSpace ts = new TupleSpace("myspace",urlhost)
public void
init() {
if (instanceCount > 0 ) {
// fail applet with message
} else {
instanceCount++; // keep track of how many are started
}
...
public void
destroy() {
instanceCount--;
if (instanceCount == 0) {
try {
_ts.eventDeRegister(_seqNum);
} catch (TupleSpaceException tse) {
Debug.out(tse);
}
TupleSpace.cleanup(_ts.getServer(),_ts.getPort());
_ts = null;
}
}
Note that there is an instance count that is used in this case to prevent 2 instances of the applet from running. This is one simple way to prevent one instance of an applet from destroying the environment for another instance of the applet.
The indirection that results from this scheme makes it possible to easily alter the handlers associated with a space. By adding a new factory it is possible to completely alter the semantics of a space. The selection of a handler is completely up to the factory, and the implementation of a command is completely up to the handler.
By stacking factories so that a new one passes any commands it doesn't recognize along to the previous factory, it is possible to augment the semantics of a space (adding a new command for instance) without the need to completely implement basic functionality.
TSpaces allows a user with ADMIN authority for the server to add TSFactory and TSHandler objects dynamically at runtime. This makes TSpaces extremely flexible
Note: We have since added Tuple Expiration to the base system so this is a useless example but it is still a good example.
To implement the behavior that we want, we have defined StaleTuple, which is a SubClassableTuple, and StaleTupleSpaceHandler.
StaleTuple is used to hide the details of the Tuple format under get and set methods and is similar to our earlier example of a SubclassableTuple
StaleTupleSpacehandler is where all of the interesting code resides. Since the implementation of a handler at runtime involves both client and server actions, let's look at the client actions first. The client code that will be invoked is in the main() method for StaleTupleSpacehandler. In a real application, this would more likely be part the application code but it is convenient here to bundle it all in one file.
TupleSpace ts = new TupleSpace(TSName,
Host,TupleSpace.DEFAULTPORT,
null,null,
MyUserid,MyPassword);
This will create a TupleSpace with the specified name on the specified
server. Note that we specify our userid and password which in our case was
specified on the command line but could have been prompted for with a GUI
interface. This userid must have ADMIN authority for the Server
so that we will have the authority to issue the following methods.
Now we will add a TSFactory object that supports downloadable handlers. We have supplied TSFExtendable which will satisfy this need.
final String FN = "com.ibm.tspaces.server.handler.TSFExtendable";
ts.addFactory( new Class[] { Class.forName(FN)} );
Now we will create an instance of StaleTupleSpaceHandler and issue an addHandler command for each new or modified command that this handler will service.
StaleTupleSpaceHandler dh = new StaleTupleSpaceHandler();
ts.addHandler( TupleSpace.WRITE, new Class[] { dh.getClass()} );
ts.addHandler( StaleTupleSpaceHandler.START, new Class[] { dh.getClass()} );
We will now issue the START command and use our modified Write command to write an instance of StaleTuple to this TupleSpace.
Tuple argtuple = new Tuple(60*1000);
ts.command( StaleTupleSpaceHandler.START,argtuple);
StaleTuple test1 = new StaleTuple("key1","some data");
test1.setTimeToLive(15000); // 15 seconds of life
ts.write(test1);
The above code issues the new START command and specifies that
the new thread should be invoked every 60 seconds. Note that we
have to use the
TupleSpace.command()
method since the TupleSpace class doesn't know anything about "START".
The command() method will simply send the START command and any argument Tuple
to the server.
However, the normal TupleSpace.write command can be used
but at the server our new
implementation will be invoked for this particular space.
As we will see in the next section, our new write implementation will
use the specified TimeToLive value to generate a timestamp that is
added to the StaleTuple before it is written to the database.
Now let's look at the implementation of the StaleTupleSpaceHandler. There are a number of methods that are required to be present for a Command handler to implement or extend commands. In addition to the brief description here, the sample code has comments that describe what is needed.
public AccessAttribute []
attributes(String cmdString, SuperTuple argTuple)
throws TSHandlerException {
AccessAttribute admin[] = { AccessAttribute._ADMIN_ATTRIBUTE };
AccessAttribute write[] = { AccessAttribute._WRITE_ATTRIBUTE};
if(cmdString.equals(TupleSpace.WRITE) )
return write;
if(cmdString.equals(StaleTupleSpaceHandler.START) )
return admin;
} // attributes
Note that there is a check for the Write command being enabled. This enabled switch would be set by the START command. For this command this is really not needed but because the Server internally uses the WRITE command, it may be important that the user added WRITE command is completely setup before it is turned loose to handle all WRITE requests.
if (_enabled)
((StaleTuple)argTuple).updateExpirationDate();
final TSHandler simpleWrite = ts.mostBasicHandler( TupleSpace.WRITE, argTuple);
simpleWrite.command( ts, TupleSpace.WRITE, argTuple, clientID_, communicator_, user_);
retValue.add(argTuple.getField(0));
return retValue;
The implementation of the START command is quite different. It gets the parameter that says how often we want to check for expired tuples and then calls a special constructor for the StaleTupleSpaceHandler to construct a new Runnable instance and then it sets up a thread for the daemon and starts the thread executing.
Field first = argTuple.getField(0); final long howOften = ((Long)first.getValue()).longValue(); StaleTupleSpaceHandler newExecuter = new StaleTupleSpaceHandler(ts,howOften,clientID_, communicator_); Thread doTheLoop = new Thread(newExecuter,"StaleKiller"); doTheLoop.setDaemon( true); // this is a daemon so set it on doTheLoop.start(); _enabled = true; // enable the WRITE command
The code for the run method is shown below. Basically it builds a template that contains the current date and time and then uses this template to issue a delete command that will delete all StaleTuple instances that have an earlier timestamp. In order to satisfy the transaction locking of TSpaces, it first generates a Client TransactionID and calls the TransactionManager to begin a transaction. After the delete, it call the TransactionManager to commit the transaction.
public void run(){
long throwOutBeforeThis = System.currentTimeMillis();
StaleTimestamp timestamp = new StaleTimestamp(throwOutBeforeThis);
StaleTuple template = new StaleTuple(timestamp);
int ct =0;
String myClientID;
TransactionManager transMgr = TSServer.getTransMgr();
TSHandler simpleDelete = _ts.mostBasicHandler( TupleSpace.DELETE, template);
while(true) {
// generate a new Transaction Identifier and tell TransactionManager
ct++;
myClientID = _clientID+"START"+ct;
int transID = transMgr.beginTrans(myClientID);
throwOutBeforeThis = System.currentTimeMillis();
// Create a subclass of Field that contains the current date/time
timestamp = new StaleTimestamp(throwOutBeforeThis);
template = new StaleTuple(timestamp);
SuperTuple delTuple = simpleDelete.command( _ts,
TupleSpace.DELETE,
template,
myClientID,
_communicator,
_user);
transID = transMgr.commitTrans(myClientID);
delay( (int)_howOften) ;
}
The sharp eyed reader may have noticed that something is wrong with the above description. The DELETE command only deletes Tuples that match the template so it should only delete Tuples that have the exact same timestamp instead of earlier timestamp. The reason that this works is that the time stamp field is an instance of StaleTimestamp which implements the matches() such that any timestamp that is less than the template timestamp will return true.
TupleSpace ts = new TupleSpace("xmlTest", server);
String xml = "<?xml version="1.0"?><MESSAGE>Hello World!</MESSAGE>";
Field xmlF = new XMLField(xml);
Tuple xmlT = new Tuple("myXML Tuple",xmlF);
ts.write(xmlT);
...
Currently, the XMLField constructor requires that you pass in the entire
content of the XML document as a single String. This limitation will
be fixed in the full release.
We should mention briefly the internals of the XML support. When a new tuple is written to a TSpace containing a XMLField, the XML string content is passed off and converted into a TupleTree. A TupleTree is essentially a tree of tuples, which as a whole mirrors the DOM (Document Object Model) tree of the XML document. Tuples are analogous to a node in the XML DOM tree. Each tuple is an instance of XMLTuple and contains a XTuple object. The XTuple object is an encapsulation of the XML specific data for a given node, and contains TupleID references to the DOM node's parents and siblings. For information on the exact contents of the XTuple object, see the JavaDoc.
To make a query on existing XML documents which have been written to the Space, we use the same query structure that supported AND, OR, and Index queries. For example, this code excerpt executes a simple query on the set of XML documents, assuming the previous code block has been executed:
...
String xql = "/MESSAGE";
Tuple result = ts.scan(new XMLQuery(xql));
System.out.println("Result tuples: "+result);
...
The Tuples which are returned are the Tuples that contain an XMLField that describes
an XML document that has one or more nodes that match the XQL query.
We need to explain here the exact XML query syntax supported by TSpaces. In this version of XML query support, we've decided to support a subset of the XQL language specification. XQL is a path expression based query language proposed to the W3C query workshop. For detailed information on XQL, check out the XQL FAQ. While reading the FAQ, please keep in mind that TSpaces only supports the core subset of XQL functionality, namely, those of Tagnames, Tagvalues, Descendants, and Attribute constraints.
Put another way, some of the more notable XQL functionality that is missing from TSpaces include:
To make the TSpace XML queries more real, here are a couple of sample
queries and what they do:
| /ADDRESS | Return all occurrences of the ADDRESS tag where it is anchored to the root of the document. |
| ADDRESS//ZIP | Return all occurrences of the ZIP tag where it is an eventual descendant of the ADDRESS tag, which occurs in the same document, but is not required to be anchored to the root. |
| ADDRESS/STREET='CHANNING WAY' | Return all occurrences of the STREET tag where its content is equal to the 'CHANNING WAY' string, and it is a direct child of the ADDRESS tag. |
| /*/ADDRESS[@TYPE='temporary']/CITY | Return all occurrences of the CITY tag where its parent node is ADDRESS, with an attribute of TYPE with value 'temporary'. The ADDRESS tag must have a parent which is anchored to the root of the document. |
In order to use the new XML support, the server needs to have access to a XML parser. We have used the XML Parser that is available on the IBM Alphaworks site. To download the xerces.jar file that you will require, go to the http://www.alphaWorks.ibm.com/tech/xml site and request the download option. Specify the XML4J-bin_3_0_1 release (or later) and download the zip file. Then extract the xerces.jar file and save it somewhere convenient. To make it accessible to the server, this jar file needs to be in the classpath used when you start the server. If you are using the distributed tspaces.bat file to start the server, you can simply specify the JAR_XML environment variable.
set JAR_XML=c:\xml\xerces.jar bin\tspaces
yourname/ tspaces/ ... lib/ tspaces.jar tspaces_client.jarLet's assume that this is a Unix system and you installed it in the /u/joe directory. The important file that you need to worry about is then /u/joe/tspaces/lib/tspaces_client.jar which contains the TSpaces client class files.
If you are using the Sun JDK without any IDE, you need to add the above jar file to the Java CLASSPATH environment variable or specify it for both the "javac" and "java" command line. For example:
export CLASSPATH=.:/u/joe/tspaces/lib/tspaces_client.jar
javac yourfile.java
java yourfile
or
javac -classpath .:/u/joe/tspaces/lib/tspaces_client.jar yourfile.java
java -cp .:/u/joe/tspaces/lib/tspaces_client.jar yourfile
If you are using any of the hundreds of IDEs like IBM Visual Age, Symantec Cafe, Kawa etc,
then you would follow their instructions for integrating jar files into the environment.
However, just because this is distributed in a jar file does not imply that this is
setup as a JavaBean, it's not.
Note: There are certain client services such as the AddHandler facility that require access to TSpaces server classes. For those cases, you should add tspaces.jar to the CLASSPATH.
If you write your own script or invoke it directly, you have to ensure that the CLASSPATH is set correctly so that the jar files that contain the Server code is available. Let's assume that this is a WinNT system and you installed TSpaces in the c:\java directory. The following should be sufficient to run the TSpaces server.
set CLASSPATH=c:\java\tspaces\classes;c:\java\tspaces\lib\tspaces.jar
java com.ibm.tspaces.server.TSServer [options]
There are a number of options that can be specified when starting the server.
Options:
[-a password] Password for sysadmin userid
[-b] Boot without restoring TupleSpaces
[-B] Boot without restoring TupleSpaces or User/Group status
[-c ConfigurationFile] specify location of the Configuration file
[-d checkpointDirectory] specify a checkpoint directory
[-p port#] specify a port number [default 8200]
[-i interval] specify a checkpoint interval
[-D] Turn on Debug output
[-A] Allow Admin actions via http
[-S] Start the HTTP debug interface
Note that the spaces after the option flags are needed
All of the options can be specified by the Configuration file. If the
-c option is not specified, it will look for a "tspaces.cfg" in the
current directory and if still not found, then it will run with a set of hard-coded defaults.
A sample tspaces.cfg file is distributed with the system that contains comments about the options.
So you should ensure that the CLASSPATH that is used when starting the server, includes the directory where the client class files are placed (usually the current directory at compile time or specified via "java -d classdir").
Also, once the class file is loaded by the server, that version continues to be used. So if you correct a bug in the client class and recompile, you may need to restart the server. Of course, use of the Serialver option will make this unnecessary. See the User Objects Example for an example of this.
Another consideration with the above, is that when you allow user objects to be sent to the server, then certain methods of those objects will be invoked in the server Virtual Machine. This may be a security problem and/or a reliability problem. In general, if you have a TSpaces server with sensitive data in it and/or reliability is important, you probably want to have tight control over the placement of client Java class files into the Server library. The earlier section on User Objects describes how one can bypass these problems with Object PreSerialization.
The server maintains a Hashtable of Userids and password keys. These password keys are the passwords from the user that have been turned into a digest by the SHA algorithm that is part of java.security and then the results turned into a BigInteger. The user/password combinations can either be read from the config file (not recommended) or entered via the Admin application. The Hashtable is currently written in a tuple to the Admin space and also to a file for backup in case the server does not have checkpointing or is rebooted.
The client code in TupleSpace takes the password supplied on the constructor and turns it into a BigInteger and then sends the BigInteger over the socket where it is compared to the BigInteger maintained by the server. So since the SHA digest is non-reversable, anyone seeing the socket data or looking at the file on disk, would not be able to determine the original password.
The above enables the client to securely tell the Server who they are. If not specified by the client, then the userid of "anonymous" is used.
Although currently the Userid and group structure is managed by TSpaces, the design is such that in the future, the User/Group configuration could be interfaced to the installation's User/Group setup for the Operating System where the T Space server is running.
The initial hierarchy group and user hierarchy is built from the Server Configuration file. the first time that the server is started or when the "-B" operand is specified on startup. If you look at the tspaces.cfg file that is distributed with TSpaces, you will see that it sets up a very basic hierarchy with the top level group users that contains a subgroup of AdminGroup and the users sysadmin, guest and anonymous. The comments in the file will hopefully show how one could enhance this. Based on the configuration file, a TsACLDBase object is built that contains a linked list that represents the User/Group hierarchy and this object is written to the Admin Space on the server.
Also a DefaultACL is built based on the configuration file. The default ACL is the ACL used for any Space that is created without specifying an ACL. If there is no config file, then we build an ACL that gives Read,write authority to group Users and read,write,admin authority to sysadmin. The CreateAcl is also built based on the configuration file. It is used to control who has permission to create a new space. The default is to grant all users, including anonymous, the permission to create Spaces.
The default ACLs and any ACLs that are created when a new Space is created are written and maintained in the Admin Space.
cd your_tspaces_directory bin\admin.bat [host] [port] [userid] [password]This application uses the Java JFC "Swing" classes ("Swing 1.1"). These are included with Java 1.2 but if you have Java 1.1 then you must install the Swing classes yourself and the SWING_HOME environment variable must be specified. We apologize in advance for slow load time of the application, it takes awhile to load the Swing classes. Also this is work in process, so it is not as easy to use this application as we would have liked. One hint is that it does use popup menus, so click the mouse button assigned to "popup" on a user or group entry. That will bring up the menu options to add or delete users or groups. Clicking it on an Acl permission entry will allow you to modify or add new AclEntry objects.
The user/group hierarchy information and the user/password information is stored in the TSpaces Admin space. But because you may want to run the server in non Persistent mode, it is also backed up into files so you will not lose your user/group information when you restart. These backup files (acldb.ser and password.ser) are stored in the current directory. If you want to completely start from scratch and get the initial ACL information from the configuration file then you should specify the -B flag on the command line.
A new interface between the client and the Server is available for the case where the client and the server are running on the same machine and under the same Java Virtual Machine. In this case, the client calls are directly implemented by the Server without the TCPIP socket overhead. Under certain circumstances, it can also reduce the overhead of Object serialization.
To do this the client must start the server with the following code:
TSServer ts = new TSServer(); Thread serverThread = new Thread( ts, "TSServer" ); serverThread.start();The above TSServer default constructor will let all of the command line options have the default values, including having the default tspaces.cfg file in the current directory. You can refer to the JavaDoc API for TSServer to see how you would specify other options for the server.
Now to use the new high performance interface that avoids all TCPIP socket overhead, you add the following line to your client code:
TupleSpace.setTSCmdImpl("com.ibm.tspaces.TSCmdLocalImpl");
The rest of your code is unchanged. It should be noted that this special interface will be used only by this client. Any other clients on another system or even on this system but under a different JVM will continue to use the socket interface to communicate with this server.
The local interface can avoid much of the overhead of Object Serialization under some circumstances. It important that the objects stored in the server be independent from the objects that the client is manipulating. If the client and the server are both running under the same JVM, then this means that when an object is transfered from the client to the server (or viceversa) that the server gets a copy of the object. If the Tuple consists of only String and Number fields, then this copy is done by a call to the Tuple.clone method which is relatively efficient. Otherwise, the copy is made by calling the same Object Serialization routines that are used when the client and server talk over a socket interface.
The bottom line is: if you have an application where the Tuples consist of only String and Number fields and you have a client that needs high performance and can run on the same system as the TSpaces server, then using this special local interface may be worthwhile.
http://hostname:8201/debug
[HTTPServer] HTTPServerSupport = true HttpPort = 8201 # Specify the directory where downloadable class files are kept ClassesDirectory = c:\tspaces\