10.2. Domain Objects Tutorial

10.2.1. Building the Domain Object

This section will take you step by step through the process of building a domain object.

10.2.1.1. Starting the Class

A domain object starts off as a standard Java class that extends one of the four domain object base classes. Because of the auditing and permissioning requirements, AuditedACSObject is the base class.

The code in Example 10-1 illustrates the initial definition of the class and the import statements.

package com.arsdigita.notes;

import com.arsdigita.db.Sequences;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.auditing.AuditedACSObject;
import com.arsdigita.kernel.Stylesheet;
import com.arsdigita.kernel.User;                          (1)
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.PersistenceException;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.metadata.ObjectType;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Date;
// Stylesheets
import java.util.Locale;
import java.util.ArrayList;

import org.apache.log4j.Category;

/**
 * Note class.  Extends AuditedACSObject to implement      (2)
 * persistent Note objects.
 *
 * <p>
 * Note: according to current DomainObject docs, APIs are not yet
 * stable. Changes may invalidate the Note class.
 *
 * @author Scott Seago
 **/
public class Note extends AuditedACSObject {               (3)


    private static Category log =                          (2)
        Category.getInstance(Note.class.getName());

    /**
     * BASE_DATA_OBJECT_TYPE represents the full objectType name for the
     * Note class
     **/
    private static final String BASE_DATA_OBJECT_TYPE =    (4)
          "com.arsdigita.notes.Note";
...

    /**
     * Returns the appropriate object type for a Note so that proper
     * type validation will occur when retrieving Notes by  OID
     *
     * @return The fully qualified name of of the base data object
     * type for the Note object type.
     */
    protected String getBaseDataObjectType() {
        return BASE_DATA_OBJECT_TYPE;                      (4)
    }

}
(1)
The AuditedACSObject is imported from the auditing service package.
(2)
Log4j is used as a uniform debugging tool across WAF. The convention for initializing the log4j system is to initialize a category with the name of the current class, as shown here.
(3)
By extending this baseclass, this Java class becomes a domain object. It inherits a data object and methods for manipulating it. Calls to these methods are automatically observed and modifications are logged in the audit trail.
(4)
The data object type for the domain object is declared as a member variable, BASE_DATA_OBJECT_TYPE. It is final and static, so the compiler will replace it with a constant. An inherited function, getBaseDataObjectType, is overridden to return this variable. This method is used by the constructors on the superclasses to verify that the data object matches this type or is a subtype. For more information on this topic, see Section 10.2.1.2 Constructors.

Example 10-1. Starting to Build the Domain Object

10.2.1.2. Constructors

Before any of the methods of a class can be used, an instance must be constructed in memory. There are several different varieties of constructors for domain objects, all of which are illustrated in the Notes domain object.

There are two different types of constructors for domain objects. One is used for constructing domain objects by creating a new data object. The no-arg constructors — the String typename and the ObjectType type — are this type of constructor. These constructors use the persistence system to create a new data object that matches the type passed as an argument.

The other type of constructor relies on retrieval of an existing data object to construct the domain object. The first of these takes an Object ID or OID as input, and then retrieves a data object corresponding to that OID. An OID is comprised of two pieces: a BigDecimal numeric ID and an object type identifier. The second constructor takes a data object as input and constructs a DomainObject to wrap it. This constructor does not need to construct the underlying data object, so it is preferable to use this constructor if the data object has already been loaded into memory.

These constructors exist in DomainObject, ObservableDomainObject, ACSObject, and AuditedACSObject. Subclasses should replicate these constructors, unless there is a good reason to restrict them. The comments following Example 10-2 explain the overall utility of each constructor, and why each subclass should use them. Additional constructors can be added, and this is encouraged if it makes the class more convenient to use.

NoteNote
 

Be aware that none of the constructors will change the persistent state of the DomainObject or its data object. To store a domain object's properties in the database, the save method must be called. To delete a DomainObject, the delete method must be called.

public class Note extends AuditedACSObject {
    /**
     * BASE_DATA_OBJECT_TYPE represents the full objectType name for the
     * Note class
     **/
    private static final String BASE_DATA_OBJECT_TYPE = 
       "com.arsdigita.notes.Note";

    /**
     * Default constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType of "Note".
     *
     * @see AuditedACSObject#AuditedACSObject(String)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note() {                                        (1)
        this(BASE_DATA_OBJECT_TYPE);
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by the string
     * typeName
     *
     * @param typeName The name of the ObjectType of the
     * contained DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note(String typeName) {                         (2)
        super(typeName);
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by type
     *
     * @param type The ObjectType of the contained
     * DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note(ObjectType type) {
        super(type);                                       (3)
    }


    /**
     * Constructor. Retrieves a Note instance, retrieving an existing
     * note from the database with OID oid. Throws an exception if an
     * object with OID oid does not exist or the object 
     * is not of type Note
     *
     * @param oid The OID for the retrieved
     * DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(OID)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.OID
     *
     * @exception DataObjectNotFoundException Thrown if we cannot
     * retrieve a data object for the specified OID
     *
     **/                                                   (4)
    public Note(OID oid) throws DataObjectNotFoundException {
        super(oid);
    }
...
}
(1)
The empty constructor is used to construct a new Note with its default data object type. This is the common case for constructing a new Note, where a specific subtype of domain object is not needed and an existing Note is not being retrieved.
(2)
The typeName specified in this constructor must be a String that names an object type. This does the same thing as the constructor that takes an object type, but is more convenient, because you only need to specify a String with the type name.
(3)
This constructor takes an ObjectType that matches the base object type, in this case, com.arsdigita.notes.Note or a subtype. The constructor requiring an ObjectType can be used to specify a specific subtype of the Notes data object. This is useful if the data object may be used with different domain objects that have different methods.

In most cases, the empty constructor, which defaults to the base object type, is sufficient, and any reason to use the other constructors should be specified. Even if the Note domain object will not be used with multiple object types, it is necessary to have these constructors for a subclass to provide this functionality. Without providing the constructors on every class, a subclass cannot rely on the constructor chain to reach the constructors on ACSObject and DomainObject.

(4)
This constructor is used to construct a Notes domain object with a Notes domain object that has already been created. The OID is used to retrieve an instance of the correct domain object. The retrieved data object can then be accessed using the set/get methods and all other methods defined.

Example 10-2. Domain Object Constructors

The role of each constructor can be clarified by examining how the constructors are implemented in DomainObject, the root base class for all domain objects.

public abstract class DomainObject {
    private final DataObject m_dataObject;

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by the string
     * typeName.
     *
     * @param typeName The name of the ObjectType of the
     * new instance.
     *
     * @see com.arsdigita.persistence.Session#create(String)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public DomainObject(String typeName) {
        Session s = SessionManager.getSession();           (1)
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session 
                from " + "the session manager while instantiating " +
                "a class with ObjectType = " + typeName);
        }

        m_dataObject = s.create(typeName);
        initialize();
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by type.
     *
     * @param type The ObjectType of the new instance.
     *
     * @see com.arsdigita.persistence.Session#create(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public DomainObject(ObjectType type) {
        Session s = SessionManager.getSession();           (1)
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session 
                from " + "the session manager while instantiating " +
                "a class with ObjectType = " + type.getName());
        }

        m_dataObject = s.create(type);
        initialize();
    }

    /**
     * Constructor. The contained DataObject is retrieved
     * from the persistent storage mechanism with an OID specified by
     * oid.
     *
     * @param oid The OID for the retrieved
     * DataObject.
     *
     * @exception DataObjectNotFoundException Thrown if we cannot
     * retrieve a data object for the specified OID
     *
     * @see com.arsdigita.persistence.Session#retrieve(OID)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.OID
     **/
    public DomainObject(OID oid) throws DataObjectNotFoundException {
        Session s = SessionManager.getSession();
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session (2)
                from " + "the session manager while instantiating " +
                "a class with OID = " + oid.toString());
        }

        m_dataObject = s.retrieve(oid);
        if (m_dataObject == null) {
            throw new DataObjectNotFoundException
                ("Could not retrieve a DataObject with " +
                 "OID = " + oid.toString());
        }
        initialize();
    }

    /**
     * Constructor. Creates a new DomainObject instance to encapsulate 
     * a given data object.
     *
     * @param dataObject The data object to encapsulate in the new domain
     * object.
     * @see com.arsdigita.persistence.Session#retrieve(String)
     **/
    public DomainObject(DataObject dataObject) {
        m_dataObject = dataObject;
        initialize();
    }                                                      (2)

    /**
     * Returns the base data object type for this domain object class.
     * Intended to be overridden by subclasses whenever the subclass will
     * only work if their primary data object is of a certain base type.
     *
     * @return The fully qualified name ("modelName.typeName") of the base
     * data object type for this domain object class,
     * or null if there is no restriction on the data object type for
     * the primary data object encapsulated by this class.
     **/
    protected String getBaseDataObjectType() {
        return null;
    }

    /**
     * Called from all of the DomainObject constructors
     * to initialize or validate the new domain object or its
     * encapsulated data object.  This was introduced in order to
     * support efficient validation of the encapsulated data object's
     * type.  If the validation is typically performed in class
     * constructors, then redundant validation is performed in
     * superclass constructors.  This validation now occurs here.
     **/
    protected void initialize() {
        if (m_dataObject == null) {
            throw new RuntimeException
                ("Cannot create a DomainObject with        (3)
                   a null data object");
        }

        String baseTypeName = getBaseDataObjectType();
        if (baseTypeName == null) {
            return;
        }
        // ensure data object is instance of baseTypeName 
        // or a subtype thereof.
        ObjectType.verifySubtype(baseTypeName, 
                                 m_dataObject.getObjectType());
    }
...
}
(1)
These constructors are used to create an empty data object. The Session class is used look up the metadata that defines the object type and returns the data object. Because the data object is a private member variable, it is denoted m_dataObject.
(2)
These two constructors are used to create a DomainObject when a data object already exists. The OID-based constructor searches for a data object with the given OID. If the object cannot be found, a DataObjectNotFound exception is thrown. Otherwise, the DomainObject is initialized with the given data object. The data-object-based constructor is used if the data object is available, so that an additional one does not need to be retrieved from the database.
(3)
The initialize() is called after the domain object instance is created. One use of this function is to validate the data object type to see if it is the same type or a subtype of the base data object type. If not, an exception is thrown. In order for this to work, your subclass must implement getBaseDataObjectType(), as in the Notes example. Subclasses of DomainObject can add their own initialization logic, but should always call super.initialize() in the first line of the function so that initializations from the superclasses are processed.

Example 10-3. Implementing Constructors

10.2.1.3. Adding Create Methods

The constructors described in Section 10.2.1.2 Constructors are intended to be used for creating empty data objects or retrieving existing data objects. However, in some situations, it is useful to have a method that takes in a set of application-specific parameters and creates a domain object for you:

/**
 * Creates a new note and sets the title, body, and theme.
 *
 * @param title The title describing the note.
 * @param body  The body of the note.
 * @param theme The theme of the note.
 */
public static Note create(String title, String body, NoteTheme theme) {
    Note note = new Note();
    note.setTitle(title);
    note.setBody(body);
    note.setTheme(theme);
    return note;
}

This method is static, so it can be called without an instance of a Note. It constructs a new note and then sets the title, body, and theme properties to the values passed into the method. It then returns the constructed Note object. The Note's data is not saved in the database by the constructor. Such data is not persisted until the save method is executed.

10.2.1.4. DomainObject Methods

Several methods are inherited from DomainObject that are useful for building other methods for subclasses. In general, methods are added to subclasses to provide application logic specific to a business domain. A common case of this are accessors and mutators for data object properties. Such methods are usually called by process objects or UI components.

10.2.1.4.1. Accessors and Mutators

Because DomainObjects wrap data objects, accessors and mutators for the domain object properties are common methods to provide on a domain object. The DomainObject class provides a set and get that are automatically delegated to the data object member of DomainObject. The set method is used to set properties of the domain object to a value. The get method is used to retrieve the values of those properties.

Using the Note domain object, the following example demonstrates how to implement an accessor and mutator for the contained data object's property.

/**
 * Retrieve the title of the note.
 *
 * @return The title.
 */
 public String getTitle() {
     return (String) get("title");
 }

/**
 * Set the title of the note.
 *
 * @param title A title for the note.
 */
 public void setTitle(String title) {
     set("title", title);
 }

The accessor, getTitle works by calling get with the name of the property to retrieve. The getmethod returns a java.lang.Object, so the return type must be casted to a String. The setTitle method works by calling the set with the name of the property to be set and the value.

The member data object in DomainObject is private to prevent access to the data object outside of the methods, ensuring an interface/implementation barrier. Subclasses of DomainObject cannot access the data object directly but must use methods on DomainObject instead. Because this barrier guarantees that all access to the properties of the data object are through set and get methods, subclasses can add behavior to these methods. For example, the ObservableDomainObject adds behavior to the set method that enables observers of the DomainObject to be notified when a property is changed.

10.2.1.4.2. Initialize

The DomainObject class provides an initialize method that is executed after the constructor is run. The Domain Subclasses can override this method and add further initialization logic. However, the initialize method on the superclass should be run to ensure that all inherited member variables are initialized and so that the data object type checking is executed.

A good use of the initialize method is to set initial values for data object properties. The ACSObject class uses the initialize method to determine the value for the ObjectType property.

/**
 * Called from base class constructors (DomainObject constructors).
 */
protected void initialize() {
    super.initialize();
    if (isNew()) {
        String typeName =
            getObjectType().getModel().getName() +
            "." + getObjectType().getName();

        set("objectType", typeName);
    }
}

10.2.2. Conventions

  1. Every domain object with a public constructor should have a public final static String BASE_DATA_OBJECT_TYPE variable with the value equal to the primary data object type used by the domain object. The common use for the BASE_DATA_OBJECT_TYPE variable is to construct an OID for the data object given its numeric id. For convenience, you can provide a no-arg constructor that will dispatch to the object type constructor with the BASE_DATA_OBJECT_TYPE variable.

  2. The protected method getBaseDataObjectType is required to provide proper data object type checking. In the common case, the method returns the BASE_DATA_OBJECT_TYPE variable. The DomainObject initialize method ensures that the data object used to create a DomainObject is equal to that type or a subtype of the object type returned by this method. For example, if you try to instantiate an ACSObject with a data object that is not a subtype of the ACSObject data object type, you will get an exception. By default, this method returns null, which disables the type checking

10.2.3. Examples

10.2.3.1. Creating Messages

By default, a new Message has a MIME type of text/plain, so creating and storing simple text messages is straightforward:

Party from; Message msg = new Message(from, "the subject", "the body");
msg.save();

To create a message in a different format, like text/html, the API provides a method to set the MIME type when the Message is created:

Message msg = new Message(from, subject);
msg.setBody("<p>this is the body</p>", MessageType.TEXT_HTML);
msg.save();

Finally, a Message can also be instantiated by retrieving it from the database by supplying its unique ID:

Message msg = new Message(new BigDecimal(1234));

Note that the Message class only support bodies with a primary MIME type of text. The MessageType interface defines legal MIME types for use in specifying a message body.

10.2.3.2. Referring to Other Objects

Messages can store an optional reference to an ACSObject. This is useful for attaching messages to other items in the system. For example, a discussion forum might implement a Topic class to represent a collection of related messages. The process of adding a message to a given topic can be implemented by setting a reference to that topic:

// One topic in a discussion forum
class Topic extends ACSObject {

// Add a new posting for this topic
addNewPosting (User postedBy, String subject, String post) {
Message msg = new Message(postedBy, subject, post);
msg.setRefersTo(this);
msg.save();
}
}

10.2.3.3. Attachments

Additional pieces of content can be attached to a message. These content items can be any media type with a supported DataHandler. Convenience methods are provided for attaching plain text documents directly to a message:

Message msg = new Message(from, subject);
msg.setText("See the attached document.");
msg.attach("A simple document", "simple.txt");
msg.save();

To attach more complex types of content, you use a MessagePart and an appropriate data source or DataHandler. There are convenience methods to attach content retrieved from a File or a URL. The following example shows how to attach an uploaded image to a message:

File file = new File("image.gif");

MessagePart part = new MessagePart();
part.setContent(image, "image.gif", "An image");

Message msg = new Message(from, subject, "See attached image");
msg.attach(part);
msg.save();

The code for downloading the image from a remote server is almost identical:

URL url = new URL("http://mysite.net/image.gif");

MessagePart part = new MessagePart();
part.setContent(url, "image.gif", "An image from mysite");

Message msg = new Message(from, subject, "See attached image");
msg.attach(part);
msg.save();

Note that the above two examples do not explicitly set a MIME type for the content. This is determined automatically when the content is handed off to a DataHandler for processing. In both cases shown above the content will be of type image/gif.

10.2.3.4. Simple Threading

A Message object can store a reference to another Message object to implement basic threading. A reply is created using the reply() method of the parent Message:

Message parent = new Message(new BigDecimal(1234)); Message reply =
parent.reply();

reply.setFrom(from);
reply.setText("I do not agree, because...");
reply.save();

When a reply is created this way, the subject is initialized as appropriate for a reply to the parent, and a reference to the parent message is stored internally. The client code must set other message properties, including the body of the reply.

10.2.3.5. Advanced Threading

Many applications need to store a more complex, structured set of responses than the flat organization provided by Message. The ThreadedMessage class provides support for organizing messages into a tree structure as outlined above. The API for ThreadedMessage is identical, but the reply() also takes care of setting a special sort key that determines the proper location of a message in the tree.

The following example shows a simple discussion thread and the technique used to retrieve all messages from the database for display in the correct order. The structure of the underlying tree is as follows:

msg0
msg2
    msg4
    msg5
msg3
    msg6
msg1

Although in a real application the structure of messages and replies would be generated over time, the following example shows the sequence of calls that produces the same organization, for illustration purposes.

// A common object that all messages refer to.  
// In practice this might a forum or a content 
// item that is	being discussed by a group of 
// collaborators.

ACSObject anchor;
          
// Create the message

ThreadedMessage msg[];

msg[0] = new ThreadedMessage();
msg[0].setRefersTo(anchor);

msg[1] = new ThreadedMessage();
msg[1].setRefersTo(anchor);

msg[2] = msg[0].replyTo(from,body);
msg[3] = msg[0].replyTo(from,body);
msg[4] = msg[2].replyTo(from,body);
msg[5] = msg[2].replyTo(from,body);
msg[6] = msg[3].replyTo(from,body);

Note that the two root-level messages (0 and 1) explicitly set the reference to our anchor object, but the replies do not. This information is automatically transferred to the replies when they are created, as are the subject and other properties, in the same way as Message.reply().

In addition, ThreadedMessage.replyTo() also sets a special sort key that determines the correct position of the message in the tree. Details of the sort keys are not important, but they are generated so that all common messages can be retrieved in the correct order using a single order by clause. A special query provided by the messaging package is used to reconstruct the tree structure shown above:

Session session = SessionManager.getSession();
DataQuery query = session.retrieveQuery
 ("com.arsdigita.messaging.getMessageTree");
query.addEqualsFilter("object", anchor);

while (query.next() {
ThreadedMessage msg = new ThreadedMessage
 ((BigDecimal) query.get("id"));
System.out.println("message " + msg.toString());
}

The addEqualsFilter is required to retrieve only those messages that refer to the same ACSObject.