Databases and MIDP, Part 5: Searching a Record Store

   
By Eric Giguere, June 2004  

In Part 4 of this series, you learned how to traverse a record store, sort records into useful order, and use filters to select desired records. This article explores the various strategies for finding one or more records that meet criteria you specify.

Searching Strategies

Obviously, the simplest way to search for a particular record or set of records is to use a filter. The filter needs to know how the data is stored in a record, so you'll want to reuse your data mapping classes wherever possible. In Part 3, for example, we defined FieldList and FieldBasedStore classes to handle the reading and writing of arbitrary data to a record store. With a bit of refactoring, we can move the code that isn't specific to a record store into a new base class, FieldBasedRecordMapper:

package j2me.rms;

import java.io.*;
import javax.microedition.rms.*;
import j2me.io.*;

// A base class for writing and reading arbitrary
// data as defined by a FieldList

public abstract class FieldBasedRecordMapper {

    // Some useful constants

    public static Boolean TRUE = new Boolean( true );
    public static Boolean FALSE = new Boolean( false );

    // Markers for the types of string we support

    private static final byte NULL_STRING_MARKER = 0;
    private static final byte UTF_STRING_MARKER = 1;

    // Constructs the mapper for the given list

    protected FieldBasedRecordMapper(){
    }

    // Prepares for input by setting the data buffer.

    protected void prepareForInput( byte[] data ){
        if( _bin == null ){
            _bin = new DirectByteArrayInputStream( data );
            _din = new DataInputStream( _bin );
        } else {
            _bin.setByteArray( data );
        }
    }

    // Prepares the store for output. The streams are reused.

    protected void prepareForOutput(){
        if( _bout == null ){
            _bout = new DirectByteArrayOutputStream();
            _dout = new DataOutputStream( _bout );
        } else {
            _bout.reset();
        }
    }

    // Reads a field from the buffer.

    protected Object readField( int type ) throws IOException {
        switch( type ){
            case FieldList.TYPE_BOOLEAN:
                return _din.readBoolean() ? TRUE : FALSE;
            case FieldList.TYPE_BYTE:
                return new Byte( _din.readByte() );
            case FieldList.TYPE_CHAR:
                return new Character( _din.readChar() );
            case FieldList.TYPE_SHORT:
                return new Short( _din.readShort() );
            case FieldList.TYPE_INT:
                return new Integer( _din.readInt() );
            case FieldList.TYPE_LONG:
                return new Long( _din.readLong() );
            case FieldList.TYPE_STRING: {
                byte marker = _din.readByte();
                if( marker == UTF_STRING_MARKER ){
                    return _din.readUTF();
                }
            }
        }

        return null;
    }

    // Converts an object to a boolean value.

    public static boolean toBoolean( Object value ){
        if( value instanceof Boolean ){
            return ((Boolean) value).booleanValue();
        } else if( value != null ){
            String str = value.toString().trim();

            if( str.equals( "true" ) ) return true;
            if( str.equals( "false" ) ) return false;

            return( toInt( value ) != 0 );
        }

        return false;
    }

    // Converts an object to a char.

    public static char toChar( Object value ){
        if( value instanceof Character ){
            return ((Character) value).charValue();
        } else if( value != null ){
            String s = value.toString();
            if( s.length() > 0 ){
                return s.charAt( 0 );
            }
        }

        return 0;
    }

    // Converts an object to an int. This code
    // would be much simpler if the CLDC supported
    // the java.lang.Number class.

    public static int toInt( Object value ){
        if( value instanceof Integer ){
            return ((Integer) value).intValue();
        } else if( value instanceof Boolean ){
            return ((Boolean) value).booleanValue() ? 1 : 0;
        } else if( value instanceof Byte ){
            return ((Byte) value).byteValue();
        } else if( value instanceof Character ){
            return ((Character) value).charValue();
        } else if( value instanceof Short ){
            return ((Short) value).shortValue();
        } else if( value instanceof Long ){
            return (int) ((Long) value).longValue();
        } else if( value != null ){
            try {
                return Integer.parseInt( value.toString() );
            }
            catch( NumberFormatException e ){
            }
        }

        return 0;
    }

    // Converts an object to a long. This code
    // would be much simpler if the CLDC supported
    // the java.lang.Number class.

    public static long toLong( Object value ){
        if( value instanceof Integer ){
            return ((Integer) value).longValue();
        } else if( value instanceof Boolean ){
            return ((Boolean) value).booleanValue() ? 1 : 0;
        } else if( value instanceof Byte ){
            return ((Byte) value).byteValue();
        } else if( value instanceof Character ){
            return ((Character) value).charValue();
        } else if( value instanceof Short ){
            return ((Short) value).shortValue();
        } else if( value instanceof Long ){
            return ((Long) value).longValue();
        } else if( value != null ){
            try {
                return Long.parseLong( value.toString() );
            }
            catch( NumberFormatException e ){
            }
        }

        return 0;
    }

    // Writes a field to the output buffer.

    protected void writeField( int type, Object value )
                                 throws IOException {
        switch( type ){
            case FieldList.TYPE_BOOLEAN:
                _dout.writeBoolean( toBoolean( value ) );
                break;
            case FieldList.TYPE_BYTE:
                _dout.write( (byte) toInt( value ) );
                break;
            case FieldList.TYPE_CHAR:
                _dout.writeChar( toChar( value ) );
                break;
            case FieldList.TYPE_SHORT:
                _dout.writeShort( (short) toInt( value ) );
                break;
            case FieldList.TYPE_INT:
                _dout.writeInt( toInt( value ) );
                break;
            case FieldList.TYPE_LONG:
                _dout.writeLong( toLong( value ) );
                break;
            case FieldList.TYPE_STRING:
                if( value != null ){
                    String str = value.toString();
                    _dout.writeByte( UTF_STRING_MARKER );
                    _dout.writeUTF( str );
                } else {
                    _dout.writeByte( NULL_STRING_MARKER );
                }
                break;
        }
    }

    // Writes a set of fields to the output stream.

    protected byte[] writeStream( FieldList list, 
                                  Object[] fields )
                                       throws IOException {
        int count = list.getFieldCount();
        int len = ( fields != null ? fields.length : 0 );

        prepareForOutput();

        for( int i = 0; i < count; ++i ){
            writeField( list.getFieldType( i ),
                        ( i < len ? fields[i] : null ) );
        }

        return _bout.getByteArray();
    }

    private DirectByteArrayInputStream  _bin;
    private DirectByteArrayOutputStream _bout;
    private DataInputStream             _din;
    private DataOutputStream            _dout;
}

Now we extend FieldBasedRecordMapper to create another abstract class, FieldBasedFilter, the base class for our filters:

package j2me.rms;

import javax.microedition.rms.*;

// A record filter for filtering records whose data
// is mapped to a field list. The actual filter will
// extend this class and implement the matchFields
// method appropriately.

public abstract class FieldBasedFilter
                      extends FieldBasedRecordMapper
                      implements RecordFilter {

    // Constructs the filter. The optional byte
    // array is an array that we want ignored,
    // usually the first record in the record store
    // where we store the field information.

    protected FieldBasedFilter(){
        this( null );
    }

    protected FieldBasedFilter( byte[] ignore ){
        _ignore = ignore;
    }

    // Compares two byte arrays.

    private boolean equal( byte[] a1, byte[] a2 ){
        int len = a1.length;

        if( len != a2.length ) return false;

        for( int i = 0; i < len; ++i ){
            if( a1[i] != a2[i] ) return false;
        }

        return true;
    }

    // Called to filter a record.

    public boolean matches( byte[] data ){
        if( _ignore != null ){
            if( equal( _ignore, data ) ) return false;
        }

        prepareForInput( data );
        return matchFields();
    }

    // The actual filter implements this method.

    protected abstract boolean matchFields();

    private byte[] _ignore;
}

Suppose we define a FieldList instance like so:

...
FieldList empFields = new FieldList( 5 );

empFields.setFieldType( 0, FieldList.TYPE_INT );
empFields.setFieldName( 0, "ID" );
empFields.setFieldType( 1, FieldList.TYPE_STRING );
empFields.setFieldName( 1, "Given Name" );
empFields.setFieldType( 2, FieldList.TYPE_STRING );
empFields.setFieldName( 2, "Last Name" );
empFields.setFieldType( 3, FieldList.TYPE_BOOLEAN );
empFields.setFieldName( 3, "Active" );
empFields.setFieldType( 4, FieldList.TYPE_CHAR );
empFields.setFieldName( 4, "Sex" );
...

A filter that matches a specific last name looks like this:

package j2me.rms;

import java.io.IOException;

// A filter that matches a specific last name
// in an employee record.

public class MatchLastName extends FieldBasedFilter {
    public MatchLastName( String name ){
        this( name, null );
    }

    public MatchLastName( String name, byte[] ignore ){
        super( ignore );
        _name = name;
    }

    protected boolean matchFields(){
        try {
            readField( FieldList.TYPE_INT );
            readField( FieldList.TYPE_STRING );

            String ln = (String)
                       readField( FieldList.TYPE_STRING );

            return ln.equals( _name );
        }
        catch( IOException e ){
            return false;
        }
    }

    private String _name;
}

Finding the matching records is a simple matter:

...
RecordStore employees = ... // list of employees
RecordFilter lname = new MatchLastName( "Smith" );
RecordEnumeration enum =
      employees.enumerateRecords( lname, null, false );

while( enum.hasNextElement() ){
    int id = enum.nextRecordId();
    ... // etc. etc./
}

enum.destroy();
...

Code carefully so that you can avoid unpacking a record any more than necessary. One approach you can take is to cache recently accessed records in memory. For example, each time a filter matches a record, unpack the record and store its unpacked form - often an object representing a single entity, like an employee - in the cache. Use its record ID as the key - you'll need to store the ID in the record, of course. When you traverse the enumeration, check the cache for the unpacked record before accessing the underlying record store.

In fact, it may be simpler to use the enumeration as a way to collect and unpack matching records into a separate list. Consider the Contact class we defined in Part 2, with its simple toByteArray() and fromByteArray() methods for mapping instances to and from byte arrays:

package j2me.example;

import java.io.*;

// The contact information for a person

public class Contact {
    private String _firstName;
    private String _lastName;
    private String _phoneNumber;

    public Contact(){
    }

    public Contact( String firstName, String lastName,
                    String phoneNumber )
    {
        _firstName = firstName;
        _lastName = lastName;
        _phoneNumber = phoneNumber;
    }

    public String getFirstName(){
        return _firstName != null ? _firstName : "";
    }

    public String getLastName(){
        return _lastName != null ? _lastName : "";
    }

    public String getPhoneNumber(){
        return _phoneNumber != null ? _phoneNumber : "";
    }

    public void setFirstName( String name ){
        _firstName = name;
    }

    public void setLastName( String name ){
        _lastName = name;
    }

    public void setPhoneNumber( String number ){
        _phoneNumber = number;
    }

    public void fromByteArray( byte[] data ) 
                                throws IOException {
        ByteArrayInputStream bin = 
                        new ByteArrayInputStream( data );
        DataInputStream din = new DataInputStream( bin );

        fromDataStream( din );
        din.close();
    }

    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream bout = 
                        new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream( bout );

        toDataStream( dout );
        dout.close();

        return bout.toByteArray();
    }

    public void fromDataStream( DataInputStream din ) 
                                throws IOException {
        _firstName = din.readUTF();
        _lastName = din.readUTF();
        _phoneNumber = din.readUTF();
    }

    public void toDataStream( DataOutputStream dout ) 
                              throws IOException {
        dout.writeUTF( getFirstName() );
        dout.writeUTF( getLastName() );
        dout.writeUTF( getPhoneNumber() );
    }
}

The following class uses a record filter as a way to find and store matching records. It returns false to indicate an empty enumeration, which is immediately discarded:

package j2me.example;

import java.io.*;
import java.util.*;
import javax.microedition.rms.*;

// Finds the contacts whose first and/or last
// names match the given values.

public class FindContacts {

    // Constructs the finder for the given names. If
    // both names are non-null, both names must match,
    // otherwise only the given name needs to match.

    public FindContacts( String fname, String lname ){
        _fname = normalize( fname );
        _lname = normalize( lname );
    }

    // Traverses the data in the record store and
    // returns a list of matching Contact objects.

    public Vector list( RecordStore rs )
                        throws RecordStoreException,
                               IOException {

        Vector v = new Vector();
        Filter f = new Filter( v );
        RecordEnumeration enum =
               rs.enumerateRecords( f, null, false );

        // The enum will never have any elements in it,
        // but we call this to force it to traverse
        // its list.

        enum.hasNextElement();
        enum.destroy();

        return v;
    }

    // Returns whether or not a given Contact
    // instance matches our criteria.

    public boolean matchesContact( Contact c ){
        boolean sameFirst = false;
        boolean sameLast = false;

        if( _fname != null ){
            sameFirst =
               c.getFirstName().toLowerCase().equals(_fname);
        }

        if( _lname != null ){
            sameLast =
               c.getLastName().toLowerCase().equals( _lname );
        }

        if( _fname != null && _lname != null ){
            return sameFirst && sameLast;
        }

        return sameFirst || sameLast;
    }

    // Normalize our name data

    private static String normalize( String name ){
        return( name != null ? 
                name.trim().toLowerCase() : null );
    }

    private String _fname;
    private String _lname;

    // A record filter that always returns false but
    // whenever it finds a matching contact it adds it
    // to the given list.

    private class Filter implements RecordFilter {
        private Filter( Vector list ){
            _list = list;
        }

        public boolean matches( byte[] data ){
            try {
                Contact c = new Contact();
                c.fromByteArray( data );

                if( matchesContact( c ) ){
                    _list.addElement( c );
                }
            }
            catch( IOException e ){
            }

            return false;
        }

        private Vector _list;
    }
}

Unfortunately, memory limitations may prevent caching more than a few objects at a time. You can gain some performance while searching by using an index, a separately-maintained table that pairs record IDs with key values, such as contacts' names. An index is usually small enough to keep in memory, and saves you the hassle of using an enumeration each time you need to find a particular record.

 
About the Author

Eric Giguere is a software developer for iAnywhere Solutions, a subsidiary of Sybase, where he works on Java technologies for handheld and wireless computing. He holds BMath and MMath degrees in Computer Science from the University of Waterloo and has written extensively on computing topics.

 
Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.
false ,,,,,,,,,,,,,,,