See: Description
Package | Description |
---|---|
de.tmasoft.acaf.attributes |
This package contains interfaces and concrete classes for simple attributes.
|
de.tmasoft.acaf.attributes.comparators |
This package contains classes, which can be used to compare attributes
implementing the interface
AttributeInterface . |
de.tmasoft.acaf.base |
This package is the core package of ACAF containing the base
interfaces and classes, which define the component-attribute-container
composite to build up business classes and the
Visitor interface, which may be implemented by visitor
classes to walk through the composite. |
de.tmasoft.acaf.containers |
This package contains the concrete container attribute classes implementing
the interfaces
ComponentContainerInterface and
ContainerReferenceInterface Containers contain
components implementing the interface ComponentInterface and play the composite role for
components in the composite design pattern. |
de.tmasoft.acaf.exception |
This package contains the exception interface and base class of the
framework.
|
de.tmasoft.acaf.io |
This package contains interfaces and classes used to store and retrieve
business objects in/from external datastores.
|
de.tmasoft.acaf.jdbc |
This package contains the O/R mapping classes used to store and retrieve
business objects in/from a relational database.
|
de.tmasoft.acaf.msgcat |
This package contains the message catalog interface and base class used to
internationalize your application.
|
de.tmasoft.acaf.parser |
This package contains visitor classes, which can be used to manage business
objects using command lines.
|
de.tmasoft.acaf.util |
This package contains several utility classes like the classes for duration
handling
Duration and SimpleDurationFormat , the extended Tokenizer with look ahead a state reset functionality,
the ThreadLocale used to manage a thread specific
locale, the UUIDGenerator generating unique
identifiers according to the Leach-Salz specification and many others. |
de.tmasoft.acaf.visitors |
This package contain general purpose visitor classes, used to reset all
attribute values to their default value (
ClearAttributesVisitor ), the visitor to create a
deep copy of a business object tree (CopyComponentsVisitor ), the visitor to retrieve a
component from a business object composite (GetComponentVisitor ) and many others. |
de.tmasoft.acaf.xml |
This package contains the classes to serialize/deserialize a business object
tree to/from a XML document.
|
The Common Application Framework (ACAF) is used as base to implement technology-free, robust and clean business classes of an application, which should be connected to different other frameworks, without affecting the business classes itself.
The Common Application Framework promotes clean business classes, which contain only code for business relevant processes. It keeps the business classes free from code, required by technical frameworks, e.g. for O/R mapping, serialization or GUIs. This helps to adopt the software design principle "Separation of Concerns".
Beside the base interfaces and classes used to build business classes, the framework contains a package for O/R mapping, used to store business objects in relational databases, a package for XML serialization/deserialization of business objects, and many other utility classes. The following list highlights the main features and concepts of ACAF:
The main purpose of this framework is to build business classes based on certain interfaces and classes of the framework. These interfaces and classes are:
ComponentInterface
Component
AttributeInterface
ComponentContainerInterface
ContainerReferenceInterface
These interfaces and classes define a
composite, where the class Component
plays the
composite role for attributes implementing the interface AttributeInterface
and attribute classes implementing the
interfaces ComponentContainerInterface
or ContainerReferenceInterface
play the composite role for
components implementing the interface ComponentInterface
. This composite may be walked through
by a
visitor implementing the interface Visitor
. The
visitor design pattern is one of the main concepts of the framework and visitors
are used by many packages to access the business objects and attributes of a
business object tree.
Business classes participating in the framework must implement the interface
ComponentInterface
. They may extend the base class
Component
, which implements most of the methods
defined by the interface. In the following, the term component is used as
an equivalent for business class.
The main properties of a component are the component type and the
component name. The component type identifies a business class in
the class composite. For many business classes, the relationship between the
class and the component type is a 1..1 relationship, which means, that the class
name could be used as component type. However, for business classes, which are
used more than once in the class composite, different component types are
required. E.g. a business class Address may represent the address of a
person and the address of a company. In this case two component types,
PersonAddress and CompanyAddress are required by the framework to
differentiate the address business objects. Such a discriminator is especially
required by the O/R mapping classes of the package de.tmasoft.acaf.jdbc
to map the business objects to the corresponding database tables.
The component name identifies a business object uniquely in a dictionary
container implementing the interface ComponentContainerInterface
. The component name can
be considered as key of a component. When a business object is stored in a
database, the component name is usually part of the primary key.
A component may have a relationship to a parent component. All business objects (except the root component of a business object tree) have a parent component. E.g. the class AddressBook contains components of the class Person and a person contains components of the class Address. In this case, the parent component of a person is the address book, which contains the person as contact and the person is the parent component of its addresses. When the application supports only one address book, the address book is a root component and has no parent.
Business classes participating in the framework must implement their attributes
using the attribute classes of the packages de.tmasoft.acaf.attributes
and de.tmasoft.acaf.containers
, or with custom attribute classes
implementing the interface AttributeInterface
.
The main properties of an attribute are the keyword and the value.
The keyword identifies the attribute uniquely in its business object. The
keyword may be equal or similar to the attribute name. However, in a class
hierarchy attributes may have the same name in different classes, when they are
declared private! To differentiate these attributes, this framework
requires different attribute keywords. Attribute keywords are used by the O/R
mapping classes of the package de.tmasoft.acaf.jdbc
to map the attribute
value to a certain database column and by the XML serialization classes of the
package de.tmasoft.acaf.xml
to create the attributes in the XML tags.
The value of an attribute is the the classic type specific value
encapsulated by the concrete attribute class. The classes of the package de.tmasoft.acaf.attributes
provide simple attribute classes for all
primitive Java types (String, int, float, Date, ...) and some additional classes
for enumeration, set, time, duration and references. The classes of the package
de.tmasoft.acaf.containers
provide container attribute classes.
The value of a container attribute is the component collection in the
container.
Attributes must be added to the attribute list of the component base class after
construction, calling the method Component.addAttribute(AttributeInterface)
. This makes the
attributes accessible by visitors. When the component base class is not used, a
similar mechanism has to be implemented by your business classes to make methods
like ComponentInterface.visitAttribute(String,Visitor)
or ComponentInterface.visitAttributes(Visitor)
work. Attributes are usually created and added in the constructor of the
business class. So a typical constructor looks like this:
public Person (String componentName, ComponentInterface<?> parentComponent) { super (componentName, Constants.ComponentTypes.PERSON, parentComponent); firstName = new StringAttribute (Constants.Attributes.Person.FIRST_NAME); addAttribute (firstName); lastName = new StringAttribute (Constants.Attributes.Person.LAST_NAME); addAttribute (lastName); }
The simple attributes may be initialized with an initial value and a
default value. The initial value is assigned once at construction
time. The default value is used by the method AttributeInterface.clear()
to reset the attribute value to
its default. The visitor class ClearAttributesVisitor
uses this feature to reset all
attributes of a business object tree.
One of the features provided by the simple attribute classes is the conversion
of the value to/from a string representation. This feature may be used by
serialization or user-interface frameworks to exchange attribute values between
text based files or a GUI. It is used by the XML serialization classes of the
package de.tmasoft.acaf.xml
and by the command line parser classes of
the package de.tmasoft.acaf.parser
.
Container attributes make the component-attribute-container composite complete.
Container attributes implement the interfaces ComponentContainerInterface
or ContainerReferenceInterface
, which extend the interface
AttributeInterface
. So they can be added to the
attribute list of the component base class and they play the composite role for
components implementing the interface ComponentInterface
. The components contained in the
container can be considered as value of the attribute. In contrast to the
simple attributes, container attributes do not support a conversion of the value
to/from a string representation.
Containers are used to implement a composition relationship between a parent component owning the container and the so called child-components in the container. A composition is a strong relationship, which means, if the parent component is deleted, the child-components are deleted as well. There are three different types of container attributes, which are defined and implemented by the following interfaces and classes:
ContainerReferenceInterface
, ContainerReferenceAttribute
- used for 0..1
relationshipsDictionaryContainerBase
- used for 0..n
relationships and dictionary containers, which use the component name as
unique keySequenceContainerInterface
, SequenceContainerBase
- used for 0..n
relationships with a user defined ordering schema
The container classes in the package de.tmasoft.acaf.containers
apply
the
strategy design pattern. The base classes DictionaryContainerBase
and SequenceContainerBase
define the strategy, and the
derived classes implement the concrete strategy by initializing the base classes
with the appropriate collection class.
The container classes implement a runtime type-checking using the component
type of the components to be inserted into the container. Valid component
types must be passed to the constructor of the container classes or registered
by the method ComponentContainerInterface.registerType(String)
.
Reference Attributes are a special kind of simple attributes and used to
implement
associations between business objects. Reference attributes implement the
interface ComponentReferenceInterface
. ACAF
provides two implementations of this interface, the classes ComponentReferenceAttribute
and WeakReferenceAttribute
. Reference attributes have a
string representation, as all simple attributes, which is denoted as
reference path. The reference path consists of container attribute
keywords and component names or index expressions separated by a dot. The
reference path specifies the referenced component, starting at a so called
root component.
The sample competition management application shows, how reference attributes may be used. The class TeamClassification contains the reference attribute team, which references either a Team registered for the competition or another TeamClassification. The reference attribute is initialized with the competition, the TeamClassification belongs to as root component so the reference path starts at the competition. When a Team with the component name Team-4711 is referenced, which is contained in the Teams container of the competition the reference path looks like this: Teams.Team-4711. Another TeamClassification is referenced to define, which team of a Group or Pairing continues the competition in the next round. In this case the reference path references a certain index in the Classification container of a Group or Pairing and may look like this: Rounds.QR.Groups.Group-A.Classification[0]. In this case, the winner (first index: 0) of Group-A in the qualifying round QR is referenced.
Note, that the team reference attribute is implemented as WeakReferenceAttribute
. Weak reference attributes
resolve the reference path every time the referenced component is accessed via
the method WeakReferenceAttribute.getComponent()
. This is
useful in this case, because the referenced component may change, when the
position of the team classifications change in a group. Weak references should
be generally used, when the reference path may contain index expressions.
In contrast to weak references, strong references are implemented by the class
ComponentReferenceAttribute
. This class
expects, that the referenced component exists, when the path value is set. The
reference path is immediately resolved and an exception is thrown, when
the specified component doesn't exist.
The framework uses the ComponentFactory
to create
business objects with their component type, e.g. when they are
de-serialized or created by the classes of the O/R mapping package de.tmasoft.acaf.jdbc
or the parser package de.tmasoft.acaf.parser
. The component factory is implemented as singleton
and applies the prototype
and
abstract factory design patterns to create business objects. For this
purpose the business classes must implement the method ComponentInterface.cloneComponent(String,String,ComponentInterface)
.
This is the only technical method in a business class, which is required by
ACAF. A typical implementation of this method looks like this:
public Person cloneComponent (String componentName, String componentType, ComponentInterface<?> parentComponent) { return new Person (componentName, parentComponent); }
Note, that the component type has to be passed to the constructor only,
when the business class represents more than one component type. Prototypes of
all business classes must be registered at the factory in the initialization
phase of the application calling the method ComponentFactory.registerPrototype(ComponentInterface)
.
The component-attribute-container composite may be walked through by a visitor
implementing the interface Visitor
. Components,
attributes and containers play the element role in the visitor
design pattern. ACAF uses visitors in many packages to access the
attributes of the business objects. Visitors may also be used, when the
framework is extended or connected to other frameworks. Visitors should off
course not be used to bypass information hiding and to access attributes through
the backdoor! When a business process requires access to an attribute value, an
according access method should be implemented in the business class.
However, the visitor pattern may be extended to business process visitors. For this purpose an abstract base class for your business classes should be implemented, which defines a method, which accepts the business visitor:
public abstract class BaseComponent<C extends BaseComponent<?>> extends Component<C> { protected BaseComponent (String componentName, String componentType, ComponentInterface<?> parentComponent) { super (componentName, componentType, parentComponent); } public abstract void acceptBusinessVisitor (BusinessVisitor visitor) throws AcafException; }
Than you may implement an abstract base class for your business visitors. In the following example, the visitor is used to visit business objects of the classes Address, AddressBook, Person and PhoneNumber. These classes are derived form the base class BaseComponent and implement the method acceptBusinessVisitor(BusinessVisitor). In this method they should call the appropriate callback method of the business visitor passing itself (this) as parameter. When the business visitor is sent to a component, it redirects itself to the business component in the method visitComponent(ComponentInterface) because it can only be used with business components. When the visitor is sent to a container, it forwards itself to all components in the container. Note, that the business visitor doesn't process simple attributes! Concrete business visitors may override the empty callback methods for the business classes and process the passed business objects. Here the sample implementation:
public abstract class BusinessVisitor extends VisitorBase { public void visitAddress (Address address) throws AcafException {} public void visitAddressBook (AddressBook addressBook) throws AcafException {} public void visitBusinessObject (BaseComponent businessObject) throws AcafException { businessObject.acceptBusinessVisitor (this); } public void visitComponent (ComponentInterface<?> component) throws AcafException { visitBusinessObject ((BaseComponent) component); } public <C extends ComponentInterface<?>> void visitComponentContainer (ComponentContainerInterface<?, C> container) throws AcafException { container.visitComponents (this); } public <C extends ComponentInterface<?>> void visitContainerReference (ContainerReferenceInterface<?, C> componentReference) throws AcafException { componentReference.visitComponent (this); } public void visitPerson (Person person) throws AcafException {} public void visitPhoneNumber (PhoneNumber phoneNumber) throws AcafException {} public <C extends ComponentInterface<?>> void visitSequenceContainer (SequenceContainerInterface<?, C> container) throws AcafException { container.visitComponents (this); } }
Attribute play the subject role in the
observer design pattern and are observable by concrete observers
implementing the interface AttributeObserver
.
Observers are especially useful to update a GUI element when the attribute value
has been changed. The observer interface may be implemented by mediators, which
exchange the value between the attribute and the GUI element (see
mediator design pattern). To attach an observer to an attribute, a visitor
may be used, which is sent to the attribute of a business object calling the
method ComponentInterface.visitAttribute(String,Visitor)
:
public class ObserverAttacher extends VisitorBase { private AttributeObserver observer; public ObserverAttacher (AttributeObserver observer) { this.observer = observer; } public void visitAttribute (AttributeInterface<?> attribute) { attribute.attachObserver (observer); } public <C extends ComponentInterface<?>> void visitComponentContainer (ComponentContainerInterface<?, C> container) { container.attachObserver (observer); } public <C extends ComponentInterface<?>> void visitComponentReference (ComponentReferenceInterface<?, C> reference) { reference.attachObserver (observer); } public <C extends ComponentInterface<?>> void visitContainerReference (ContainerReferenceInterface<?, C> reference) { reference.attachObserver (observer); } public <C extends ComponentInterface<?>> void visitSequenceContainer (SequenceContainerInterface<?, C> container) { container.attachObserver (observer); } }
All classes of ACAF are thread safe by synchronizing the methods, which modify the state of a class, except documented otherwise. This means for example, that attributes cannot be modified by another thread, when their observers are notified. So attribute observers should not perform any long running or blocking operations!
Also the methods accepting a visitor are synchronized. This means, that a component cannot be modified while a visitor is sent to its attributes and containers cannot be modified while a visitor is sent to its components. So, using the visitor is the thread-safe way to iterate through a container in contrast to an iterator! The framework itself uses only visitors and never iterators to iterate through the components in a container.
The package de.tmasoft.acaf.msgcat
contains some interfaces and classes,
which are used to internationalize applications using the framework. The
attribute classes and the component base class Component
can be initialized with a message catalog
implementing the interface MessageCatalogInterface
,
which is used to get the localized attribute keyword or component type.
Message catalogs are used to get a localized text for a key. In Java usually
property resource files are used for this purpose, and the base class MessageCatalogBase
can be used to access such a resource
file by a ResourceBundle
. This class uses the utility class
ThreadLocale
to get the texts with the locale of
the current thread. So, in a multi-threaded application a thread specific locale
can be set. This is especially useful in server applications, which should use
the locale of the client, which issues the service request (i.e. a browser).
However, applications are free to implement the message catalog interface in any other way to get the localized texts from other sources.
ACAF itself uses property resource files with the base name AcafMessageTable as source for the localized error messages and texts. These resource files are located in the package de.tmasoft.acaf.msgcat. The package contains currently the following localizations:
When other localizations are required, the appropriate resource file has to be created and placed in this package. To create a new property resource file, the message source file AcafMessageTable.msg, located in the same package, may be extended and compiled with the Message Compiler . The author would be pleased if you sent the extended message file to him, so that other users may benefit from your localization.
The following project plan shows the activities of the implementation phase, which can be started as soon as the business class model has reached a stable state. Implementation starts with defining constants for attribute keywords and component types. When the applications should be localized, the localized texts for attribute keywords, component types and enumeration values should be defined. Then the enumeration values has to be implemented. After that, all prerequisites are complied to implement the business classes and optionally a facade to serialize/deserialize the business objects to/from XML.
When the business classes are implemented, the dependent business processes may be implemented.
When the business objects should be stored in a relational database, the
database tables and constants for the database table and column names can be
immediately defined. These database constants, the constants for attributes and
component types and the enumeration values are required to implement the O/R
mapping using the mediators of the package de.tmasoft.acaf.jdbc
. Then
the database access facade, used to retrieve and store business objects, can be
completely implemented. Note, that it is not necessary that any business classes
are implemented to start implementing the O/R mapping or the database access
facade!
Define component type constants
Define attribute keyword constants
Define localized component types (optional)
Define localized attribute keywords (optional)
Define localized enumeration values (optional)
Implement enumeration values
Implement business classes
Implement XML serialization facade
Implement business processes
Define database tables
Define database table constants
Implement O/R mapping
Implement persistence access facade
This section illustrates how top implement an application step by step. For this purpose two sample applications are used.
The first sample applications implements an address book, which contains contact persons. To each person an arbitrary number of phone numbers and addresses can be added.
The second sample application implements a competition management, which can be used to run a competition where teams play against each other like a basketball, soccer or football competition. The teams can be registered with their players for the competition. The competition may be structured with rounds, groups and pairings. Team-classifications represent a team playing in a group or pairing. Teams are usually distributed to the groups of the first round by a draw. So, the team-classifications in the groups of the first round usually reference a registered team. In the following rounds teams, which reached a certain place in the preceding round play against each other. So the team-classifications reference a team-classification at this position in a group or pairing of the preceding round, e.g. "First of group A" against "Second of group B".
The complete source code of the sample applications is part of the ACAF download.
The following implementation steps may start, when the business class model has reached a stable state.
One of the first steps is to define string constants for the component types.
Component types are used by the ComponentFactory
to
create business objects, as XML tags by XML serialization and by O/R mapping to
map the business objects to the appropriate database tables. The naming
conventions for component types may is up to you. When the application is
localized, the component type strings are usually not shown to the user of the
application and so you may use a technical notation like a class name. Otherwise
you should use a more user friendly notation. Component names are also used as
XML node names by the XML serialization classes of the package de.tmasoft.acaf.xml
. When you have a naming conventions for XML tags, this may
also influence the notation of the component type strings. However, component
types should not contain blanks or special characters like quotes or dots.
The following shows the definition of the component type constants of the address book application. The constants should be defined in a central package and component, because they are usually needed by many other components, which work with the business classes. It is a matter of taste, if the constants are defined in a separate class or in the related business class itself. In the sample applications separate classes are used to define the business constants.
public class Constants
{
...
public static class ComponentTypes
{
public static final String ADDRESS = "Address";
public static final String ADDRESS_BOOK = "Address-book";
public static final String PERSON = "Person";
public static final String PHONE_NUMBER = "Phone-Number";
}
}
Another initial step is to define string constants for attribute keywords.
Attribute keywords are used by the XML serialization as attribute names inside
the XML tags and by O/R mapping to map an attribute value to one or more
database columns. They may also be used to send visitors to a certain attribute
of a business class. When the application is not localized, attributes are
inserted into the error messages of the exceptions thrown by the attribute
classes. In this case attribute keywords may be visible to a user and should
have a user friendly notation. When attribute keywords are localized anyway, a
technical notation may be used. When you intend to store the business objects in
a relational database, the column names may be used as attribute keywords. This
makes the implementation of O/R mapping a bit easier, because there is no need
to initialize the attribute mediators of the de.tmasoft.acaf.jdbc
package with separate column names.
The following shows the definition of the attribute keyword constants of the address book application. The constants should be defined in a central package and component, because they are usually needed by many other components, which work with the business classes. It is a matter of taste, if the constants are defined in a separate class or in the related business class itself. In the sample applications separate classes are used to define the business constants. So the business classes are not polluted with the constant definitions.
public class Constants
{
public static class Attributes
{
public static class Address
{
public static final String CITY = "City";
public static final String COUNTRY = "Country";
public static final String DESCRIPTION = "Description";
public static final String STREET = "Street";
public static final String ZIP_CODE = "Zip-code";
}
public static class AddressBook
{
public static final String CONTACTS = "Contacts";
}
public static class Person
{
...
}
public static class PhoneNumber
{
...
}
}
}
When your application should be localized, or you want that user friendly strings for the component types are used by the framework (e.g. in error messages), you should define localized component types. In the sample applications message files are used to define the localized strings. These files are compiled by the Message Compiler to create the resource property files and enumeration constants for the keys to access the localized texts.
The following shows the definition of the component type for an address in English and German.
.MESSAGEKEY=CNT_TXT_COMPTYPE_ADDRESS
.SEVERITY=INFORMATION
.LANGUAGE=en
.TEXT
Address
.END
.LANGUAGE=de
.TEXT
Adresse
.END
In localized applications you should also define locale specific strings for the attribute keywords. Localized attribute keywords are returned by the attribute classes as display keyword and inserted into the error messages of the exceptions thrown by the attribute classes and so be visible to a user. Localized attribute keywords may also be used as label texts on a GUI.
In the sample applications message files are used to define the localized strings. These files are compiled by the Message Compiler to create the resource property files and enumeration constants for the keys to access the localized texts. The following shows the definition of the first name for a person in English and German.
.MESSAGEKEY=CNT_TXT_ATTR_PERSON_FIRST_NAME
.SEVERITY=INFORMATION
.LANGUAGE=en
.TEXT
First name
.END
.LANGUAGE=de
.TEXT
Vorname
.END
In localized applications the values of enumeration values should be localized
as well. The localized string of an enumeration value is returned by the methods
EnumValue.toString()
and EnumValue.toString(Locale)
of an enumeration value
and by the method EnumAttribute.getValueAsString()
of an enumeration attribute. Localized enumeration values may also be used as
label texts on a GUI.
In the sample applications message files are used to define the localized strings. These files are compiled by the Message Compiler to create the resource property files and enumeration constants for the keys to access the localized texts. The following shows the definition of the enumeration value mister for the enumeration salutation in English and German.
.MESSAGEKEY=CNT_TXT_ENUM_SALUTATION_MISTER
.SEVERITY=INFORMATION
.LANGUAGE=en
.TEXT
Mister
.END
.LANGUAGE=de
.TEXT
Herr
.END
To access the localized text strings you need to implement a message catalog,
which is used to initialize the component base class and the attributes. The
message catalog must implement the interface MessageCatalogInterface
and may extend the base class
MessageCatalogBase
. The following shows the
implementation of the message catalog singleton of the address book
application. Note, that the enumeration ContactsTextTable, which defines
the keys to access the localized texts, is created by the Message Compiler
from the message source file.
public class ContactsTextCatalog extends MessageCatalogBase
{
private static final String MESSAGE_FILE_NAME = "de.tmasoft.acaf.sample.contacts.msgcat.ContactsTextTable";
public static final ContactsTextCatalog theInstance = new ContactsTextCatalog ();
private ContactsTextCatalog ()
{
super (MESSAGE_FILE_NAME);
}
public synchronized String getAndFormatMessage (ContactsTextTable messageKey,
Object[] parameters)
{
return super.getAndFormatMessage (messageKey, parameters);
}
public synchronized String getMessage (ContactsTextTable messageKey)
{
return super.getMessage (messageKey);
}
}
After the localized enumeration values has been defined the enumeration values
can be implemented, which are a prerequisite to initialize the enumeration
attributes of the business classes. Enumeration values participating at the
framework must at least implement the interface EnumValue
and may extend the base class EnumValueBase
. Enumeration values can also be
implemented as Java enums. The following shows the implementation of the
enumeration Salutation used for the salutation attribute of a
person in the address book application. Note, that the toString()
methods of this enumeration class return the localized text values from the
message catalog. If you don't intend to localize your application, you may also
initialize the enumeration values with text constants returned by these methods.
public enum Salutation implements EnumValue<Salutation> { MISS (ContactsTextTable.CNT_TXT_ENUM_SALUTATION_MISS), MISTER (ContactsTextTable.CNT_TXT_ENUM_SALUTATION_MISTER), UNDEFINED (ContactsTextTable.CNT_TXT_ENUM_SALUTATION_UNDEFINED); private ContactsTextTable valueKey; private Salutation (ContactsTextTable valueKey) { this.valueKey = valueKey; } public String toString () { return ContactsTextCatalog.theInstance.getMessage (valueKey); } public String toString (Locale locale) { return ContactsTextCatalog.theInstance.getMessage (valueKey, locale); } }
At this point the initial work is done and we are ready to implement the
business classes. All business classes of the sample applications extend the
component base class Component
and initialize this
class with the parent component, the component type, the message catalog and the
message key to get the localized component type, also denoted as display
type. They further implement the method cloneComponent(), which just
returns an instance of the class itself. In the constructor the attributes are
created and added to the attribute lists of the base class.
The following code block shows the basic implementation of the business class Person of the address book application. All attributes are initialized with the attribute keyword, the message catalog and the message key to get the localized display keyword. The enumeration attribute salutation is initialized with the value list of the enumeration and the initial and default value UNDEFINED. The date attribute dateOfBirth is initialized with a minimum date of 1900-Jan-01, a maximum date of 9999-Dec-31 and a dynamic date format, which formats the date value with a medium format according to the locale of the current thread. The container attributes addresses and phoneNumbers are initialized with the component types used for runtime type checking and component creation.
public class Person extends Component<Person> { private OrderedComponentContainer<Address> addresses; private DateAttribute dateOfBirth; private StringAttribute firstName; private StringAttribute lastName; private OrderedComponentContainer<PhoneNumber> phoneNumbers; private EnumAttribute<Salutation> salutation; public Person (String componentName, ComponentInterface<?> parentComponent) { super (componentName, Constants.ComponentTypes.PERSON, ContactsTextTable.CNT_TXT_COMPTYPE_PERSON, ContactsTextCatalog.theInstance, parentComponent); ContactsTextCatalog messageCatalog = ContactsTextCatalog.theInstance; DynamicDateFormat dateFormat = new DynamicDateFormat (DateFormat.MEDIUM); salutation = new EnumAttribute<Salutation> (Constants.Attributes.Person.SALUTATION, ContactsTextTable.CNT_TXT_ATTR_PERSON_SALUTATION, messageCatalog, Arrays.asList (Salutation.values ()), Salutation.UNDEFINED, Salutation.UNDEFINED); addAttribute (salutation); firstName = new StringAttribute (Constants.Attributes.Person.FIRST_NAME, ContactsTextTable.CNT_TXT_ATTR_PERSON_FIRST_NAME, messageCatalog); addAttribute (firstName); lastName = new StringAttribute (Constants.Attributes.Person.LAST_NAME, ContactsTextTable.CNT_TXT_ATTR_PERSON_LAST_NAME, messageCatalog); addAttribute (lastName); dateOfBirth = new DateAttribute (Constants.Attributes.Person.DATE_OF_BIRTH, ContactsTextTable.CNT_TXT_ATTR_PERSON_DATE_OF_BIRTH, messageCatalog, null, null, false, CalendarDateFunctions.getMinimumDate (1900), CalendarDateFunctions.getMaximumDate (9999), dateFormat); addAttribute (dateOfBirth); addresses = new OrderedComponentContainer<Address> (Constants.Attributes.Person.ADDRESSES, ContactsTextTable.CNT_TXT_ATTR_PERSON_ADDRESSES, messageCatalog, Constants.ComponentTypes.ADDRESS); addAttribute (addresses); phoneNumbers = new OrderedComponentContainer<PhoneNumber> (Constants.Attributes.Person.PHONE_NUMBERS, ContactsTextTable.CNT_TXT_ATTR_PERSON_PHONE_NUMBERS, messageCatalog, Constants.ComponentTypes.PHONE_NUMBER); addAttribute (phoneNumbers); } public Person cloneComponent (String componentName, String componentType, ComponentInterface<?> parentComponent) { return new Person (componentName, parentComponent); } }
After the business classes has been implemented, a prototype for each component
type has to be registered at the ComponentFactory
.
This has to be be done once in the initialization phase of the application. The
factory is used to create the business objects by the O/R mapping package de.tmasoft.acaf.jdbc
, the XML serialization package de.tmasoft.acaf.xml
and the command line parser package de.tmasoft.acaf.parser
. The
following code block shows the prototype registration method of the address book
application.
public static void registerPrototypes () { ComponentFactory factory = ComponentFactory.theInstance; factory.registerPrototype (new Address ("Prototype", null)); factory.registerPrototype (new Person ("Prototype", null)); factory.registerPrototype (new PhoneNumber ("Prototype", null)); }
Now you may create business objects and set their attribute values using the
command line parser visitors of the package de.tmasoft.acaf.parser
. You
may implement this as JUnit tests to test your business processes, to test O/R
mapping or to test XML serialization. The following code block shows how this
may be implemented. First a ParseAndSetVisitor
,
a PrintVisitor
and an empty AddressBook
are created. The setCommand visitor is initialized with a command line,
which creates one person with an address and a phone number. Note, that the
locale of the current thread is set to ENGLISH before the visitor is sent
to the address book. This is required, because the command contains a date value
in the English locale format and the English enumeration value Mister for
the salutation. After the setCommand visitor has been sent to the address
book, it contains one contact person. This may be verified by sending the
printer visitor to the address book and printing the result on the
console.
String command; ParseAndSetVisitor setCommand = new ParseAndSetVisitor (); PrintVisitor printer = new PrintVisitor (true, 20); AddressBook addressBook = new AddressBook ("Address Book"); // // Create the command with English locale // ThreadLocale.theInstance.set (Locale.ENGLISH); command = "Contacts P1 Salutation Mister First-name John Last-name Smith Date-of-birth \"Aug 23, 1977\"" + " Addresses A1 Description Home Street \"Fleetstreet 32\" Zip-Code 88765 City Middletown Country \"United Kingdom\";" + " Phone-numbers PN1 Description Private Number \"0887 / 8473522897\""; setCommand.setCommand (command); // // Send the set-command visitor to the address book // addressBook.acceptVisitor (setCommand); // // Print the content of the address book on the console // addressBook.acceptVisitor (printer); System.out.println (printer.getResult ());
As soon as the business classes are implemented, they may be serialized/
deserialized to/from a XML document or stored and updated to/from a XML file.
The class XMLHandler
may be used for this purpose.
The following code block shows a simple XML facade implemented as singleton,
which can be used to store and update any component in/from a XML file.
public final class XmlFacade { public static final XmlFacade theInstance = new XmlFacade (); private XMLHandler xmlHandler; private XmlFacade () { xmlHandler = new XMLHandler (); } public void saveToFile (File directory, String fileName, ComponentInterface component) throws AcafException { if (!directory.exists ()) { directory.mkdirs (); } xmlHandler.saveToFile (new File (directory, fileName), component); } public void updateFromFile (File xmlFile, ComponentInterface component) throws AcafException { xmlHandler.updateFromFile (xmlFile, component); } }
This XML facade stores the whole business object tree under the passed component
in the XML file. The created XML file contains the string representation of the
attribute values returned by the method AttributeInterface.getValueAsString()
. This means, that
the XML value of the date attribute dateOfBirth of the business class
Person and the XML value of the enumeration attribute salutation
depends on the locale of the thread, which calls the method saveToFile().
This is probably not desired. To solve this problem, so called attribute
value converters of the package de.tmasoft.acaf.io
has to be
registered at the XML handler. To be able to do this, the XML handler has to be
initialized with a ComponentAttributeValueConverterMap
:
public final class XmlFacade { ... private ComponentAttributeValueConverterMap componentAttributeConverterMap; private XmlFacade () { componentAttributeConverterMap = new ComponentAttributeValueConverterMap (); xmlHandler = new XMLHandler (componentAttributeConverterMap); } }
The attribute value converters can now be registered at the component attribute converter map. Date values in XML documents should usually conform to the standard XML date format <xsd:date>. So we register a date attribute value converter for the attribute dateOfBirth of the business class Person. For the salutation attribute we register an enumeration value converter. For this purpose string constants should be defined for the XML representation of the enumeration values.
You may have noticed, that the XML facade does not reference any business class. So it may be completely implemented after the attribute keyword and component type constants has been defined and enumeration values has been implemented.
public final class XmlConstants { ... public static class Values { public static class Salutation { public static final String MISS = "MSS"; public static final String MISTER = "MST"; public static final String UNDEFINED = "UDF"; } } } public final class XmlFacade { ... private DateAttributeValueConverter xmlDateValueConverter = new DateAttributeValueConverter ("yyyy-MM-dd"); ... private void registerAttributeValueConvertersPerson () { AttributeValueConverterMap attributeValueConverterMap; List<EnumValuePair<Salutation>> salutationValuePairs; EnumAttributeValueConverter<Salutation> salutationAttributeConverter; // // Create and register the attribute value converter map for the person // attributeValueConverterMap = new AttributeValueConverterMap (); componentAttributeConverterMap.registerAttributeValueConverter (Constants.ComponentTypes.PERSON, attributeValueConverterMap); // // Register the date value converter for the attribute dateOfBirth // attributeValueConverterMap.registerAttributeValueConverter (Constants.Attributes.Person.DATE_OF_BIRTH, xmlDateValueConverter); // // Register the enumeration value converter for the attribute salutation // salutationValuePairs = Arrays.asList (new EnumValuePair<Salutation>(Salutation.MISS, XmlConstants.Values.Salutation.MISS), new EnumValuePair<Salutation>(Salutation.MISTER, XmlConstants.Values.Salutation.MISTER), new EnumValuePair<Salutation>(Salutation.UNDEFINED, XmlConstants.Values.Salutation.UNDEFINED)); salutationAttributeConverter = new EnumAttributeValueConverter<Salutation> (salutationValuePairs); attributeValueConverterMap.registerAttributeValueConverter (Constants.Attributes.Person.SALUTATION, salutationAttributeConverter); } }
This section illustrates how the visitor design pattern can be used to implement a business class specific visitor. Business visitors can be used for business processes, which operate on the the whole business object tree or a part of it. The may be used to transform the business object tree, to validate the business objects or to extract data for external systems.
Here we implement a visitor, which searches the address book for persons with a specified part of the first name, last name, city or street. First we implement the base class BaseComponent for all business classes of the address book application and the base class BusinessVisitor as described in the section Visitors above. Then all business classes are derived from this base class and implement the method acceptBusinessVisitor(). In the Person class we further implement two business methods, which return the first and last name:
public class Person extends BaseComponent<Person> { ... public void acceptBusinessVisitor (BusinessVisitor visitor) throws AcafException { visitor.visitPerson (this); } public String getFirstName () { return firstName.getValue (); } public String getLastName () { return lastName.getValue (); } ... }
Now we implement a concrete private visitor in the class AddressBook, which searches for all persons, which contain a search string in their first or last name or in the city or street of an address. The persons are collected in a hash set and returned by the method getSearchResult():
public final class AddressBook extends BaseComponent<AddressBook> { ... private class SearchVisitor extends BusinessVisitor { private String searchString; private HashSet<Person> searchResult = new HashSet<Person> (); private SearchVisitor (String searchString) { this.searchString = searchString; } private Collection<Person> getSearchResult () { return searchResult; } public void visitAddress (Address address) throws AcafException { if (address.getCity ().contains (searchString) || address.getStreet ().contains (searchString)) { searchResult.add ((Person) address.getParent ()); } } public void visitAddressBook (AddressBook addressBook) throws AcafException { addressBook.visitAttribute (Constants.Attributes.AddressBook.CONTACTS, this); } public void visitPerson (Person person) throws AcafException { if (person.getFirstName ().contains (searchString) || person.getLastName ().contains (searchString)) { searchResult.add (person); } else { person.visitAttribute (Constants.Attributes.Person.ADDRESSES, this); } } } }
This visitor may now be used by the method findContacts() in the business class AddressBook:
public final class AddressBook extends BaseComponent<AddressBook> { ... public Collection<Person> findContacts (String searchString) throws AcafException { SearchVisitor visitor = new SearchVisitor (searchString); visitor.visitAddressBook (this); return visitor.getSearchResult (); } }
The package de.tmasoft.acaf.jdbc
contains classes, which can be used to
store and retrieve business objects in/from a relational database. This is also
denoted as object-relational mapping. The classes use simple JDBC statements and
result sets for this purpose, which are created by a JDBC driver and accessed by
the interfaces defined in the package java.sql
. The JDBC package of
ACAF provides two main different ways to store business objects in the
database:
The first method, storing with updatable result sets, is the easier one and more elegant. This method requires only SQL select statements, which are used to create the result sets. When the business objects should be stored, the related rows in the result sets are updated, when new business objects should be inserted into the database, new rows are inserted into the result sets and when business objects should be deleted, the related rows are removed from the result sets. The method requires, that the JDBC driver supports updatable result sets, which is quite common today. This is also the preferred method to be used with ACAF because it is very transparent for you. When you use this method, you should also define foreign key constraints with the option ON DELETE CASCADE between the tables containing the child and parent business objects. Otherwise orphaned child business objects remain in the database. When your database doesn't support cascaded deletes or you cannot define such constraints you need to implement and call an explicit method to delete a business object with all children from the database.
The second method, storing with insert, update and delete statements may be used, when your JDBC driver doesn't support updatable result set, or you think this is more performant. You don't have to implement these statements yourself, they are generated by the classes of the JDBC package, as we will see in the following.
To illustrate, how O/R mapping works with ACAF, we start with the
definition of a simple database facade, which provides methods to store and
update a single business object, the direct child components of a business
object, the child components with all subjacent child components and the full
business object tree. The database facade is implemented as singleton and may be
initialized with a Connection
, a DataSource
or the JDBC driver class name, URL, user name and password to create a
connection via a JDBC driver.
public final class SimpleDatabaseFacade { public static final SimpleDatabaseFacade theInstance = new SimpleDatabaseFacade (); private JdbcConnectionAccessInterface connectionAccess; private SimpleDatabaseFacade () {} public void initDatabaseFacade (Connection connection) throws JdbcException { connectionAccess = new JdbcSimpleConnectionAccess (connection); } public synchronized void initDatabaseFacade (DataSource dataSource) throws JdbcException { connectionAccess = new JdbcThreadManagedDataSourceConnectionAccess (dataSource); } public void initDatabaseFacade (String jdbcDriverClassName, String databaseUrl, String username, String password) throws JdbcException { try { Class.forName (jdbcDriverClassName).newInstance (); } catch (Exception exc) { throw new JdbcGeneralError (exc); } try { Connection connection = java.sql.DriverManager.getConnection (databaseUrl, username, password); initDatabaseFacade (connection); } catch (SQLException exc) { throw new JdbcGeneralError (exc); } } public void storeChildComponents (ComponentInterface<?> component) throws AcafException, JdbcException { ... } public void storeChildComponentsFull (ComponentInterface<?> component) throws AcafException, JdbcException { ... } public void storeComponentAttributes (ComponentInterface<?> component) throws AcafException, JdbcException { ... } public void storeComponentFull (ComponentInterface<?> component) throws AcafException, JdbcException { ... } public void updateChildComponents (ComponentInterface<?> component) throws AcafException { ... } public void updateChildComponentsFull (ComponentInterface<?> component) throws AcafException { ... } public void updateComponentAttributes (ComponentInterface<?> component) throws AcafException { ... } public void updateComponentFull (ComponentInterface<?> component) throws AcafException { ... } }
This facade provides already a very flexible interface to store and retrieve business object sub-trees. You may notice, that it is very generic and doesn't reference any concrete business classes, so you may provide this facade in a very early project implementation phase! However, to implement the store and update methods some initial work is required.
The first step is to define constants for your database table and column names and for the database representation of enumeration values:
class DatabaseConstants { private DatabaseConstants() {} public static class Columns { public static class Persons { public static final String PERSON_ID = "PERSON_ID"; public static final String DATE_OF_BIRTH = "DATE_OF_BIRTH"; public static final String FIRST_NAME = "FIRST_NAME"; public static final String LAST_NAME = "LAST_NAME"; public static final String SALUTATION = "SALUTATION"; } ... } public static class Tables { public static final String ADDRESSES = "Addresses"; public static final String PERSONS = "Persons"; public static final String PHONE_NUMBERS = "PhoneNumbers"; ... } public static class Values { public static class Salutation { public static final String MISS = "MSS"; public static final String MISTER = "MST"; public static final String UNDEFINED = "UDF"; } ... } }
The next step is to define the SQL select statements, which are used to create
the JDBC result sets. The class SqlSelectStatements in the following
sample code block is initialized with a delimiter character, which is used to
enclose the table and column names in the SQL select statements. The delimiter
character can be retrieved from the database meta-data calling the method
DatabaseMetaData.getIdentifierQuoteString()
. The class
implements two select statements to select persons of the address book
application. The method selectPerson() creates a select statement, which
selects exactly one person with the primary key. This statement is used by the
store and update methods when exactly one person component should be stored or
updated. The method selectPersonsOfAddressBook() selects all persons of
the address book. Note, that the sample address book
application supports only one address book, so there is no foreign key for the
address book in the PERSONS table. This select statement is used, when
all persons of the address book should be stored or updated. According
statements has to be implemented for all database tables.
When a business object is stored in more than one table, don't use SQL joins in the select statements! This may produce non-updatable result sets. Instead define a separate select statement for each table. A general recommendation is, to keep the select statements as simple as possible. This restriction does of course not apply, when you do not intend to store your business objects in the database via the result sets.
class SqlSelectStatements { private String dl; SqlSelectStatements (String sqlDelimiter) { this.dl = sqlDelimiter; } private String getColumnsPersonAll () { return dl + DatabaseConstants.Columns.Persons.PERSON_ID + dl + "," + dl + DatabaseConstants.Columns.Persons.DATE_OF_BIRTH + dl + "," + dl + DatabaseConstants.Columns.Persons.FIRST_NAME + dl + "," + dl + DatabaseConstants.Columns.Persons.LAST_NAME + dl + "," + dl + DatabaseConstants.Columns.Persons.SALUTATION + dl; } String selectPerson () { String columns = getColumnsPersonAll (); return "SELECT " + columns + " FROM " + dl + DatabaseConstants.Tables.PERSONS + dl + " WHERE " + dl + DatabaseConstants.Columns.Persons.PERSON_ID + dl + "=?"; } String selectPersonsOfAddressBook () { String columns = getColumnsPersonAll (); return "SELECT " + columns + " FROM " + dl + DatabaseConstants.Tables.PERSONS + dl; } ... }
The JDBC attribute mediators are used to exchange the attribute values between an attribute of a business class and a result set created by a SQL select statement. The class AttributeMediators, which is shown in the following code block implements methods, which create attribute mediator list for the business classes, in this case for the business class Person. These mediator lists are used later by other mediator classes to exchange the attribute values between a business object and a result set row. The lists returned by this class contain a mediator for each attribute of the business class. Note, that it doesn't matter if the mediator list is combined later with a result set, which contains not all columns for the attributes mediators. The related attributes are just not updated from or stored in the database in this case, but no error occurs.
Simple attributes are usually mapped to one database column. However, there are
also attributes, which may be mapped to more than one column like attributes
implementing the interfaces DurationAttributeInterface
, PrecisionDateAttributeInterface
or SetAttributeInterface
. In the following example, the
JdbcDateAttributeMediator
is used to map the date
attribute dateOfBirth to a SQL date column and for the other attributes
the JdbcSimpleAttributeMediator
is used to map the
values to a SQL string column. Note, that the mediator for the salutation
attribute is initialized with an attribute value converter, to convert the
enumeration values to the string representation used as database value.
class AttributeMediators { private List<? extends JdbcAttributeMediatorInterface> mediatorsPerson = null; ... List<? extends JdbcAttributeMediatorInterface> getMediatorsPerson () { List<EnumValuePair<Salutation>> salutationValuePairs; EnumAttributeValueConverter<Salutation> salutationAttributeConverter; if (mediatorsPerson == null) { // // Create an enumeration attribute value converter for the // salutation // salutationValuePairs = Arrays.asList (new EnumValuePair<Salutation>(Salutation.MISS, DatabaseConstants.Values.Salutation.MISS), new EnumValuePair<Salutation>(Salutation.MISTER, DatabaseConstants.Values.Salutation.MISTER), new EnumValuePair<Salutation>(Salutation.UNDEFINED, DatabaseConstants.Values.Salutation.UNDEFINED)); salutationAttributeConverter = new EnumAttributeValueConverter<Salutation> (salutationValuePairs); // // Create the attribute mediator list // mediatorsPerson = Arrays.asList (new JdbcDateAttributeMediator (Constants.Attributes.Person.DATE_OF_BIRTH, DatabaseConstants.Columns.Persons.DATE_OF_BIRTH), new JdbcSimpleAttributeMediator (Constants.Attributes.Person.FIRST_NAME, DatabaseConstants.Columns.Persons.FIRST_NAME), new JdbcSimpleAttributeMediator (Constants.Attributes.Person.LAST_NAME, DatabaseConstants.Columns.Persons.LAST_NAME), new JdbcSimpleAttributeMediator (Constants.Attributes.Person.SALUTATION, salutationAttributeConverter, DatabaseConstants.Columns.Persons.SALUTATION)); } return mediatorsPerson; } ... }
The JDBC component mediators are used to exchange the component name, the component type and the index of the component in a sequence container between the business class and a result set created by a SQL select statement. They are further used to fill the parameters of the WHERE clause in the SQL select statements. So the order of the column names specified for the component name must correspond to the order of the parameters specified in the select statement! The component type needs only be stored in the database, when the database column contains business objects with different component types. The component name of a component and optionally for its parent component(s) is usually mapped to the key columns of the database table.
The class ComponentMediators, which is shown in the following code block implements methods, which create a component mediator for the business classes of the address book application. The component name of a Person is mapped to the primary key column PERSON_ID. The component names of the business classes Address and PhoneNumber are mapped to the primary key columns ADDRESS_ID and PHONE_NUMBER_ID, and the component name of their parent component, which is a Person are mapped to the foreign key column PERSON_ID. For these business classes it is not required to store the component type or the index.
class ComponentMediators { private JdbcComponentMediatorInterface mediatorAddress = null; private JdbcComponentMediatorInterface mediatorPerson = null; private JdbcComponentMediatorInterface mediatorPhoneNumber = null; JdbcComponentMediatorInterface getMediatorAddress () { if (mediatorAddress == null) { mediatorAddress = new JdbcSimpleComponentMediator (new String[] { DatabaseConstants.Columns.Addresses.ADDRESS_ID, DatabaseConstants.Columns.Addresses.PERSON_ID }); } return mediatorAddress; } JdbcComponentMediatorInterface getMediatorPerson () { if (mediatorPerson == null) { mediatorPerson = new JdbcSimpleComponentMediator (new String[] { DatabaseConstants.Columns.Persons.PERSON_ID }); } return mediatorPerson; } JdbcComponentMediatorInterface getMediatorPhoneNumber () { if (mediatorPhoneNumber == null) { mediatorPhoneNumber = new JdbcSimpleComponentMediator (new String[] { DatabaseConstants.Columns.PhoneNumbers.PHONE_NUMBER_ID, DatabaseConstants.Columns.PhoneNumbers.PERSON_ID }); } return mediatorPhoneNumber; } }
Component attribute mediators are used to exchange simple attribute values of a
component with a JDBC result set. The result set is expected to contain one row,
which represents the component and contains the values for the attributes. The
class JdbcResultSetComponentAttributesMediator
is
used for this purpose. This class is initialized with the database connection
used to execute the SQL select statement, which creates the result set, the
component mediator for the component, the SQL select statement and the attribute
mediator list used to exchange the attribute values. The component mediator is
used, to fill the parameters in the WHERE clause of the SQL select
statement and to update the result set fields with the component name and
component type.
Usually you will create one component attribute mediator for each business class with a select statement, which selects the columns for all attributes of the business class. However, when a specific business process doesn't require all attributes of a business object, you may decide to implement a statement, which selects only the columns for the required attributes (e.g. for performance reasons). In this case you have to instantiate an additional mediator with the alternative select statement. Note, that the other initialization parameters remain the same, also the attribute mediator list. The attribute mediators, for which no columns are present in the result set are just ignored.
The following code block shows the instantiation of a component attribute mediator for the business class Person of the address book application. The classes implemented so far are used to initialize the mediator.
class ComponentAttributeMediators { private JdbcConnectionAccessInterface databaseConnectionAccess; private SqlSelectStatements selectStatements; private ComponentMediators componentMediators; private AttributeMediators attributeMediators; private JdbcResultSetComponentAttributesMediator mediatorAddress = null; private JdbcResultSetComponentAttributesMediator mediatorPerson = null; private JdbcResultSetComponentAttributesMediator mediatorPhoneNumber = null; ComponentAttributeMediators (JdbcConnectionAccessInterface databaseConnectionAccess, ComponentMediators componentMediators, AttributeMediators attributeMediators, SqlSelectStatements selectStatements) { this.databaseConnectionAccess = databaseConnectionAccess; this.attributeMediators = attributeMediators; this.componentMediators = componentMediators; this.selectStatements = selectStatements; } JdbcResultSetComponentAttributesMediator getMediatorPerson () { if (mediatorPerson == null) { mediatorPerson = new JdbcResultSetComponentAttributesMediator (databaseConnectionAccess, componentMediators.getMediatorPerson (), selectStatements.selectPerson (), attributeMediators.getMediatorsPerson ()); } return mediatorPerson; } ... }
The JDBC component container mediators are used to exchange the data of the
business classes in containers with the rows of JDBC result sets. Each row in a
result set represents one component in the container and the relationship is
determined by the component name. The class JdbcResultSetComponentContainerMediator
is used fo this
purpose. When a component container should be stored in the database, this class
updates the result set rows with the attribute values of the components in the
container, when new components has been inserted into the container, id adds
rows in the result set and when components has been removed from the container
it deletes the corresponding rows in the result set. When the container should
be updated from the database, it just works the other way round; attributes of
existing components are updated with the data from the result set, non existing
components are created in the container, and components, which are not
represented by a row in the result set are removed from the container.
For this purpose, the mediator is initialized with a database connection, used to execute the SQL statement, which creates the result set, the parent component mediator, used to fill the parameters in the WHERE clause of the select statement, the select statement, the component mediator for the components in the container and a list of attribute mediators, used to exchange the values between the attributes of the components in the container and the result set. See also the class description for further information how this mediator works in detail.
One instance of this mediator is required for a container and certain SQL select statement. Usually you will implement a select statement, which selects the columns, which are required to update and store all attributes of the business objects in the container. However, when a specific business process doesn't require all attributes of the business objects, you may decide to implement a statement, which selects only the columns for the required attributes (e.g. for performance reasons). In this case you have to instantiate an additional mediator with the alternative select statement. Note, that the other initialization parameters remain the same, also the attribute mediator list. The attribute mediators, for which no columns are present in the result set are just ignored.
The following code block shows how the component container mediator for the addresses container of the business class Person is initialized using the classes we implemented so far.
class ComponentContainerMediators { private JdbcConnectionAccessInterface databaseConnectionAccess; private AttributeMediators attributeMediators; private ComponentMediators componentMediators; private SqlSelectStatements selectStatements; private JdbcResultSetComponentContainerMediator mediatorAddresses = null; ... public ComponentContainerMediators (JdbcConnectionAccessInterface databaseConnectionAccess, ComponentMediators componentMediators, AttributeMediators attributeMediators, SqlSelectStatements selectStatements) { this.databaseConnectionAccess = databaseConnectionAccess; this.attributeMediators = attributeMediators; this.componentMediators = componentMediators; this.selectStatements = selectStatements; } JdbcResultSetComponentContainerMediator getMediatorAddresses () { if (mediatorAddresses == null) { mediatorAddresses = new JdbcResultSetComponentContainerMediator (databaseConnectionAccess, componentMediators.getMediatorPerson (), Constants.Attributes.Person.ADDRESSES, true, selectStatements.selectAddressesOfPerson (), componentMediators.getMediatorAddress (), attributeMediators.getMediatorsAddress ()); } return mediatorAddresses; } ... }
Before we can put it all together, we need some methods for transaction
handling. As you may have seen in the beginning of this chapter, a connection
access object has been created by the initialization methods. When the
facade is initialized with a DataSource
, the class JdbcThreadManagedDataSourceConnectionAccess
is used,
otherwise JdbcSimpleConnectionAccess
. The first
class manages the database connection (and so the transactions) on a per thread
basis. This means each thread has its own connection and so the persistence
facade can be used in a multi-threaded environment like an application server.
The connection is retrieved from the data source. The simple connection access
just wraps the connection object and can only be used in single user
applications.
The following code block shows the quite straight forward implementation of the transaction methods. Note, that the method closeTransaction() releases the connection, which means, that connections retrieved from a data source are put back into the connection pool. Releasing a simple database connection has no effect.
private void startTransaction () throws JdbcException { Connection connection = connectionAccess.getConnection (); try { databaseConnection.setAutoCommit (false); databaseConnection.setTransactionIsolation (Connection.TRANSACTION_READ_COMMITTED); } catch (JdbcException exc) { closeTransaction (); throw exc; } } private void commitTransaction () throws JdbcException { Connection connection; try { connection = connectionAccess.getConnection (); connection.commit (); } catch (SQLException sqlException) { throw new JdbcGeneralError (sqlException); } } private void rollbackTransaction () throws JdbcException { Connection connection; try { connection = connectionAccess.getConnection (); connection.rollback (); } catch (SQLException sqlException) { throw new JdbcGeneralError (sqlException); } } private void closeTransaction () throws JdbcException { if (connectionAccess != null) { connectionAccess.releaseConnection (); } }
Now we can use all these mediator factory classes to initialize the database facade. In an initialization method all factories are created. The delimiter for table and column names is retrieved from the meta-data of the database connection and used to initialize the SQL statement factory.
In the last two lines two instances of the class IOFacade
are created. The IO facade provides an interface,
which can be used to store, insert, update and delete components in/from an
arbitrary datastore. This may be a file, a database or anything else. The
IO facade must be initialized with processors, which do the actual work. The
facade ioFacadeComponentAttributes is used to store and update only the
simple attributes of a business object (not the containers). The facade
ioFacadeChildComponents is used to store and update only the
container attributes of a business object. This provides the condition to
implement the store and update methods of the persistence facade as we will see
later.
public final class SimpleDatabaseFacade { private String sqlDelimiter = ""; private SqlSelectStatements selectStatements; private ComponentMediators componentMediators; private AttributeMediators attributeMediators; private ComponentAttributeMediators componentAttributeMediators; private ComponentContainerMediators componentContainerMediators; private IOFacade ioFacadeComponentAttributes; private IOFacade ioFacadeChildComponents; ... private void initDatabaseFacade () throws JdbcException { Connection connection = connectionAccess.getConnection (); try { componentMediators = new ComponentMediators (); attributeMediators = new AttributeMediators (); sqlDelimiter = connection.getMetaData ().getIdentifierQuoteString (); selectStatements = new SqlSelectStatements (sqlDelimiter); componentAttributeMediators = new ComponentAttributeMediators (connectionAccess, componentMediators, attributeMediators, selectStatements); componentContainerMediators = new ComponentContainerMediators (connectionAccess, componentMediators, attributeMediators, selectStatements); ioFacadeComponentAttributes = new IOFacade (); ioFacadeChildComponents = new IOFacade (); ... } catch (SQLException exc) { throw new JdbcGeneralError (exc); } finally { connectionAccess.releaseConnection(); } } }
Now we still need to initialize the IO facades. The IO facade ioFacadeComponentAttributes, which is used to store and update the simple attributes of a business object, is initialized with the component attributes mediators as store and update processors:
ioFacadeComponentAttributes.registerStoreComponentProcessor (Constants.ComponentTypes.ADDRESS, componentAttributeMediators.getMediatorAddress ()); ioFacadeComponentAttributes.registerStoreComponentProcessor (Constants.ComponentTypes.PERSON, componentAttributeMediators.getMediatorPerson ()); ioFacadeComponentAttributes.registerStoreComponentProcessor (Constants.ComponentTypes.PHONE_NUMBER, componentAttributeMediators.getMediatorPhoneNumber ()); ioFacadeComponentAttributes.registerUpdateComponentProcessor (Constants.ComponentTypes.ADDRESS, componentAttributeMediators.getMediatorAddress ()); ioFacadeComponentAttributes.registerUpdateComponentProcessor (Constants.ComponentTypes.PERSON, componentAttributeMediators.getMediatorPerson ()); ioFacadeComponentAttributes.registerUpdateComponentProcessor (Constants.ComponentTypes.PHONE_NUMBER, componentAttributeMediators.getMediatorPhoneNumber ());
The IO facade ioFacadeChildComponents, which is used to store and update the container attributes of a business object, is initialized with the component container mediators as store and update processors:
ioFacadeChildComponents.registerStoreComponentProcessor (Constants.ComponentTypes.ADDRESS_BOOK, componentContainerMediators.getMediatorPersons ()); ioFacadeChildComponents.registerStoreComponentProcessor (Constants.ComponentTypes.PERSON, componentContainerMediators.getMediatorAddresses ()); ioFacadeChildComponents.registerStoreComponentProcessor (Constants.ComponentTypes.PERSON, componentContainerMediators.getMediatorPhoneNumbers ()); ioFacadeChildComponents.registerUpdateComponentProcessor (Constants.ComponentTypes.ADDRESS_BOOK, componentContainerMediators.getMediatorPersons ()); ioFacadeChildComponents.registerUpdateComponentProcessor (Constants.ComponentTypes.PERSON, componentContainerMediators.getMediatorAddresses ()); ioFacadeChildComponents.registerUpdateComponentProcessor (Constants.ComponentTypes.PERSON, componentContainerMediators.getMediatorPhoneNumbers ());
The last step is to complete the implementation of the store and update methods. Here you can see how the IO facades are used to store the different business object trees. The first method just stores the simple attributes of the business object using the IO facade ioFacadeComponentAttributes. The second method stores the direct child business objects in the containers of the business object using the IO facade ioFacadeChildComponents. The third method stores all child business objects (grandchildren, ...) calling the method storeComponentTree() of the IO facade ioFacadeChildComponents and the last method stores the whole business object tree combining method one and tree.
Note, that all methods, which store child business objects, also delete the business objects from the database, which has been removed from the container of their parent. When foreign keys with the option ON DELETE CASCADE has been defined between the tables containing the child business objects and the tables containing their parents, all children are deleted automatically from the database.
public void storeComponentAttributes (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacadeComponentAttributes.storeComponent (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } } public void storeChildComponents (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacadeChildComponents.storeComponent (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } } public void storeChildComponentsFull (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacadeChildComponents.storeComponentTree (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } } public void storeComponentFull (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacadeComponentAttributes.storeComponent (component); ioFacadeChildComponents.storeComponentTree (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } }
Here you can see how the IO facades are used to update the different business object trees. The implementation is analog to the store methods, calling the update methods of the IO facades.
public void updateComponentAttributes (ComponentInterface<?> component) throws AcafException { try { startTransaction (); ioFacadeComponentAttributes.updateComponent (component); commitTransaction (); } catch (AcafException exception) { rollbackTransaction (); throw exception; } finally { closeTransaction (); } } public void updateChildComponents (ComponentInterface<?> component) throws AcafException { try { startTransaction (); ioFacadeChildComponents.updateComponent (component); commitTransaction (); } catch (AcafException exception) { rollbackTransaction (); throw exception; } finally { closeTransaction (); } } public void updateChildComponentsFull (ComponentInterface<?> component) throws AcafException { try { startTransaction (); ioFacadeChildComponents.updateComponentTree (component); commitTransaction (); } catch (AcafException exception) { rollbackTransaction (); throw exception; } finally { closeTransaction (); } } public void updateComponentFull (ComponentInterface<?> component) throws AcafException { try { startTransaction (); ioFacadeComponentAttributes.updateComponent (component); ioFacadeChildComponents.updateComponentTree (component); commitTransaction (); } catch (AcafException exception) { rollbackTransaction (); throw exception; } finally { closeTransaction (); } }
When no foreign keys with cascaded delete option can be defined between the child and parent database tables, you may need to implement an explicit delete method to avoid, that orphaned children remain in the database. Note, that this method has to be called explicitly when a business object should be deleted from the database. The following code block shows a sample implementation of such a method. Note, that first all children of the business object are deleted by the method deleteComponentTree() and then the business object itself, just in case that foreign keys are defined between the child and parent tables. The DatabaseFacade of the sample competition management application implements such a method.
public void deleteComponent (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacadeChildComponents.deleteComponentTree (component); ioFacadeComponentAttributes.deleteComponent (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } }
When you cannot use updatable result sets to store business objects, you may
register instances of the classes JdbcStoreComponentStatements
and JdbcDeleteComponentStatements
as store and delete
processors at the IO facades. These classes issue explicit SQL INSERT,
UPDATE and DELETE statements to store or delete business objects
in/from the database.
The persistence facade StatementDatabaseFacade of the address book application illustrates how these classes are used. They are initialized with the component and attribute mediators already created and used in the previous steps. For each business class a store processor is registered at the IO facade used to store the business objects. Here the sample registration for a Person:
JdbcStoreComponentStatements storeStatement; storeStatement = new JdbcStoreComponentStatements (connectionAccess, sqlDelimiter); storeStatement.registerStatement (DatabaseConstants.Tables.PERSONS, componentMediators.getMediatorPerson (), attributeMediators.getMediatorsPerson ()); ioFacade.registerStoreComponentProcessor (Constants.ComponentTypes.PERSON, storeStatement);
To delete business objects from the database, delete processors has to be registered at the IO facade used to delete the business objects. Here the sample registration for a Person:
JdbcDeleteComponentStatements deleteStatement; deleteStatement = new JdbcDeleteComponentStatements (connectionAccess, sqlDelimiter); deleteStatement.registerStatement (DatabaseConstants.Tables.PERSONS, componentMediators.getMediatorPerson ()); ioFacade.registerDeleteComponentProcessor (Constants.ComponentTypes.PERSON, deleteStatement);
The method to store a business object completely is implemented like this:
public void storeComponent (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacade.storeComponentTree (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } }
Note, that the store method doesn't delete business objects from the database, which has been removed from a container. To delete a business object from the database an explicit delete method has to be implemented and called. This doesn't work as transparent as with updatable result sets. Here the implementation of the delete method, which deletes the whole business object tree from the database. This works, too, even when the database doesn't support cascaded deletes:
public void deleteComponent (ComponentInterface<?> component) throws AcafException, JdbcException { try { startTransaction (); ioFacade.deleteComponentTree (component); commitTransaction (); } catch (JdbcException jdbcException) { rollbackTransaction (); throw jdbcException; } finally { closeTransaction (); } }
For the most cases, the mediator classes of the de.tmasoft.acaf.jdbc
can
be used to exchange the data between the business objects and the result sets or
statements. Sometimes you have to implement your own special mediator. One
example for such a case is found in the
competition management application. The rounds in this application
may belong to a competition or a super-round. In the first case,
the foreign key column COMPETITION_ID of the database table ROUNDS
contains the component name of the parent competition in the
second case, the foreign key column SUPER_ROUND_ID contains the
component name of the parent super-round. The other column is set
to NULL. This situation cannot be handled by the JdbcSimpleComponentMediator
. So the mediator
JdbcRoundComponentMediator is derived, which overrides the method
updating the result set.
The rounds container is a multi-type sequence container. So the component type and the index of the rounds in the container are stored in the database. For the component type a component type converter is used, because the database value is different from the type string. So the constructor initializes the simple component mediator with the key columns ROUND_ID, COMPETITION_ID and SUPER_ROUND_ID, the column ROUND_TYPE containing the component type of the round the component type converter, the column SEQUENCE_NUMBER containing the index of the round and the start index value one.
The method updateResultSetWithComponentKey() updates the key columns of the result set with the component name of the round and its parent component. The first key column (ROUND_ID) is set to the component name of the round. When the parent component of the round is a competition, the second key column (COMPETITION_ID) is set with the component name of the competition and the third key column (SUPER_ROUND_ID) to NULL. Otherwise the parent component of the round is a super-round and the second key column is set to NULL and the third key column is set with the component name of the super-round.
public class JdbcRoundComponentMediator extends JdbcSimpleComponentMediator { public JdbcRoundComponentMediator () { super (new String[] {DatabaseConstants.Columns.Rounds.ROUND_ID, DatabaseConstants.Columns.Rounds.COMPETITION_ID, DatabaseConstants.Columns.Rounds.SUPER_ROUND_ID}, DatabaseConstants.Columns.Rounds.ROUND_TYPE, getRoundTypeConverter (), DatabaseConstants.Columns.Rounds.SEQUENCE_NUMBER, 1); } static private ComponentTypeConverter getRoundTypeConverter () { ComponentTypeConverter.TypeMapping[] typeMappings; typeMappings = new ComponentTypeConverter.TypeMapping[] { new ComponentTypeConverter.TypeMapping(Constants.ComponentTypes.GROUP_ROUND, DatabaseConstants.Values.RoundType.GROUP_ROUND), new ComponentTypeConverter.TypeMapping(Constants.ComponentTypes.PLAYOFF_ROUND, DatabaseConstants.Values.RoundType.PLAYOFF_ROUND), new ComponentTypeConverter.TypeMapping(Constants.ComponentTypes.SUPER_ROUND, DatabaseConstants.Values.RoundType.SUPER_ROUND) }; return new ComponentTypeConverter(typeMappings); } public void updateResultSetWithComponentKey (List<Integer> fieldIndices, ResultSet resultSet, ComponentInterface> component) throws JdbcComponentWriteError { String componentName = ""; ComponentInterface<?> currentComponent = component; Iterator<Integer> iterator = fieldIndices.iterator (); try { if (iterator.hasNext ()) { componentName = currentComponent.getName (); JdbcUtilityFunctions.updateResultSetWithString (componentName, resultSet, iterator.next ().intValue ()); } currentComponent = currentComponent.getParent(); componentName = currentComponent.getName (); if (currentComponent.getType ().equals (Constants.ComponentTypes.COMPETITION)) { if (iterator.hasNext ()) { JdbcUtilityFunctions.updateResultSetWithString (componentName, resultSet, iterator.next ().intValue ()); } if (iterator.hasNext ()) { JdbcUtilityFunctions.updateResultSetWithString (null, resultSet, iterator.next ().intValue ()); } } else { if (iterator.hasNext ()) { JdbcUtilityFunctions.updateResultSetWithString (null, resultSet, iterator.next ().intValue ()); } if (iterator.hasNext ()) { JdbcUtilityFunctions.updateResultSetWithString (componentName, resultSet, iterator.next ().intValue ()); } } } catch (JdbcFieldWriteError exc) { throw new JdbcComponentWriteError (component.getType (), component.getName (), componentName, exc.getColumnName (), exc.getException()); } } }
The mapping of the most simple attributes is straight forward. Set attributes
implementing the interface SetAttributeInterface
contain a collection of
enumeration values and may be mapped in different ways to the database. The most
simple way is to use a JdbcSimpleAttributeMediator
,
which maps the enumeration values as comma separated value string in the
database. The enumeration values may be converted to a special database value
using the value converter class SetAttributeValueConverter
. A sample for this method may be
found in the competition management application, where the
countries of the business class Competition are mapped in this
way:
List extends JdbcAttributeMediatorInterface> getMediatorsCompetition () { SetAttributeValueConverter<CountryEnumeration> countriesAttributeConverter; if (mediatorsCompetition == null) { countriesAttributeConverter = new SetAttributeValueConverter<CountryEnumeration> (CountryEnumeration.getIso2Converter ()); mediatorsCompetition = Arrays.asList (new JdbcPrecisionDateAttributeDateMediator (Constants.Attributes.Competition.BEGIN, DatabaseConstants.Columns.Competitions.BEGIN, DatabaseConstants.Columns.Competitions.BEGIN_PRECISION), new JdbcSimpleAttributeMediator (Constants.Attributes.Competition.COMPETITION_CODE, DatabaseConstants.Columns.Competitions.COMPETITION_CODE, true), new JdbcSimpleAttributeMediator (Constants.Attributes.Competition.COUNTRIES, countriesAttributeConverter, DatabaseConstants.Columns.Competitions.COUNTRIES), new JdbcPrecisionDateAttributeDateMediator (Constants.Attributes.Competition.END, DatabaseConstants.Columns.Competitions.END, DatabaseConstants.Columns.Competitions.END_PRECISION), new JdbcSimpleAttributeMediator (Constants.Attributes.Competition.NOTE, DatabaseConstants.Columns.Competitions.NOTE, true), new JdbcSimpleAttributeMediator (Constants.Attributes.Competition.OFFICIAL_NAME, DatabaseConstants.Columns.Competitions.OFFICIAL_NAME, true)); } return mediatorsCompetition; }
Another way to map set attributes to the database is using the JdbcSetAttributeValueColumnsMediator
. This mediator
requires, that each enumeration value of the value set is mapped to one boolean
column in the database. This approach may be used, when the enumeration contains
only a view values.
When the set attribute may contain a lot of enumeration values, a separate
database table may be required to store the enumeration values for the set. In
this case the mediator class JdbcResultSetComponentSetAttributeMediator
can be used.
The database table must contain the foreign key columns of the business object
the set attribute belongs to and one column for the enumeration value. The
mediator is initialized with database connection used to create the SQL select
statement for this table, a component mediator to set the parameters in the
WHERE clause of the select statement and the key columns in the result
set, the attribute keyword of the set attribute, the column name for the
enumeration value, an enumeration value converter to convert the enumeration
value to the database value and the select statement to create the result set.
In the competition management application the nationalities
of the business class Player are mapped in this way to the database table
PLAYER_NATIONALITIES. The set attribute mediator may be registered as
processor, for the business class the set attribute belongs to, at the IOFacade
. The following code block shows the methods, which
create the SQL statement and the mediators:
private String getColumnsPlayerNationalities () { return dl + DatabaseConstants.Columns.PlayerNationalities.COUNTRY_CODE + dl + "," + dl + DatabaseConstants.Columns.PlayerNationalities.PLAYER_ID + dl; } String selectPlayerNationalities () { String columns = getColumnsPlayerNationalities (); return "SELECT " + columns + " FROM " + dl + DatabaseConstants.Tables.PLAYER_NATIONALITIES + dl + " WHERE " + dl + DatabaseConstants.Columns.PlayerNationalities.PLAYER_ID + dl + "=?"; } JdbcComponentMediatorInterface getMediatorPlayer () { if (mediatorPlayer == null) { mediatorPlayer = new JdbcSimpleComponentMediator (new String[] { DatabaseConstants.Columns.Players.PLAYER_ID, DatabaseConstants.Columns.Players.TEAM_ID }); } return mediatorPlayer; } JdbcResultSetComponentSetAttributeMediator<CountryEnumeration> getMediatorPlayerNationalities () { if (mediatorPlayerNationalities == null) { mediatorPlayerNationalities = new JdbcResultSetComponentSetAttributeMediator<CountryEnumeration> (databaseConnectionAccess, componentMediators.getMediatorPlayer (), Constants.Attributes.Player.NATIONALITIES, DatabaseConstants.Columns.PlayerNationalities.COUNTRY_CODE, CountryEnumeration.getIso2Converter (), selectStatements.selectPlayerNationalities ()); } return mediatorPlayerNationalities; }
Reference attributes implement a 0..1 relationship between two business objects,
so a foreign key may be defined between the database tables containing these
objects. When this is the case, the mediator class JdbcReferenceAttributeMediator
may be used to fill the
foreign key columns with the component name of the referenced component
and its parents. See the class description for details how this works. This
mediator can only be used, when the reference path contains no index expression
and the number of container keyword/component name pairs is fixed. In the
competition management application the reference attributes teamA and
teamB of the business class Game are mapped in this way. These
attributes reference a team-classification representing the teams playing
against each other by their component name. So the attribute mediator
list for a Game is implemented as shown in the following code block:
List<? extends JdbcAttributeMediatorInterface> getMediatorsGame () { if (mediatorsGame == null) { mediatorsGame = Arrays.asList (new JdbcSimpleAttributeMediator (Constants.Attributes.Game.LABEL, DatabaseConstants.Columns.Games.GAME_LABEL, true), new JdbcIntegerAttributeMediator (Constants.Attributes.Game.SCORE_A, DatabaseConstants.Columns.Games.SCORE_A), new JdbcIntegerAttributeMediator (Constants.Attributes.Game.SCORE_B, DatabaseConstants.Columns.Games.SCORE_B), new JdbcReferenceAttributeMediator (Constants.Attributes.Game.TEAM_A, Constants.Attributes.Group.CLASSIFICATION, DatabaseConstants.Columns.Games.TEAM_A), new JdbcReferenceAttributeMediator (Constants.Attributes.Game.TEAM_B, Constants.Attributes.Group.CLASSIFICATION, DatabaseConstants.Columns.Games.TEAM_B)); } return mediatorsGame; }
Another way to map a reference attribute to the database is, to just store the
reference path as string in the database table. This approach is used in the
competition management application, where the reference attribute
team of the business class TeamClassification is stored as string
in the database. The reference path of this attribute contains index expressions
and may reference a Team or another TeamClassification and the
length of the reference path may vary, depending on how many round levels are
involved. The delimiter characters may be replaced in the reference path stored
in the database using the value converter ReferenceAttributeValueConverter
when desired. The following
code block shows the initialization of the simple attribute mediator for this
case:
List<? extends JdbcAttributeMediatorInterface> getMediatorsTeamClassification () { if (mediatorsTeamClassification == null) { mediatorsTeamClassification = Arrays.asList (new JdbcSimpleAttributeMediator (Constants.Attributes.TeamClassification.LABEL, DatabaseConstants.Columns.TeamClassifications.CLASSIFICATION_LABEL, true), new JdbcIntegerAttributeMediator (Constants.Attributes.TeamClassification.GAMES_PLAYED, DatabaseConstants.Columns.TeamClassifications.GAMES_PLAYED), new JdbcIntegerAttributeMediator (Constants.Attributes.TeamClassification.POINTS, DatabaseConstants.Columns.TeamClassifications.POINTS), new JdbcSimpleAttributeMediator (Constants.Attributes.TeamClassification.SCORE_DIFFERENCE, DatabaseConstants.Columns.TeamClassifications.SCORE_DIFFERENCE), new JdbcSimpleAttributeMediator (Constants.Attributes.TeamClassification.TEAM, new ReferenceAttributeValueConverter ('.', '\0', '[',']'), DatabaseConstants.Columns.TeamClassifications.TEAM_PATH)); } return mediatorsTeamClassification; }
The following code block shows a simple JUnit test method, which stores and retrieves an address book in/from the database. The sample is taken from the address book application. First an address book is created and some contacts are created by the method initializeAddressBook(). Then a copy of the address book is created, which is used after the update from the database to compare the updated address book with the original one. After storing the address book all contacts are removed by the ParseAndClearVisitor and the address book is updated from the database. Now it should be equal to the copied address book.
public void testSimpleTestCase () throws AcafException { AddressBook addressBookCopy; AddressBook addressBook = new AddressBook ("Address Book"); ParseAndClearVisitor clearCommand = new ParseAndClearVisitor (); SimpleDatabaseFacade databaseFacade = SimpleDatabaseFacade.theInstance; // // Create some contacts // initializeAddressBook (addressBook); // // Create a copy and store the address book in the database // addressBookCopy = addressBook.createCopy (); databaseFacade.storeComponentFull (addressBook); // // Remove all contacts from the address book // clearCommand.setCommand ("Contacts .*"); addressBook.acceptVisitor (clearCommand); // // Update the address book from the database and compare it with the copy // databaseFacade.updateComponentFull (addressBook); Assert.assertTrue (addressBook.isComponentEqual (addressBookCopy)); }
The history of ACAF starts in the year 1989, when I joined a software
development department at Digital Equipment, where I developed network
management software for network operation systems. From that period date the
terms component, attribute, component name, component
type and container. The manageable items in these network boxes, like
lines, circuits, logical units, etc., was internally denoted as components and
their properties as attributes. The commands to manage the network components
had pretty much the same syntax as command line syntax implemented by the
parsing visitors of the package de.tmasoft.acaf.parser
. The software was
developed in those days with Bliss, a programming language between assembler and
C.
In 1992 we started a project, where we implemented a operation system emulation on VMS with C++. To manage the operation system I transferred the network management concepts and implemented them in C++ as a framework. Afterwards I used the concept and the framework in several other projects. Meanwhile design patterns became popular and I redesigned the framework using design patterns like composite, visitor, and the component factory was born.
In 1999 we started a Java project to develop a classic GUI application with a database in the backend. In these days Java was still supported by Microsoft and we used the Microsoft Visual J++ environment with ADO record sets to exchange data between the application and the database. For this project I ported the framework to Java and implemented an O/R mapping with ADO. For this purpose I implemented ADO attribute adapters, which has been initialized with the normal attribute as adaptee and the ADO record set retrieved from the database. When an ADO attribute was modified, it modified the adapted attribute and the field in the record set. The disadvantage of this approach was the tight coupling between ADO and the business classes. To attach the GUI to the business classes I designed a mediator framework, which exchanged the data between the attributes and the GUI elements. The mediators has been attached as oberver to the attributes and as delegate to the GUI elements. So they has been notified when the value of the GUI element or the attribute has been changed, and could adjust the value of the other side.
In 2003 I joined a project, where we developed a web-service backend application on SAP. After I presented the concepts of the framework to the SAP software architect, which supported the project development team, we decided to port parts of the framework to ABAP/Objects and used it as base for the application. Especially the visitor concept was very useful for this project, because we had to parse and create many different XML structures to serialize and deserialize the business objects. We also used a visitor to verify the business objects and to create a list of error messages for the client of the web-service.
In 2008 I used the Java framework to implement a web-service application with a database in the backend. For this application I implemented the O/R mapping with JDBC result sets using mediators instead of adapters. In 2015 I reviewed, refactored and cleared out the framework and improved the documentation to be able to publish it.