One very useful feature of Codename One is its Storage class, which provides a cross-platform key-value store that can be used to store simple data (e.g. Strings, Integers, Doubles), large binary data (e.g. byte arrays of encoded movies or images), and custom data types (i.e. your own objects). Storage is not shared between applications so it is like your own persistent Hashtable that allows you to store anything you might need for your application.
The API is simple. It provides methods to read objects, write objects, delete objects, check for object existence, and listing objects that are currently stored in storage. All lookups are based on key-value lookups.
A simple example:
Storage s = Storage.getInstance();
// Save the "Hello World" string to storage
s.writeObject("mystring", "Hello World");
// Read my "Hello World" string back from storage
String hello = (String)s.readObject("mystring");
// Delete my string from storage
s.deleteStorageFile("mystring");
Just as we stored a String, we could have stored a Vector of Strings, or a Hashtable of key-value pairs, or a tree of Vectors and nested Hashtables. The only caveat is that the Vectors and Hashtables can only contain objects that can be externalized.
What Can Be Externalized?
I don’t have a definitive list of what can be externalized in Codename One, but in general, you can externalize:
- Primitive types (e.g. int, float, long, double, byte, etc..)
- Arrays of primitive types (e.g. int[], float[], long[], double[], etc..)
- Strings
- Vectors
- Hashtables
- Objects implementing the Externalizable interface.
Hence, if you want to save your own custom objects in Storage, you need to implement the Externalizable interface. It is worth noting that you can’t simply implement the Serializable interface as you do in regular java. You need to implement Codename One’s externlizable interface that explicitly defines how to read and write the objects to/from a DataOutputStream/DataInputStream. This is due to the fact that Codename One doesn’t support reflection. In addition to implementing the Externalizable interface, you also need to register your class with CodenameOne (via the Util.register() method) so that it knows which class to use when deserializing your objects.
Saving Custom Types to Storage
As mentioned above, any object that you want to persist to Storage must implement the Externalizable interface. If you try to save objects that don’t implement this interface it will raise an exception. If you, subsequently try to read an object that hasn’t been registered with Codename One via the Util.register() method, then Storage.readObject() will simply fail silently and return null. This will occur, if *any* object in the graph that you are trying to read is not registered.
The Externalizable interface requires 4 methods:
- getVersion() - This should return the version of your object. This will be used to record the version of the object when it is written to storage. This value will be passed to your internalize() method when you read the object so that you can handle old serialization structures properly when you modify your class.
- getObjectId() - This should return a unique String ID for the class (not the object as the method name seems to indicate). This should match the id that is registered with Util.register() so that it knows which class to instantiate when loading objects from a DataInputStream. But you could use anything here, as long as you use the same ID in the Util.register() method.
- externalize() - This method should write your object to a DataOutputStream.
- internalize() - This method should read your object from a DataInputStream.
Your class should also include a public constructor that takes no arguments.
Example Class:
Let’s look at a simple example class for a user profile.
class Profile {
public String firstName, lastName;
public int age;
public List<String> emails = new Vector<String>();
}
Note that I’m making all of the members public for simplicity and to reduce code in this example. Normally you would probably make the members private and implement setter/getter methods to access them.
Now, let’s implement the Externalizable interface on this class:
class Profile implements Externalizable {
public String firstName, lastName;
public int age;
public List<String> emails = new Vector<String>();
@Override
public int getVersion() {
return 1;
}
@Override
public void externalize(DataOutputStream out) throws IOException {
Util.writeUTF(firstName, out);
Util.writeUTF(lastName, out);
out.write(age);
Util.writeObject(emails, out);
}
@Override
public void internalize(int version, DataInputStream in) throws IOException {
firstName = Util.readUTF(in);
lastName = Util.readUTF(in);
age = in.readInt();
emails = (List<String>)Util.readObject(in);
}
@Override
public String getObjectId() {
return "Profile";
}
}
I want to comment on a few things here that are important:
- The order in which we read members from the DataInputStream in the internalize() method must be exactly the same as the order in which we write them in the externalize() method.
- We use Util.writeUTF() to write Strings instead of the DataOutputStream’s writeUTF() method, because it handles null values. I.e. if you try to pass a null string to the DataOutputStream’s writeUTF() method, it will throw a NullPointerException.
- We use Util.readObject() Util.writeObject() for writing objects (like the Vector containing email addresses). DataInputStream/DataOutputStream don’t provide equivalents.
Reading and Writing Profiles
Now let’s test out our class:
Profile steve = new Profile();
steve.firstName = "Steve";
steve.lastName = "Hannah"
Storage s = Storage.getInstance();
s.writeObject("steve", steve);
Profile newSteve = (Profile)s.readObject("steve");
System.out.println("Profile first name : "+newSteve.firstName);
If you try to run this example you’ll get a NullPointerException when you try to access the firstName property of newSteve. If you retrace your steps, you’ll find that the object was written OK (you can use the Storage.listEntries() method to see what keys are stored in storage). It’s just that the line:
Profile newSteve = (Profile)s.readObject("steve");
returns null. This is because we forgot to register our Profile class with Util, so it didn’t know which class to use for deserialization. If we add the line:
Util.register("Profile", Profile.class);
at any point before we try to read the object from storage, then it will work as expected. This is a big gotcha.
Tip: If you are getting null values out of storage, you should make sure that you have registered classes for *ALL* objects that are being read, including nested objects.
Where to Place Registration Code?
I’m still sorting out where the best place is to store the code that registers a class with Util. Here are a few options:
- Explicitly register all classes that you are using inside your application’s controller (e.g. in the start() method). If your application is self contained, this may be the simplest way. However, if your app may be using classes from external libraries, you may find it difficult to identify all of the possible classes that you may need to retrieve from storage.
- Inside a static block for the class that implements the Externalizable interface. e.g.
public class Profile implements Externalizable { static { Util.register("Profile", Profile.class); } … }
This will work, if you have referenced the class from somewhere inside your code before you unserialize the object. However, if your object is nested and its class is not referenced directly from code, then this static block may not be run before the object is deserialized (which will result in readObject() returning null).
For example, we might load a Vector of Profiles like this:
Vector v = Storage.getInstance().readObject("profiles");
If we don’t explicitly reference Profile in our code, here, then this will fail (silently) because the Profile class will not be registered yet. However, if we first reference the Profile class, it will work. E.g.
Profile p = new Profile(); Vector v = Storage.getInstance().readObject("profiles");
- Other ideas? You can place the registration code anywhere you like. You just have to be aware of when/if your registration will be run vs when your objects are likely to be read from Storage.
Trouble Shooting
During my experiments with Storage, I ran across a few "gotchas" that you should watch out for:
- Object keys cannot be "paths". I.e., don’t include the "/" character in the key for write/readObject or you may get some unexpected results. E.g.
Don’t do:byte[] b = new byte[]{'a','b','c'}; Storage.getInstance().writeObject("bytes/foobar", b);
Or you will get an error like:
java.io.FileNotFoundException: /Users/shannah/.cn1/bytes/foobar (Not a directory)
I’m not sure if this is a bug, but it’s something to watch out for. You can use any other character you want. Just don’t use a slash!
- Make sure ALL objects in your hierarchy that you are saving implement the Externalizable interface (or are supported natively by CN1 eg. Vector, Hashtable, etc..)
- Make sure you have run Util.register() for ALL classes that will be read from storage before trying to read them from storage. If you do not do this, Storage.readObject() will fail silently, returning null.
- Use Util.writeUTF()/Util.readUTF() when writing/reading Strings in your externalize() method. Don’t use out.writeUTF() because this will throw a NullPointerException for null strings.
- Use Util.writeObject()/Util.readObject() for writing/reading objects inside your externalize()/internalize() methods. There is no equivalent in DataOutputStream/DataInputStream.