Andy Wilkinson wrote:

>At present (perhaps naively) this is the area of developing the editor which
>I foresee causing me most difficulty, although this may say more about my
>knowledge of serializing Java classes than the ease of the rest of the
>development work. Any insight you can give me into how you have used your
>placeholder classes, or pointers to places to read up about the topic would
>be greatly appreciated.

Andy,

You can find much of the info you need regarding Serialization in the
Java API docs. Look at both the Serializable and Externalizable
interfaces. Also, if you downloaded the JDK source with your package
have a look at the ObjectOutputStream/ObjectInputStream source. This
will give you a very good insight into the whole process.

Serialization enables you to read and write an object (and most
significantly, all objects in the tree beneath that object) to a Java
stream. If this stream is a file stream then your object ends up in a
persistent state and can be restored later (with its full object
subgraph of course). If the stream is a network socket then you can
transmit that object across the network where it can be instantly
reconstructed at the other end.

Before looking at Java's built-in serialization, let's serialize an
object ourselves. We'll create a simple AWT button and serialize it
(please excuse the 'catch-all' exception handling in all of these
examples, which is there for clarity. Oh, and if you're
color-blind...tough :)

import java.io.*;
import java.awt.*;

public class SerializedDemo {

   public static void main(String[] args) {

      Button b = new Button("Press Me");
      b.setBackground(Color.blue);
      b.setForeground(Color.red);

      DataOutputStream out = null;
      try {
         File f = new File("SerializedButton.demo");
         out = new DataOutputStream(new BufferedOutputStream(new
FileOutputStream(f)));
         out.writeUTF("java.awt.Button");
         out.writeUTF(b.getLabel());
         out.writeInt(b.getBackground().getRGB());
         out.writeInt(b.getForeground().getRGB());
         out.flush();
      } catch (Exception e) {
         System.out.println("IOError saving file " + e);
      } finally {
         try {
            out.close();
         } catch (Exception e2) {
         }
      }
   }
}

Okay, now we'll deserialize this button and load it into a GUI:

import java.io.*;
import java.awt.*;

public class LoadASerializedButton {

   public static void main(String[] args) {

      Button b = null;
      DataInputStream in = null;
      try {
         File f = new File("SerializedButton.demo");
         in = new DataInputStream(new BufferedInputStream(new
FileInputStream(f)));
         Class c = Class.forName(in.readUTF());
         b = (Button)c.newInstance();
         b.setLabel(in.readUTF());
         b.setBackground(new Color(in.readInt()));
         b.setForeground(new Color(in.readInt()));
         in.close();
      } catch (Exception e) {
          System.out.println("Error loading file: " + e);
          b = null;
      } finally {
         try {
            in.close();
         } catch (Exception e2) {
         }
      }

      if (b != null) {
         Frame f = new Frame();
         f.setLayout(new FlowLayout());
         f.add(b);
         f.setSize(200, 200);
         f.setVisible(true);
      }
   }
}

Note that in this second example we didn't use the code 'new Button()'.
The Java classloader used a String reference to create the desired
class.

As you may imagine, it's possible to create an interface which all
objects you want to serialize can implement. Then a single object
reader/writer can be used to handle serialization and deserialization.
Here's an example of such an interface:

import java.io.*;

public interface SerializableObject {

   public void writeObject(DataOutputStream out) throws IOException;

   public void readObject(DataInputStream in) throws IOException;

   public String getClassName();
}

Armed with this interface we can now write a class to load and save our
Java objects. This one is a filesaver/loader using a String path, but a
socket or URL version is very similar. Again a warning about exceptions
- in a true implementation you would throw exceptions back to the
caller.

import java.io.*;

public class FileSerializer {

   public void saveObject(SerializableObject obj, String filename) {

      DataOutputStream out = null;
      try {
         File f = new File(filename);
         out = new DataOutputStream(new BufferedOutputStream(new
FileOutputStream(f)));
         out.writeUTF(obj.getClassName());
         obj.writeObject(out);
         out.flush();
      } catch (Exception e) {
         System.out.println("Error saving object: " + e);
      } finally {
         try {
            out.close();
         } catch (Exception e2) {
         }
      }
   }

   public Object loadObject(String filename) {

      SerializableObject obj = null;
      DataInputStream in = null;
      try {
         File f = new File(filename);
         in = new DataInputStream(new BufferedInputStream(new
FileInputStream(f)));
         Class c = Class.forName(in.readUTF());
         obj = (SerializableObject)c.newInstance();
         obj.readObject(in);
         in.close();
      } catch (Exception e) {
         System.out.println("Object load error: " + e);
         obj = null;
      } finally {
         try {
            in.close();
         } catch (Exception e2) {
         }
      }
      return obj;
   }
}

We can now rewrite our simple Button so it implements this interface:

import java.io.*;
import java.awt.*;

public class SimpleButton extends Button implements SerializableObject {

   private static final String CLASS_SIMPLE_BUTTON = "SimpleButton";
   private static final int VERSION_ID = 0;

   public void writeObject(DataOutputStream out) throws IOException {

      out.writeInt(VERSION_ID);
      out.writeUTF(getLabel());
      out.writeInt(getBackground().getRGB());
      out.writeInt(getForeground().getRGB());
   }

   public void readObject(DataInputStream in) throws IOException {

      in.readInt();  //the version id. We're on version 0, so no
handling required
      setLabel(in.readUTF());
      setBackground(new Color(in.readInt()));
      setForeground(new Color(in.readInt()));
   }

   public String getClassName() {

      return CLASS_SIMPLE_BUTTON;
   }
}

Note that we've introduced the concept of versioning here. Suppose I
release my app to customers all around the world and a year later modify
the SimpleButton class in a new release. What happens if someone tries
to load an old version of this class into a VM with the new version in
the classpath? Versioning can take care of this, so in your readObject()
method you'll have code like:

   versionId = in.readInt();
   switch (versionId) {
      case 0: //read with version 0 code
              break;
      case 1: //read with version 1 code
              break;
   }

... and so on. This means your file format is good for a long, long
time.

If you're not asleep yet, we'll write a final test program which creates
a SimpleButton, serializes it, then loads it straight back for display:

import java.io.*;
import java.awt.*;

public class SimpleButtonTest {

   public static void main(String[] args) {

      SimpleButton b = new SimpleButton();
      b.setLabel("Press Me");
      b.setBackground(Color.blue);
      b.setForeground(Color.red);

      FileSerializer ser = new FileSerializer();
      ser.saveObject(b, "testButton.btn");

      //...pretend 6 months has passed by...

      SimpleButton b2 = (SimpleButton)ser.loadObject("testButton.btn");
      Frame f = new Frame();
      f.setLayout(new FlowLayout());
      f.add(b2);
      f.setSize(200, 200);
      f.setVisible(true);
   }
}

I'm in danger of code overload here, so I won't go on to show how you
can save and load whole trees of objects. It's very simple - if you
imagine a Panel which implements SerializableObject and which has
several SimpleButton children, then the writeObject() method of this
Panel will write its own fields to the OutputStream and then call the
writeObject() method of each of its child SimpleButtons. Hopefully you
can now appreciate how this process is of direct relevance to J3D and
the saving of entire scene graphs, and also to transmitting them across
the Web via Java sockets.

Java 1.1 basically took much of the workload out of this process by
introducing a new interface, 'Serializable', to take care of this in an
automatic way. New interfaces were introduced (stress *interfaces*, hint
hint), namely ObjectInput and ObjectOutput, which have direct
implemetations in ObjectInputStream and ObjectOutputStream. These
classes have a readObject/writeObject method which takes care of all the
serialization process. A Java class implementing Serializable can let
these classes take care of all the serialization process, and
tremendously complex object graphs can be serialized.

In 1.1 the serialization process was somewhat flawed because the
read/write methods of these streams were inefficient. In Java 1.2 a new
save/load protocol was introduced which uses byte blocks rather than
single-byte read/writes to save data. This has made the whole process
far more powerful. Having said that I must admit that we generally still
use our own routines to serialize objects rather than
ObjectInput/OutputStream, because there's still an overhead with these
classes. However, that gap is narrowing and we've written our classes so
that a simple name change will immediately convert them to straight
serialization. I expect this will maybe happen under Java 1.3.

Okay, before I get accused of being off-topic, how does all this relate
to Java3D? To test serialization of J3D classes we need a reader/writer
class which will use core Java serialization. Here it is, very similar
to our FileSerializer above:

import java.io.*;

public class FileSerializer2 {

   public void saveObject(Object obj, String filename) {

      ObjectOutputStream out = null;
      try {
         File f = new File(filename);
         out = new ObjectOutputStream(new BufferedOutputStream(new
FileOutputStream(f)));
         out.writeObject(obj);
         out.flush();
      } catch (Exception e) {
         System.out.println("Error saving object: " + e);
      } finally {
         try {
            out.close();
         } catch (Exception e2) {
         }
      }
   }


   public Object loadObject(String filename) {

      Object obj = null;
      ObjectInputStream in = null;
      try {
         File f = new File(filename);
         in = new ObjectInputStream(new BufferedInputStream(new
FileInputStream(f)));
         obj = in.readObject();
         in.close();
      } catch (Exception e) {
         System.out.println("Error reading object: " + e);
         obj = null;
      } finally {
         try {
            in.close();
         } catch (Exception e2) {
         }
      }
      return obj;
   }
}

To test whether this saver/loader works, let's run it with a Button
first. Not our SimpleButton, but a straight AWT button:

import java.io.*;
import java.awt.*;

public class CoreSerializationTest {

   public static void main(String[] args) {

      Button b = new Button();
      b.setLabel("Press Me");
      b.setBackground(Color.blue);
      b.setForeground(Color.red);

      FileSerializer2 ser = new FileSerializer2();
      ser.saveObject(b, "testButton2.btn");

      //...pretend 6 months has passed by...

      Button b2 = (Button)ser.loadObject("testButton2.btn");
      Frame f = new Frame();
      f.setLayout(new FlowLayout());
      f.add(b2);
      f.setSize(200, 200);
      f.setVisible(true);
   }
}

Unless I've screwed up (a more than likely scenario!) you should get
this serialized object to load into your GUI. If you look at the API for
Button though you'll see that it doesn't directly implement
Serializable. However its superclass Component does, and so serializing
this Button will work.

Right, let's try this with a J3D object. We'll use a TriangleArray:

import javax.media.j3d.*;

public class FirstJ3DTest {

   public static void main(String[] args) {

      int n = 3;
      float[] coords = new float[n];
      TriangleArray ta = new TriangleArray(n,
GeometryArray.COORDINATES);
      ta.setCoordinates(0, coords);
      FileSerializer2 ser = new FileSerializer2();
      ser.saveObject(ta, "triangleArray.test");
   }
}

You can compile this code without error, but when you run it you'll get
a 'NotSerializableException' runtime error. This is because
TriangleArray does not implement Serializable, nor do any of its
superclasses (GeometryArray, Geometry, NodeComponent and
SceneGraphObject).

Fine, we'll fix this by creating a new class which extends TriangleArray
and implements Serializable:

import java.io.*;

public class ExtendedTriangleArray extends TriangleArray implements
Serializable {

   public ExtendedTriangleArray(int vertexCount, int vertexFormat) {

      super(vertexCount. vertexFormat);
   }
}

Job done. Easy. Let's run the code again:

import javax.media.j3d.*;

public class SecondJ3DTest {

   public static void main(String[] args) {

      int n = 3;
      float[] coords = new float[n];
      ExtendedTriangleArray ta = new ExtendedTriangleArray(n,
GeometryArray.COORDINATES);
      ta.setCoordinates(0, coords);
      FileSerializer2 ser = new FileSerializer2();
      ser.saveObject(ta, "triangleArray.test");
   }
}

You can run this code without complaint from the VM. The file will
apparently save without hitch. However, when you try and deserialize the
file back again you'll get an 'InvalidClassException'. This is thrown
because TriangleArray doesn't have a no-arg constructor, i.e, there's
no:

   public TriangleArray() {}

This is crucial to serialization. Even if a superclass doesn't implement
Serializable it's still possible for a subclass to serialize its parent
if the classloader is able to call a no-arg constructor at runtime. If
you go and look at the Swing API library you'll see that every class has
a no-arg constructor. This is also fundamental to JavaBeans, so that
component builders can create a no-arg version of a class.

Inavriably with serialization you need the ability to set object
properties *after* that object has been constructed. With triangle array
you can't do this because the only place you can set the vertex count
and vertex format is in the constructor. There isn't a setVertexCount()
method, nor a setVertexFormat().

There are a number of ways to serialize a TriangleArray by proxy, and
the simplest to demonstrate is one where you create a class which simply
holds a TriangleArray as a class member. There's a slight glitch with
this though as I'm sure you'll realise if you look at this class:

import javax.media.j3d.*;
import java.io.*;

public class ProxyTriangleArray implements Serializable {

   private TriangleArray ta;

   public void setTriangleArray(TriangleArray ta) {

      this.ta = ta;
   }


   public TriangleArray getTriangleArray() {

      return ta;
   }
}

If you try and serialize this class you'll get runtime errors for the
simple reason that the TriangleArray member can't be serialized.
Fortunately however (see the docs) serialization allows you to implement
your own readObject()/writeObject methods to handle the serialization
process yourself. You must follow the exact signatures of these methods,
which strangely (but cleverly because you get object security) are
*private* methods, even though the ObjectStream can access them.

So here's our final version, which can serialize out and load back a J3D
TriangleArray. It's incomplete because it only handles coordinates, but
I'm sure you can see that by testing the vertex format and writing
booleans to the stream we can handle all possible vertex formats without
difficulty.

import javax.media.j3d.*;
import java.io.*;

public class ProxyTriangleArray implements Serializable {

   private TriangleArray ta;

   public void setTriangleArray(TriangleArray ta) {

      this.ta = ta;
   }


   public TriangleArray getTriangleArray() {

      return ta;
   }


   private void writeObject(ObjectOutputStream out) throws IOException {

      int n = ta.getVertexCount();
      float[] f = new float[n];
      ta.getCoordinates(0, f);
      out.writeInt(n);
      out.writeInt(ta.getVertexFormat());
      out.writeObject(f);
   }


   private void readObject(ObjectInputStream in) throws IOException {

      int vertexCount  = in.readInt();
      int vertexFormat = in.readInt();
      ta = new TriangleArray(vertexCount, vertexFormat);
      try {
         ta.setCoordinates(0, (float[])in.readObject());
      } catch (ClassNotFoundException e) {
         throw new IOException("Vertex array's gone AWOL");
      }
   }
}


And a final test:

import javax.media.j3d.*;

public class ThirdJ3DTest {

   public static void main(String[] args) {

      int n = 3;
      float[] coords = new float[n];
      coords[0] = 1.2345f;
      coords[1] = 9.9998f;
      coords[2] = 0.0054f;
      TriangleArray ta = new TriangleArray(n,
GeometryArray.COORDINATES);
      ta.setCoordinates(0, coords);
      ProxyTriangleArray pta = new ProxyTriangleArray();
      pta.setTriangleArray(ta);
      FileSerializer2 ser = new FileSerializer2();
      ser.saveObject(pta, "triangleArray.test");

      //pretend 6 months have gone by. It must seem like that anyway by
now

      ProxyTriangleArray pta2 =
(ProxyTriangleArray)ser.loadObject("triangleArray.test");
      TriangleArray ta2 = pta.getTriangleArray();
      System.out.println(ta2);
      n = ta2.getVertexCount();
      System.out.println("Vertex count = " + n);
      float[] f = new float[n];
      ta2.getCoordinates(0, f);
      for (int i=0; i<f.length; i++) {
         System.out.println("f[" + i + "] = " + f[i]);
      }
   }
}

So I hope this sort of answers your question. Obviously it would be so
much easier if the J3D API had all of this built-in, in which case the
only problem would be ensuring that an object is detached from the scene
graph before trying to serialize it.

With luck we'll see serialization implemented in a future release. It's
a must for JavaBeans, and it's JavaBean-like 3D objects coupled with
serialization that is going to make everyone take a very close look at
Java3D.

Andrew Moulden

===========================================================================
To unsubscribe, send email to [EMAIL PROTECTED] and include in the body
of the message "signoff JAVA3D-INTEREST".  For general help, send email to
[EMAIL PROTECTED] and include in the body of the message "help".

Reply via email to