This section will take you step by step through the process of building a domain object.
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.
Example 10-1. Starting to Build the Domain Object
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.
Note | |
---|---|
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. |
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.
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.
Example 10-3. Implementing Constructors
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.
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.
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.
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); } } |
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.
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
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.
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(); } } |
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.
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.
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.