Custom Datatypes Proposal 1
From this mailing list thread...
On our last few design calls we've been working through the generic AttributeType mechanism that we're introducing in 1.9. (
We wanted to summarize the current state of things. Some of this is in trunk, but some is refactoring Darius is doing. We're especially interested in thoughts about how these classes should be linked together with parameterized types, since we're not convinced we've gotten that right:
/**
* Capable of converting T to/from a String that can be persisted in the varchar column of a database.Â
* E.g. a date would be stored as yyyy-mm-dd and an image might be stored as a uri pointing to aÂ
* PACS system. Also capable of validating T, e.g. so we can use a plain java.util.Date to representÂ
* "date-in-past" but limit its possible values.
* Defines a String "datatypeHandled", e.g. "date", "regex-validated-string")
*/
interface CustomDatatypeHandler<T> {
T fromPersistedString(String);
String toPersistedString(T);
void validate(T);
String render(String persistedValue, String view);
// can be configured with setHandlerConfiguration(String);
}
/**
* holds the definition of a custom datatype (which is handled by a handler of the above interface).Â
* For example VisitAttributeType and GlobalProperty (we're able to do typed GPs trivially now)
*/
interface CustomDatatyped {
String getDatatype(); // required
/**
* if specified, will be handled by this specific CustomDatatypeHandler, otherwise it will be handledÂ
* by the default handler for this thing's datatype
*/
String getPreferredHandlerClassname(); // optional
String getHandlerConfig(); // optional
}
/**
* for user-defined extensions to core domain objects, which would be handled by adding customÂ
* database columns in a less generic system. E.g. VisitAttributeType implements AttributeType<Visit>
* datatype/handler specified via CustomDatatyped superinterface
*/
interface AttributeType<OwningType extends Customizable> extends CustomDatatyped, OpenmrsMetadata {
Integer getMinOccurs();
Integer getMaxOccurs();
}
/**
* trivial implementations of AttributeType (via BaseAttributeType)
*/
class VisitAttributeType
class LocationAttributeType
class ProviderAttributeType
/**
* Implemented by domain classes that may be customized by the user via custom attributes.Â
* E.g. Visit implements Customizable<VisitAttribute>, Location implements Customizable<LocationAttribute>
* Has convenience methods for dealing with a collection of attributes, of different types:
*/
interface Customizable<AttrClass extends Attribute> {
Collection<AttrClass> getAttributes(); //includes voided
Collection<AttrClass> getActiveAttributes(); //non-voided
void addAttribute(AttrClass);
void setAttribute(AttrClass); // voids other attributes of the given type
}
/**
* holds a value managed by a CustomDatatypeHandler. E.g. VisitAttribute, GlobalProperty.Â
* Any implementation of this has a corresponding CustomDatatyped implementation. "persistedValue"
* is a String suitable for persisting in a db varchar column; "objectValue" is what you'd wantÂ
* to work with in the API.
*/
interface CustomValue {
String getPersistedValue();
void setPersistedValue();
Object getObjectValue(); // don't remember why this isn't <T>
void setObjectValue(Object);
}
/**
* value corresponding to an AttributeType (which defines its datatype, whether it'sÂ
* required, whether it can repeat, etc), e.g. VisitAttribute corresponds to VisitAttributeType
*/
interface Attribute<OwningType extends Customizable> implements CustomValue, OpenmrsData {
OwningType getOwner();
void setOwner(OwningType);
AttributeType<OwningType> getAttributeType();
}
/**
* trivial implementation of Attribute (via BaseAttribute)
*/
class VisitAttribute
class LocationAttribute
class ProviderAttribute
/**
* this is interesting in that it both defines a custom datatype and stores its value
*/
class GlobalProperty implements CustomDatatyped, CustomValue
Comments welcome.
Roger's notional x-ray object
Roger's proposal
These use cases all rely on the custom data service object and handler object described below. These three use cases show the way the three different uses of custom datatypes interface with the cdso and handler.
Roger's notes on the custom data service object
These are work notes, see below for the final answer
Interface with the custom datatype consumer (settings (formerly Global Properties from platform 1.8 downwards), object attribute, complex obs)
one object per consumer – in attribute context, this means one per property type; in the settings (formerly Global Properties from platform 1.8 downwards) context, this means one for each property that is being displayed
constructor() – default handler, handler config="", minOccurs=0, maxOccurs=1
constructor(String handler, String handlerConfig, int minOccurs, int maxOccurs)
the consumer loads the custom datatype with all its values and receives them back via save
standard interface
data is exchanged through a List<CustomDataRow>; deleted objects have newValue=null, new objects have oldValue=null
public CustomDataRow { public String key; // optionally set by/returned to consumer public String oldValue; // set by/returned to consumer public String newValue; // returned to consumer public String voidReason; // returned to consumer public Object data; // for internal use by custom data service object }
The list is copied to a local list on load and then to a temporary list before returning from save; changes to the list by the consumer have no effect and the data object is never returnd to the customer
load(List<CustomDataRow>) (empty indicates no values exist)
save() : List<CustomDataRow> throws DataError Exception (empty indicates no values exists and none were added; underlying data is saved)
customerIterator() : ListIterator<Integer>
alternate interface where maxOccurs=1 (throws UnsupportedOperation Exception if maxOccurs != 1)
load(String) (null indicates no value exists)
save() : String throws DataError Exception (null indicates not set or voided; underlying data is saved)
getVoidReason() : String (returns null if not present)
both interfaces
cancel() (any changes not previously saved are abandoned and the custom data service object is cleared)
isDirty() : Boolean (whether any changed have been made to the list)
validate() : List<String> (each element is validated and the multiplicity is validated; any errors found are returned as messages; if no errors are found, returns and empty list and dirty is cleared; calling validate before saving guarantees no error will be thrown)
cancel() (changes are discarded and loaded data is cleared)
the custom data service object delegates handler calls to the handler
The Custom Data Service object as a data service
differs from other datatype services
may delegate to other datatype services
may have load-on-demand for large objects
may delegate to external data sources
may need multiple strategies in get/set methods if allocation of data elements between external and internal services changes depending on the external service
serves the consumer as a data service for the custom datatype
uses integer interface to select an element (=0 after load)
size() : int
index() : int
index(int) : int throws DataError Exception (sets and returns index, throwing error if > size()-1
getTitle() : String (exposes the short text representation of the indexed)
all elements may be get/set, but UnsupportedOperation Exception may be thrown by non-public or read-only elements
implemented by calls to getX(null) or setX(null, parameters), see below
serves the handler as a data service for the custom datatype
exposes the list iterator to the handler
all elements may be get/set, but UnsupportedOperation Exception may be thrown by read-only elements
implemented by calls to getX(this) or setX(this, parameters)
serves external data sources as a data service for the custom datatype
all elements may be get/set, but UnsupportedOperation Exception may be thrown by read-only elements
implemented by calls to getX(this) or setX(this, parameters)
Interface with the handler
The data service object initializes the handler object with its config
The data service object maintains the iterator and calls validate() for each element as part of its own validate method
The handler delegates to the data service object calls to the consumer service or other object services to get necessary information
Example: get all possible answer concepts to a question concept extracted by the handler from handler config
Example: get all values of this element currently existing in the DB
Example: get the birthdate of the patient to whom this attribute belongs
Interface for external data sources
Any connection setup such as logging into a remote service should be done in the constructor; it should be ready to go when the data service object references it
Roger's final design
Attribute/Method | Description |
---|---|
CustomDataService Interface | |
* handlerName : String | Handler to use to render |
* handlerConfig : String | Handler config to use to render |
* minOccurs : Integer | Minimum number of values (>0) |
* maxOccurs : Integer | Maximum number of values (0=infinity) |
- handler : CustomDataHandler | Handler instance based on Handler |
- data : List<CustomDataRow> | Holds a set of values for a single datatype |
+ loadAll(data : List<CustomDataRow>) | Loads a set of CustomDataObjects |
+ saveAll() : List<CustomDataRow> | Saves changed data and returns newValues to persist or voidReasons, throws data error if invalid |
+ load(data : String) | Loads a single oldValue, throws UnsupportedOperation exception if maxOccurs != 1 |
+ add(data : String, key : String) | Adds an oldValue and key; clear must be called first if not new |
+ save() : String | Saves changed data and returns newValue to persist, throws UnsupportedOperation exception if maxOccurs != 1, throws data error if invalid |
+ getVoidReason() : String | Gets voidReason for singleValue, null if not void, throws UnsupportedOperation exception if maxOccurs != 1 |
+ clear() | Resets all data objects discarding any changes |
+ isDirty() : Boolean | Returns true if there are changed objects |
+ validate() : List<String> | Validates each data item and checks multiplicity, returns messages if errors are found |
+ render(style : String) : Object | Delegates to handler |
+ render(style : String, index : Integer) : Object | Delegates to handler |
+ getScript(): String | Delegates to handler |
+ getHTML(name : String) | Delegates to handler |
+ getProperties() : Properties | Delegates to handler |
+ setProperties(p : Properties) | Delegates to handler |
+ size() : Integer | Returns number of values |
+ getElement(name: String, index : Integer) : Object | Returns named data element of indexed value => getElement(null,name,index) |
+ setElement(name: String, index : Integer, o : Object) | Sets named data element of indexed value => setElement(null,name,index,o) |
+ getElement(name : String) : Object | Returns named data element of value0 => getElement(null,name,0) |
+ setElement(name : String, o : Object) | Sets named data element of value0 => setElement(null,name,0,o) |
+ getValue() : Datatype | Returns typed value, throws UnsupportedOperation exception if this is not a simple type |
+ setValue(d: Datatype) | Returns typed value, throws UnsupportedOperation exception if this is not a simple type |
+ getTitle() : String | Returns the short representation of value0 |
+ getTtile(index : Integer) | Returns the short representation of valueindex |
CustomDataHandlerSupport Interface | |
+ getElement(me : Object,name: String, index : Integer) : Object | Get named, indexed data element, exposed/unexposed depending on me |
+ setElement(me : Object, name: String, index : Integer, o : Object) | Set named, indexed data element, exposed/unexposed depending on me |
+ size() : Integer | Returns number of values |
CustomDataHandler Interface | |
* handlerConfig : String | Handler's copy of its config |
+ render(style : String) : Object | Renders multiple objects in style |
+ render(style : String, index : int) : Object | Renders single object in style |
+ getScript(): String | Reference to an include file to go in header portion of page |
+ getHTML(name : String) | HTML to insert in text for getting multiple objects, name property is prefixed with name |
+ getProperties() : Properties | Returns a properties object containing properties to be returned on submit |
+ setProperties(p : Properties) | Receives a properties object after submit to update value list |
CustomDataRow Class | |
+ key : String | Optionally set by/returned to consumer |
+ oldValue : String | Set by/returned to consumer |
+ newValue : String | Returned to consumer |
+ voidReason : String | Returned to consumer, null if not voided |
- data : Object | For internal use by custom data service object |