Hello World Java

Now we've defined the data we're going to be sharing, let's start with our first Java component, HelloWriter, which will write an Announcement object to working memory. The first step is to create the source file for the component in our java source directory. Create the file HelloWriter.java under subarchitectures/hello-world/src/java/helloworld. Next, edit HelloWriter.java to contain:

package helloworld;

import cast.architecture.ManagedComponent;

public class HelloWriter extends ManagedComponent {

}

This creates a component as a subclass of cast.architecture.ManagedComponent (the most commonly used CAST component that can read from and write to WM) but does nothing else.

Before we go any further, let's compile this component to check that everything's working so far. To do this we will use Apache Ant compile everything (this is overkill for a single file, but useful as we add more components).

To do this, first copy the "top level" ant build file build-toplevel.xml from CAST's template directory (the install prefix + share/cast/templates, e.g. /usr/local/share/cast/templates)into $TUTORIAL_ROOT and rename to build.xml. E.g.

cp /usr/local/share/cast/templates/build-toplevel.xml $TUTORIAL_ROOT/build.xml

Next, edit this file to include our new project. To do this open the newly created $TUTORIAL_ROOT/build.xml and add the following line to the end of "template" target (around line 24):

  <ant target="${target}" dir="subarchitectures/hello-world" />

Finally we must add a subarchitecture-specific build file for our tutorual subarchitecture. The easiest way to do this is to copy the CAST ant subarchitecture template file from the CAST template directory as follows:

cp /usr/local/share/cast/templates/build-template.xml $TUTORIAL_ROOT/subarchitectures/hello-world/build.xml

All that remains now is to build the project. Do this by running ant:

cd $TUTORIAL_ROOT
ant all

If this has been successful you should see a new file called output/classes/helloworld/HelloWriter.class. If you see an error similar to "taskdef class Slice2JavaTask cannot be found" make sure your CLASSPATH is setup as described in the installation guide (both the prerequisites section and step 7), i.e. make sure that the Ice and cast jar files are included in your CLASSPATH.

To complete our sanity check we should run our new component and see what happens. As the component doesn't do anything nothing should happen, but a successful nothing is better than an unsuccessful something! CAST systems are run using clients and servers, and are configured using a file called a CAST file which describes which components to run on which machines. Let's create a basic CAST file called subarchitectures/hello-world/config/helloworld.cast and enter the following lines:

HOST localhost

SUBARCHITECTURE hello-world
JAVA WM cast.architecture.SubarchitectureWorkingMemory
JAVA TM cast.architecture.AlwaysPositiveTaskManager
JAVA MG writer helloworld.HelloWriter

The first line defines the default host one which the components should be run. For this you can try the name of your machine or "localhost". After that we define a subarchitecture. The first two lines in this construct define the component classes used for the subarchitecture working memory (WM) and task manager (TM). The example above uses built-in components for these roles. The final line in the subarchitecture definition introduces our new component. The first part of the line tells CAST what language the component is written in. The second part tells CAST that it's a managed component (i.e. the class we inherited for our component). The third part defines a unique ID we can use to refer to our component. And the final part is the class that CAST will load to instantiate our component. For more information see the configuration instructions.

Now we've defined our system we must can run it. At this point you should also make sure that output/classes is included in your CLASSPATH, so that the CAST server can find the results of our compilation. Open two terminals and change directory to $TUTORIAL_ROOT in each of them. In the first terminal, run the cast-server executable. This should give you some output like the following:

vonnegut:tutorial nah$ cast-server
Java server: 1530
CPP server: 1531

The numbers tell you the process numbers of the underlying CAST servers. Finally run the cast-client executable, passing our new CAST file as the first argument:

vonnegut:tutorial nah$ cast-client subarchitectures/hello-world/config/helloworld.cast
CoSy Architecture Schema Toolkit. Release: 0.2.0

As our component doesn't do anything, you should see much else in terms of output. To stop the system running Ctrl-C the client script. This will send appropriate shutdown signals to all running components.

Having got this far we can be pretty confident that CAST is running fine and that our new component has been accepted into its bosom. Next we can extend our component to actually do something. For our tutorial we want HelloWriter to add an Announcement object to working memory for some other component to read. To do this, we must get the component to execute some code when it runs. Every component in CAST inherits a runComponent() member function which is called when the component is run. To use this, extend your HelloWriter class to include the following lines:

@Override
protected void runComponent() {
  println("Look out world, here I come...");
}

This code just prints out (using CAST's built in print method) a nice message so we know things are running. Recompile and run your component to verify that this works.

Next we need to create an instance of Announcement to write to working memory. We have already defined this object in Slice, but we now need to use this in Java. To do this we must use Ice's slice2java program to generate Java source code from our Slice definition file. Although you could do this by hand, Ice provides an ant task to do this for you . Open slice2java-template.xml from CAST's cmake directory, and paste the contents into your subarchitecture's build.xml file's slice task, replacing MYSLICEFILE with HelloWorldData as you go. E.g. we should change $TUTORIAL_ROOT/subarchitectures/hello-world/build.xml to contain:

<target name="slice" depends="prepare" description="generates source from slice">
  <slice2java tie="true" outputdir="${src.dir}">
    <fileset dir="${slice.dir}" includes="HelloWorldData.ice"/>
      <includepath>
        <pathelement path="${slice.dir}"/>
      </includepath>
  </slice2java>
</target>

This will create Java versions of the classes defined in our slice file. When you rebuild the project you should now see some output about "slice2java" and some extra compilations.

With this step complete we can now finally create an instance of Announcement to write to working memory. First off, include the newly generated class file:

import helloworld.Announcement;

Then, in runComponent() create an instance of Announcement with the message you want to send:

@Override
protected void runComponent() {
  Announcement ann = new Announcement("Hello World!");
}

The only remaining task for this component is to write the newly created object to working memory. All working memory entries are created with an ID string which should ideally be generated using newDataID(). The ID and the object are then sent to working memory via a call to addToWorkingMemory as demonstrated in the final line of runComponent (you'll also need an additional import):

import cast.AlreadyExistsOnWMException;

protected void runComponent() {
  Announcement ann = new Announcement("Hello World!");
  try {
    addToWorkingMemory(newDataID(), ann);
  } catch (AlreadyExistsOnWMException e) {
    e.printStackTrace();
  }
}

Now we have our writer component we need HelloReader to read the announcement that has been written to WM. Create the component by copying HelloWriter, but remove the runComponent() method (as we don't need it for this component). Compile the code to check that this new component is correct.

When data on working memory is changed (e.g. when a new Annoucement object is written to it) the working memory generates a new Working Memory Change event which is sent to all components that listening for such events. So, the first thing our HelloReader must do is register with the working memory to listen for the events it cares about. When a component is started up, before runComponent() is called (but after the constructor and configure), each component's start member function is called. It is here we will tell the WM what change events we're interested in. We do it here because it guarantees that we'll see any changes that are created by other components' runComponent methods, because start is always called before runComponent (for more information on methods are called when, see System Architecture).

To do this, we first add the start method to HelloReader.java:

@Override
protected void start() {
}

Next we need to tell working memory what we're interested in. We do this by passing CAST a WorkingMemoryChangeFilter which describes what WM changes the component is interested in, and a WorkingMemoryChangeReceiver which is a callback object that is called when events matching the filter occur. Although you can create change filters by hand, the class ChangeFilterFactory provides helper functions to create most commonly required filter types. For our HelloReader component, start by importing the classes:

//Factory functions for change filters
import cast.architecture.ChangeFilterFactory;
//Classes for receiving and managing changes
import cast.architecture.WorkingMemoryChangeReceiver;
import cast.cdl.WorkingMemoryChange;
import cast.cdl.WorkingMemoryOperation;

... then add the following line to the start function:

  addChangeFilter(ChangeFilterFactory.createLocalTypeFilter(Announcement.class, WorkingMemoryOperation.ADD), 
                                                            new WorkingMemoryChangeReceiver() {
                                                              public void workingMemoryChanged(WorkingMemoryChange _wmc) {
                                                                makeAnnouncement(_wmc);
                                                              }
                                                            });

This does the following things. ChangeFilterFactory.createLocalTypeFilter(Announcement.class, WorkingMemoryOperation.ADD) creates a working memory change filter that listens for additions to working memory (WorkingMemoryOperation.ADD) of the Announcement class (the first argument) in the working memory of this component's subarchitecture (ChangeFilterFactory.createLocalTypeFilter). If you used ChangeFilterFactory.createGlobalTypeFilter you could listen for matching events on any working memory in the whole system. The second argument to addChangeFilter creates a new change receiver object that calls a member function in a class. In this case we've told it to call the member function makeAnnouncement which we've yet to define. So, next we'll define this. In order to be called when a change occurs, the function must have a particular signature. It must return void and must accept and a single WorkingMemoryChange object, i.e.:

private void makeAnnouncement(WorkingMemoryChange _wmc) {
}

The input argument, WorkingMemoryChange contains a number of fields which describe the event that has occurred (these are matched to the filter to determine whether the change event should be received or not). We can use one of these, the address field, to access the changed data on working memory. The address is of type WorkingMemoryAddress which contains an id field and a subarchitecture field. In this case id is the string generated by HelloWriter using newDataID(). The subarchitecture field will contain the name of the current component's subarchitecture. This is "hello-world" as described above in the CAST file. With the address from the change event we can read the data from working memory. There are many ways to do this, but the easiest is to use getMemoryEntry. This accepts an address and returns an Ice smart pointer to the requested object. To use this, first add the following imports:

//For exceptions thrown by getMemoryEntry
import cast.UnknownSubarchitectureException;
import cast.DoesNotExistOnWMException;

Then add the following to makeAnnouncement:

  try {
    Announcement ann = getMemoryEntry(_wmc.address, Announcement.class);
  } catch (DoesNotExistOnWMException e) {
    e.printStackTrace();
  } catch (UnknownSubarchitectureException e) {
    e.printStackTrace();
  }

And finally we can print out our announcement in makeAnnouncement by adding:

  println("I'd like to make an announcement: " + ann.message);

At this point we have completed our code, but need to add our new component into our system to test it. To do this, add the following extra line to the end of the helloworld.cast CAST file:

JAVA MG reader helloworld.HelloReader

With this in place you should be able to re-run the system and see (in the server terminal):

CoSy Architecture Schema Toolkit. Release: 0.2.0
["writer": Look out world, here I come...]
["reader": I'd like to make an announcement: Hello World!]

If you see this, congratulations, you've written your first CAST system :)


Generated on Mon Jun 13 15:56:05 2011 for CoSy Architecture Schema Toolkit (CAST) by  doxygen 1.5.8