Java ACDP Tutorial

Design

Designing an ACDP database involves coding subclasses for the CustomDatabase and the CustomTable classes, and optionally for the SimpleType class if there is a need for one or more custom column types. Furthermore, data encryption and decryption can be enabled by providing an implementation of the ICipherFactory interface.

The remaining sections of this page are

A Sample World

In an educational institution students attend courses and teachers instruct courses. You can contact a student and a teacher using their contact details. The following UML diagram, created with the UMLet tool, shows the entities, their attributes and the associations.
UML diagram
In UML an arrow shows the direction of navigation from one entity to another. For example, starting from a student, there should be an easy way to find that student's courses or contact details. However, starting from a course or some contact details no obvious link is required to a student. Note that the association between Teacher and Course is bidirectional.

The black diamond ( ) indicates a composition. Since the contact details are rather complex, by dedicating them their own entity, we avoid the repetition of contact related attributes in the Student and Teacher entities, including any code for checking the validity of values, for example whether a given value is a valid phone number.

Coding Table Classes

Translating the world depicted in the UML diagram above into a collection of ACDP database tables is straightforward: Each entity has a direct correspondence to an ACDP table and a link from one entity to the same or another entity corresponds to either a reference column type or an array of references column type, depending on the cardinality of the link.

For each table class we need to define the columns of the table as well as the characteristic sequence of columns, also known as the table definition:

  1. Create a Column instance with the CL column factory class for each column of the table and assign each column instance to a separate variable.
  2. Define a constructor and invoke the initialize method of the super class with the column instances declared in the first step.
Without further ado:
   import static acdp.design.ST.Nulls.NO_NULL;
   import static acdp.design.ST.Nulls.NULLABLE;
   import static acdp.design.ST.OutrowStringLength.SMALL;
   
   final class CourseTable extends CustomTable {
      static final Column<String> ID = CL.ofString(NO_NULL, SMALL);
      static final Column<String> NAME = CL.ofString(NO_NULL, SMALL);
      static final Column<Ref> TEACHER = CL.ofRef();
   
      CourseTable() {
         initialize(ID, NAME, TEACHER);
      }
   }
   
   final class StudentTable extends CustomTable {
      static final Column<Integer> STUDENT_NUMBER = CL.ofInteger(NO_NULL);
      static final Column<String> NAME = CL.ofString(NO_NULL, SMALL);
      static final Column<Ref> CONTACT = CL.ofRef();
      static final Column<Ref[]> COURSES = CL.ofArrayOfRef(30);
      
      StudentTable() {
         initialize(STUDENT_NUMBER, NAME, CONTACT, COURSES);
      }
   }
   
   final class TeacherTable extends CustomTable {
      static final Column<String> NAME = CL.ofString(NO_NULL, SMALL);
      static final Column<Float> SALARY = CL.ofFloat(NULLABLE);
      static final Column<Ref> CONTACT = CL.ofRef();
      static final Column<Ref[]> COURSES = CL.ofArrayOfRef(45);
   
      TeacherTable() {
         initialize(NAME, SALARY, CONTACT, COURSES);
      }
   }
   
   final class ContactTable extends CustomTable {
      static final Column<String> PHONE_NUMBER = CL.ofString(
                                     Charset.forName("US-ASCII"), NULLABLE, 12);
      static final Column<String> EMAIL = CL.ofString(NO_NULL, SMALL);
      static final Column<String> STREET = CL.ofString(NO_NULL, SMALL);
      static final Column<String> CITY = CL.ofString(NO_NULL, SMALL);
      static final Column<String> STATE = CL.ofString(NO_NULL, SMALL);
      static final Column<Integer> POSTAL_CODE = CL.ofInteger(NO_NULL);
      static final Column<String> COUNTRY = CL.ofString(NULLABLE, SMALL);
      
      ContactTable() {
         initialize(PHONE_NUMBER, EMAIL, STREET, CITY, STATE, POSTAL_CODE, COUNTRY);
      }
   }
Remarks:
  1. The column variable declarations include the static modifier. This is possible because we do not intend to reuse any of the table classes in our database or some other database. (See the description of the CustomTable class for details.)
  2. Always include the final modifier in the declaration of a column variable. Here, we included the final modifier in the declaration of the table classes too. A more elaborated table class may be derived from a more general (abstract) table super class.
  3. Note that we have not yet assigned names to the tables and columns. We have also not yet specified the target tables of the reference column types that appear in the table classes Courses, Student and Teacher. This will be done later in Chapter Creation when we prepare the table classes for processing by ACDP's Setup Tool.
  4. The CL column factory class provides factory methods for columns with a type corresponding to a built-in Java type, including arrays. The NO_NULL or NULLABLE argument creates a column type that prohibits or allows the null value to be stored in the column. The null value is always allowed as a value in a column of an array, a reference or an array of references column type.
  5. Invoking CL.ofString() returns a column with a nullable string column type of a length comparable to the length of an ordinary Java String instance.
  6. A column of a string column type created with the SMALL argument allows for strings with a length of 255 characters at most.
  7. The PHONE_NUMBER column in the Contact table is created by an invocation of CL.ofString(Charset.forName("US-ASCII"), NULLABLE, 12). This allows for a phone number with a length of 12 characters at most. The really special thing about this column type, however, is its so called inrow storage scheme which allows the particularly efficient storage of values.
  8. A student can attend at most 30 courses and a teacher instructs at most 45 courses.
Just for information: To create an array of values of a type different from the reference type, use the CL.ofArray(int, SimpleType) method with any built-in or custom non-array element type. For example, CL.ofArray(73, ST.beDouble(NO_NULL)) creates an array with a maximum of 73 Double values that are not allowed to be null. (The ST class is a factory class for ACDP's built-in non-array column types.)

Coding the Database Class

Coding the database class includes defining the tables as well as a constructor which, when invoked, opens the database.
   final class EduDB extends CustomDatabase {
      static final CourseTable COURSE_TABLE = new CourseTable();
      static final StudentTable STUDENT_TABLE = new StudentTable();
      static final TeacherTable TEACHER_TABLE = new TeacherTable();
      static final ContactTable CONTACT_TABLE = new ContactTable();
   
      EduDB(Path mainFile, int opMode, boolean writeProtect,
                                                   int consistencyNumber) {
         open(mainFile, opMode, writeProtect, null, consistencyNumber,
                 COURSE_TABLE, STUDENT_TABLE, TEACHER_TABLE, CONTACT_TABLE);
      }
   }
Remarks:
  1. The table variable declarations include the static modifier. This is possible because we do not intend to reuse any of the table classes in our or some other database. (See the description of the CustomDatabase class for details.)
  2. Always include the final modifier in the declaration of a table variable. Here, we included the final modifier in the declaration of the database class too. A more elaborated database class may be derived from a more general (abstract) database super class.
  3. Note that we have not yet assigned a name to the database. This will be done later in Chapter Creation when we prepare the database class for processing by ACDP's Setup Tool.
  4. The arguments of the open method are explained in Chapter Opening.

Coding a Custom Column Type

Suppose we want to store the salary of a teacher as a value of type java.math.BigInteger rather than java.lang.Float. Since ACDP has no such built-in type, we must code our own BigInteger column type.

All non-array column types are derived from the SimpleType class. A class that extends the SimpleType class must implement the toBytes(T) and the fromBytes(byte[], int, int) methods which convert a value of type T, for example BigInteger, into a byte array and vice versa.

Moreover, implementers of a custom column type must implement a public and static factory method annotated with the TypeFromDesc annotation which creates an instance of the column type from its so called type descriptor. The type descriptor is a String that uniquely identifies the column type. It contains enough information so that an instance of the column type can be created from it.

The BigInteger column type looks as follows:
   public final class BigIntegerType extends SimpleType<BigInteger> {
      private static final String TD_PREFIX = "BigInteger";
   
      @TypeFromDesc
      public static final BigIntegerType createType(String typeDesc) throws
                                                           CreationException {
         final TypeDesc td = new TypeDesc(typeDesc);
         if (!td.prefix.equals(TD_PREFIX) || td.scheme != Scheme.INROW ||
                                                               !td.variable) {
            throw new CreationException("Illegal type descriptor");
         }
         return new BigIntegerType(td.limit, Nulls.get(td.nullable));
      }
   
      BigIntegerType(int limit, Nulls nulls) throws IllegalArgumentException {
         super(BigInteger.class, Scheme.INROW, nulls.value(), limit, true);
      }

      @Override
      protected final String typeDescPrefix() {
         return TD_PREFIX;
      }
   
      @Override
      protected final byte[] toBytes(BigInteger val) throws NullPointerException {
         return val.toByteArray();
      }

      @Override
      public final BigInteger fromBytes(byte[] bytes, int offset,
                                     int len) throws IndexOutOfBoundsException {
         return new BigInteger(Arrays.copyOfRange(bytes, offset, offset + len));
      }
   }
Remarks:
  1. The limit parameter of the constructor lets us create columns wich accept integer values of an arbitrary size. Recall that Java's int and long types are 4 and 8 bytes in size, respectively.
  2. The nulls parameter of the constructor specifies whether the column type allows the null value.
  3. The second and fifth arguments of the super constructor invocation tell us that the BigInteger column type uses the efficient inrow storage scheme and expects the byte array returned by the toBytes method to be of a variable length.
  4. The type descriptor starts with a prefix which by default starts with the uppercase letter L followed by the qualified name of the column type's class name. Overriding the typeDescPrefix() method lets you specify your own prefix. Such a custom prefix must start with an uppercase letter because the type descriptor of a custom column type must start with an uppercase letter.
  5. Note that the createType(String) factory method throws a CreationException if the argument is not a valid type descriptor for the BigInteger column type.
Recall that we want the BigInteger custom column type to replace the Float built-in column type in the Salary column of the Teacher table. Simply replace the line of code
   static final Column<Float> SALARY = CL.ofFloat(NULLABLE);
in the TeacherTable class with
   static final Column<BigInteger> SALARY = CL.create(
                          new BigIntegerType(20, NULLABLE));
The Salary column is now a column of type BigInteger with a maximum size in its two's-complement binary representation of 20 bytes. The column accepts the null value.

Coding a Cipher Factory

Data stored in an ACDP database is not encrypted by default. If encryption is required then we need to provide an implementation of the ICipherFactory interface. Such a class defines two methods: A factory method returning an instance of Java's javax.crypto.Cipher class and a method that initializes the cipher. That said, the strength of encryption of data stored in an ACDP database is not a property of ACDP itself but is completely governed by the strength of encryption the Cipher instance returned by the createCipher method is able to deliver.

Here is a ready-to-play-with cipher factory class which unrealistically discloses the secret key in the source code:

   final class CipherFactory implements ICipherFactory {
      private final IvParameterSpec iv = new IvParameterSpec(new byte[] {
                                 114, -8, 22, -67, -71, 30, 118, -103,
                                 51, -45, -110, -65, 16, -127, -73, 103 });

      private final Key key = new SecretKeySpec(new byte[] { 114, -8, 23,
                                   -67, -71, 30, 118, -103, 51, -45, -110,
                                   -65, 16, -127, -73, 103 }, "AES");
      @Override
      public final Cipher createCipher() throws NoSuchAlgorithmException,
                                                  NoSuchPaddingException {
         // Example with padding: AES/CBC/PKCS5Padding
         return Cipher.getInstance("AES/CTR/NoPadding");
      }

      @Override
      public final void initCipher(Cipher cipher, boolean encrypt) throws
                  InvalidKeyException, InvalidAlgorithmParameterException {
         cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
                                                                   key, iv);
      }
   }
When we speak of an ACDP database we usually mean the writable WR type. However, as described in Section Creating an RO Database from a WR Database of Chapter Creation, there exists a second type: the readonly RO type. The cipher for a WR database must be a byte-oriented stream cipher or must behave that way. With the NoPadding attribute, the createCipher method of the cipher factory from above returns such a cipher. The cipher factory can therefore be used for a WR database. (No such restriction applies for an RO database.) For details consult the description of the ICipherFactory interface.
Back To Top