//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library.tools.mapping;
import openadk.library.*;
import openadk.library.tools.cfg.ADKConfigException;
import openadk.library.tools.xpath.SIFXPathContext;
import openadk.util.XMLUtils;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
/**
* A FieldMapping defines how to map a local application field to an element or
* attribute of the SIF Data Object type encapsulated by the parent ObjectMapping.
* Each FieldMapping is associated with a <i>Rule</i> that is evaluated at
* runtime to carry out the actual mapping operation on a SIFDataObject instance.
* The way the rule behaves is up to its implementation.<p>
*
* A FieldMapping may have a default value. If set, the default value is
* assigned to the SIF element or attribute if the corresponding field value is
* null or undefined. This is useful if you wish to ensure that a specific SIF
* element/attribute always has a value regardless of whether or not there is a
* corresponding value in your application's database.
* <p>
*
* A Field Mapping that has a default value can also set another attribute that
* specifies the behavior of the Field Mapping if the application value being
* mapped is null.
* <p>
*
* The application-defined field name that is associated with a FieldMapping
* must be unique; that is, there cannot be more than one FieldMapping for the
* same application field. However, if you wish to map the same field to more
* than one SIF element or attribute, you can create an <i>alias</i>. An alias
* is a FieldMapping that has a unique field name but refers to another field.
* For example, if your application defines the field STUDENT_NUM and you wish
* to define two FieldMappings for that field, create an alias:
* <p>
*
* <code>
* // Create the default mapping<br/>
* FieldMapping fm = new FieldMapping("STUDENT_NUM","OtherId[@Type='06']");<br/><br/>
* <br/>
* // Create an alias (the field name must be unique)<br/>
* FieldMapping zz = new FieldMapping("MYALIAS","OtherId[@Type='ZZ']");<br/>
* zz.setAlias( "STUDENT_NUM" );<br/><br/>
* </code>
*
* In the above example, the "STUDENT_NUM" mapping produces an <OtherId>
* element with its Type attribute set to '06'. The "MYALIAS" mapping produces
* a second <OtherId> element with its Type attribute set to 'ZZ'. Both
* elements will have the value of the application-defined STUDENT_NUM field.
* Note that if MYALIAS were an actual field name of your application, however,
* the value of the <OtherId Type='ZZ'> element would be equal to that
* field's value. When creating aliases be sure to choose a name that does not
* conflict with the real field names used by your application.
* <p>
*
* @author Eric Petersen
* @version ADK 1.0
*/
public class FieldMapping extends Mapping
{
private static final String ATTR_VALUESET = "valueset";
private static final String ATTR_ALIAS = "alias";
private static final String ATTR_DEFAULT = "default";
private static final String ATTR_NAME = "name";
private static final String ATTR_DATATYPE = "datatype";
private static final String ATTR_SIFVERSION = "sifVersion";
private static final String ATTR_DIRECTION = "direction";
private static final String ATTR_IFNULL = "ifnull";
/**
* The field mapping behavior for null fields is unspecified. In this
* case, the behavior is identical to IFNULL_DEFAULT
* @see #setNullBehavior(byte)
*/
public static final byte IFNULL_UNSPECIFIED = 0;
/**
* Specifies that if the field being mapped is NULL, this field mapping
* should use the default value, if set.
* @see #setNullBehavior(byte)
*/
public static final byte IFNULL_DEFAULT = 1;
/**
* Specifies that if the field being mapped is NULL, this field mapping
* should not generate a SIF Element, even if a default value is specified
* @see #setNullBehavior(byte)
*/
public static final byte IFNULL_SUPPRESS = 2;
protected byte fNullBehavior = IFNULL_UNSPECIFIED;
private Rule fRule;
protected String fField;
protected String fDefValue;
protected String fAlias;
protected String fValueSet;
protected MappingsFilter fFilter;
protected Node fNode;
private SIFDataType fDataType = SIFDataType.STRING;
/**
* Constructor
*/
public FieldMapping()
{
this( null, (String)null, (Node)null );
}
/**
* Constructs a FieldMapping with an XPath-like rule
* @param name The name of the local application field that maps to the
* SIF Data Object element or attribute described by this FieldMapping
* @param rule An XPath-like query string described by the <code>SIFDTD.lookupByXPath</code>
* method
*/
public FieldMapping( String name, String rule )
{
this( name, rule, null );
}
/**
* Creates a new FieldMapping rule
* @param name The name of the field being mapped
* @param rule The XPath rule associated with this mapping
* @param node The optional DOM Node associated with this FieldMapping
*/
public FieldMapping( String name, String rule, Node node )
{
setNode( node );
setFieldName( name );
if( rule != null ){
setRule( rule );
}
}
/**
* Constructs a FieldMapping with an <OtherId> rule.<p>
*
* @param name The name of the local application field that maps to the
* SIF Data Object element or attribute described by this FieldMapping
* @param rule An OtherIdMapping object that describes how to select
* a <OtherId> element during a mapping operation
*/
public FieldMapping( String name, OtherIdMapping rule )
{
this( name, rule, null );
}
/**
* Constructs a FieldMapping with an <OtherId> rule.<p>
*
* @param name The name of the local application field that maps to the
* SIF Data Object element or attribute described by this FieldMapping
* @param rule An OtherIdMapping object that describes how to select
* a <OtherId> element during a mapping operation
* @param node The optional DOM Node that this FieldMapping stores its configuration
* to, or Null
*/
public FieldMapping( String name, OtherIdMapping rule, Node node )
{
setNode( node );
setFieldName( name );
if( rule != null ){
setRule( rule );
}
}
/**
* Gets the optional DOM Node associated with this FieldMapping instance.
* The DOM Node is usually set by the parent ObjectMapping instance when a
* FieldMapping is populated from a DOM Document.
* @return The DOM Node associated with this this FieldMapping instance
*/
public Node getNode()
{
return fNode;
}
/**
* Sets the optional DOM Node associated with this FieldMapping instance.
* The DOM Node is usually set by the parent ObjectMapping instance when a
* FieldMapping is populated from a DOM Document.
* @param node The DOMNode associated with this FieldMapping instance
*/
public void setNode( Node node )
{
fNode = node;
}
/**
* Creates a new FieldMapping instance and populates its properties from
* the given XML Element
* @param parent
* @param element
* @return a new FieldMapping instance
* @throws ADKConfigException If the FieldMapping cannot read expected
* values from the DOM Node
*/
public static FieldMapping fromXML(
ObjectMapping parent,
Element element )
throws ADKConfigException
{
if( element == null ){
throw new IllegalArgumentException( "Argument: 'element' cannot be null" );
}
String name = element.getAttribute( ATTR_NAME );
FieldMapping fm = new FieldMapping();
fm.setNode( element );
fm.setFieldName( name );
fm.setDefaultValue( XMLUtils.getAttribute( element, ATTR_DEFAULT ) );
fm.setAlias( XMLUtils.getAttribute( element, ATTR_ALIAS ) );
fm.setValueSetID( XMLUtils.getAttribute( element, ATTR_VALUESET ) );
String ifNullBehavior = element.getAttribute( ATTR_IFNULL );
if( ifNullBehavior.length() > 0 ){
if( ifNullBehavior.equalsIgnoreCase( "default" ) ){
fm.setNullBehavior( IFNULL_DEFAULT );
}
else if (ifNullBehavior.equalsIgnoreCase( "suppress") ){
fm.setNullBehavior( IFNULL_SUPPRESS );
}
}
String dataType = element.getAttribute( ATTR_DATATYPE );
if( dataType != null && dataType.length() > 0 ){
try{
fm.setDataType( SIFDataType.valueOf( SIFDataType.class, dataType.toUpperCase() ) );
} catch ( IllegalArgumentException iae ){
ADK.getLog().warn( "Unable to parse datatype '" + dataType + "' for field " + name, iae );
}
}
String filtVer = XMLUtils.getAttribute( element, ATTR_SIFVERSION );
String filtDir = XMLUtils.getAttribute( element, ATTR_DIRECTION );
if( filtVer != null || filtDir != null )
{
MappingsFilter filt = new MappingsFilter();
if( filtVer != null )
filt.setSIFVersion( filtVer );
if( filtDir != null )
{
if( filtDir.equalsIgnoreCase("inbound") )
filt.setDirection( MappingsDirection.INBOUND );
else
if( filtDir.equalsIgnoreCase("outbound") )
filt.setDirection( MappingsDirection.OUTBOUND );
else
throw new ADKConfigException(
"Field mapping rule for " + parent.getObjectType() + "." + fm.getFieldName() +
" specifies an unknown Direction flag: '" + filtDir + "'" );
}
fm.setFilter( filt );
}
// FieldMapping must either have node text or an <otherid> child
Node otherIdNode = XMLUtils.getFirstElementIgnoreCase( element, "otherid" );
if( otherIdNode == null )
{
String def = XMLUtils.getText( element );
if( def != null )
fm.setRule( def );
else
fm.setRule( "" );
}
else
{
Element otherId = (Element)otherIdNode;
fm.setRule( OtherIdMapping.fromXML( parent, fm, otherId ), otherId );
}
return fm;
}
/**
* Writes the values of this FieldMapping to the specified XML Element
*
* @param element The XML Element to write values to
*/
public void toXML( Element element ){
XMLUtils.setOrRemoveAttribute( element, ATTR_NAME, fField );
if( fDataType == SIFDataType.STRING )
{
XMLUtils.removeAttribute( element, ATTR_DATATYPE );
} else {
XMLUtils.setAttribute( element, ATTR_DATATYPE, fDataType.name() );
}
XMLUtils.setOrRemoveAttribute( element, ATTR_DEFAULT, fDefValue );
XMLUtils.setOrRemoveAttribute( element, ATTR_ALIAS, fAlias );
XMLUtils.setOrRemoveAttribute( element, ATTR_VALUESET, fValueSet );
MappingsFilter filt = getFilter();
if( filt != null )
{
writeFilterToXml( filt, element );
}
writeNullBehaviorToXml( fNullBehavior, element );
getRule().toXML( element );
}
/**
* Writes the mapping filter to an XML Element
* @param filter
* @param element The XML Element to write the filter to
*/
private void writeFilterToXml( MappingsFilter filter, Element element )
{
if( filter == null )
{
element.removeAttribute( ATTR_SIFVERSION );
element.removeAttribute( ATTR_DIRECTION );
}
else
{
if( filter.hasVersionFilter() ){
element.setAttribute( ATTR_SIFVERSION, filter.getSIFVersion() );
} else {
element.removeAttribute( ATTR_SIFVERSION );
}
MappingsDirection direction = filter.getDirection();
if( direction == MappingsDirection.INBOUND )
{
element.setAttribute( ATTR_DIRECTION, "inbound" );
}
else if( direction == MappingsDirection.OUTBOUND )
{
element.setAttribute( ATTR_DIRECTION, "outbound" );
}
else
{
element.removeAttribute( ATTR_DIRECTION );
}
}
}
private void writeNullBehaviorToXml( byte behavior, Element element )
{
switch( behavior )
{
case IFNULL_DEFAULT:
element.setAttribute( ATTR_IFNULL, "default" );
break;
case IFNULL_SUPPRESS:
element.setAttribute( ATTR_IFNULL, "suppress" );
break;
default:
element.removeAttribute( ATTR_IFNULL );
break;
}
}
/**
* Creates a copy this ObjectMapping instance.<p>
* @param newParent The parent that this FieldMapping is associated with
* @return A "deep copy" of this object
* @throws ADKMappingException
*/
public FieldMapping copy( ObjectMapping newParent )
throws ADKMappingException
{
FieldMapping m = new FieldMapping();
if( fNode != null && newParent.fNode != null ) {
m.fNode = newParent.fNode.getOwnerDocument().importNode( fNode, false );
}
m.setFieldName( fField );
m.setDefaultValue( fDefValue );
m.setAlias( fAlias );
m.setValueSetID( fValueSet );
m.setNullBehavior( fNullBehavior );
if( fFilter != null ) {
MappingsFilter filtCopy = new MappingsFilter();
filtCopy.fVersion = fFilter.fVersion;
filtCopy.fDirection = fFilter.fDirection;
m.setFilter( filtCopy );
}
m.setDataType( fDataType );
if( getRule() != null )
m.setRule(getRule().copy( m ));
return m;
}
/**
* Sets the name of the local application field that maps to the SIF Data
* Object element or attribute
*
* @param name A field name. (This value will be used as the key in the
* HashMap populated by the Mappings.map methods.)
*/
public void setFieldName( String name )
{
fField = name;
if( fNode != null && name != null )
XMLUtils.setAttribute( fNode, "name", name );
}
/**
* Gets the name of the local application field that maps to the SIF Data
* Object element or attribute
*
* @return The local application field name. (This value will be used as
* the key in HashMaps populated by the Mappings.map methods)
*/
public String getFieldName()
{
return fField;
}
/**
*
* Returns the key to a Field Mapping. The Key of a field mapping consists
* of it's alias or field name and any filters that are defined
* @return A string representing the key of this object
*/
public String getKey()
{
StringBuilder key = new StringBuilder();
key.append( fField );
if( fAlias != null ){
key.append( '_' );
key.append( fAlias );
}
if( fFilter != null ){
if( fFilter.hasDirectionFilter() ){
key.append( '_' );
key.append( fFilter.getDirection() );
}
if( fFilter.hasVersionFilter() ){
key.append( '_' );
key.append( fFilter.getSIFVersion() );
}
}
return key.toString();
}
/**
* Sets the ID of the ValueSet that should be used to translate the value
* of this field.<p>
*
* Note: The Mappings classes do not automatically perform translations if
* this attribute is defined. Rather, it is provided so that agents can
* associate a ValueSet with a field in the Mappings configuration file,
* and have a means of looking up that association at runtime.<p>
*
* @param id The ID of a ValueSet defined in the Mappings (e.g. "EthnicityCodes"). If
* set to NULL or "", the ValueSet is removed
*
* @see #getValueSetID
*
* @since ADK 1.5
*/
public void setValueSetID( String id )
{
fValueSet = id;
if( fValueSet != null && fValueSet.trim().length() == 0 ){
fValueSet = null;
}
if( fNode != null ) {
XMLUtils.setOrRemoveAttribute( fNode, ATTR_VALUESET, fValueSet );
}
}
/**
* Gets the ID of the ValueSet that should be used to translate the value
* of this field.<p>
*
* Note: The Mappings classes do not automatically perform translations if
* this attribute is defined. Rather, it is provided so that agents can
* associate a ValueSet with a field in the Mappings configuration file,
* and have a means of looking up that association at runtime.<p>
*
* @return The value passed to the <code>setValueSetID</code> method
*
* @since ADK 1.5
*
* @see #setValueSetID
*/
public String getValueSetID()
{
return fValueSet;
}
/**
* Sets a default value for this field when no corresponding element or
* attribute is found in the SIF Data Object. The Mapping.map methods will
* create an entry in the HashMap with this default value.
*
* @param defValue A default string value for this field
*/
public void setDefaultValue( String defValue )
{
fDefValue = defValue;
if( fNode != null ){
XMLUtils.setOrRemoveAttribute( fNode, ATTR_DEFAULT, defValue );
}
}
/**
* Gets the default value for this field when no corresponding element or
* attribute is found in the SIF Data Object. The Mapping.map methods will
* create an entry in the HashMap with this default value.
*
* @return The default string value for this field
*/
public String getDefaultValue()
{
return fDefValue;
}
/**
* Quickly determines whether this field mapping has a default value defined
* without going through the extra work of actually resolving the default value
* @return True if this field mapping has a default value defined
*/
public boolean hasDefaultValue()
{
return fDefValue != null;
}
/**
* @param converter
* @param formatter
* @return The strongly-typed datatype
* @throws ADKMappingException If the default value specified cannot be converted to the
* proper data type
*/
public SIFSimpleType getDefaultValue( SIFTypeConverter converter, SIFFormatter formatter )
throws ADKMappingException
{
if( fDefValue != null && converter != null ){
try{
return converter.parse( formatter, fDefValue );
} catch( ADKParsingException adkpe ){
throw new ADKMappingException( adkpe.getMessage(), null, adkpe );
}
}
return null;
}
/**
* Defines this FieldMapping to be an alias of another field mapping. During
* the mapping process, the FieldMapping will be applied if the referenced
* field exists in the Map provided to the Mappings.map method. Aliases are
* required when an application wishes to map a single application field to
* more than one element or attribute in the SIF Data Object.<p>
*
* To use aliases, create a FieldMapping where the field name is a unique
* name and the alias is the name of an existing field. For example, to map
* an application-defined field named "STUDENT_NUM" to more than one
* element/attribute in the SIF Data Object,
*
* <code>
* // Create the default mapping<br/>
* FieldMapping fm = new FieldMapping("STUDENT_NUM","OtherId[@Type='06']");<br/><br/>
* <br/>
* // Create an alias; the field name must be unique<br/>
* FieldMapping fm2 = new FieldMapping("STUDENT_NUM_B","OtherId[@Type='ZZ']=STUDENTID:$(STUDENTNUM)");<br/>
* </code>
*
* @param fieldName The name of the field for which this entry is an alias
*/
public void setAlias( String fieldName )
{
fAlias = fieldName;
if( fNode != null ){
XMLUtils.setOrRemoveAttribute( fNode, ATTR_ALIAS, fieldName );
}
}
/**
* Determines if this FieldMapping is an alias of another field mapping.
*
* @return The name of the field for which this entry is an alias, or null
* if this FieldMapping is not an alias.
*
* @see #setAlias
*/
public String getAlias()
{
return fAlias;
}
/**
* Evaluates this rule against a SIFXpathContexts and returns the text value
* of the element or attribute that satisfied the query.<p>
*
* @param xpathContext The SIFXpathContext the rule is evaluated against
* @param version The SIFVersion to use when lookup up the value
* @param returnDefault True if the default value should be returned when there is no value
* @return The value of the element or attribute that satisfied the
* query, or null if no match found. If the <code>returnDefault<code> parameter
* is set to true, the default value will be returned, if specified
* @exception ADKSchemaException is thrown if the rule associated with this
* object is invalid or the default value cannot be parsed
*/
public SIFSimpleType evaluate( SIFXPathContext xpathContext, SIFVersion version, boolean returnDefault )
throws ADKSchemaException
{
SIFSimpleType value = null;
if( getRule() != null ){
value = getRule().evaluate( xpathContext, version );
}
if( value == null && fDefValue != null && returnDefault ){
// TODO: Support all data types
try
{
return fDataType.getConverter().parse( ADK.getTextFormatter(), fDefValue );
} catch( ADKParsingException adkpe ){
throw new ADKSchemaException( "Error parsing default value: '" + fDefValue + "' for field " + fField + " : " + adkpe, null, adkpe );
}
}
return value;
}
/**
* Sets this FieldMapping rule to an XPath-like query string
* @param definition An XPath-like query string described by the
* <code>SIFDTD.lookupByXPath</code> method
*/
public void setRule( String definition )
{
fRule = new XPathRule( definition );
if( fNode != null )
XMLUtils.setText( fNode, definition );
}
/**
* Sets this object's rule to an "<OtherId> rule"
* @param otherId An OtherIdMapping object that describes how to select
* a <OtherId> element during a mapping operation
*/
public void setRule( OtherIdMapping otherId )
{
fRule = new OtherIdRule( otherId );
if( fNode != null ) {
fRule.toXML( fNode );
}
}
/**
* Sets an OtherId rule for this FieldMapping
* @param otherId The OtherIdMapping instance to use for this field mapping
* @param node The DOM Node associated with the OtherId mapping
*/
public void setRule( OtherIdMapping otherId, Node node )
{
fRule = new OtherIdRule( otherId, node );
}
/**
* Gets the field mapping rule
* @return A Rule instance
*/
public Rule getRule()
{
return fRule;
}
/**
* Sets optional filtering attributes. This field mapping rule will
* only be applied if the filters match the values passed to the
* <code>Mappings.map</code> method at runtime.<p>
*
* @param filter A MappingsFilter instance (null to clear the current
* filter attributes)
* @see #getFilter
* @since ADK 1.5
*/
public void setFilter( MappingsFilter filter )
{
fFilter = filter;
if( fNode != null ){
writeFilterToXml( filter, (Element)fNode );
}
}
/**
* Gets optional filtering attributes.<p>
* @return A MappingsFilter instance or null if none defined for
* this field rule
* @see #setFilter
* @since ADK 1.5
*/
public MappingsFilter getFilter()
{
return fFilter;
}
/**
* Sets the behavior that the field mapping should follow when mapping
* a <code>null</code> value. The value set should be one of the the
* <code>IFNULL_XXX</code> constants defined in this class. The default
* behavior for null values if this value is not set is to use the
* default value, if present.
* @param behavior One of the the <code>IFNULL_XXX</code> constants defined in this class
*/
public void setNullBehavior( byte behavior )
{
if( behavior < IFNULL_UNSPECIFIED || behavior > IFNULL_SUPPRESS ){
throw new IllegalArgumentException( "Value must be one of the FieldMapping.IFNULL_XXX constants." );
}
fNullBehavior = behavior;
if( fNode != null ){
writeNullBehaviorToXml( behavior, (Element)fNode );
}
}
/**
* Returns the behavior that this field mapping will follow when mapping
* a <code>null</code> value.
* @return One of the the <code>IFNULL_XXX</code> constants defined in this class
*/
public byte getNullBehavior()
{
return fNullBehavior;
}
/**
* Sets the name of the data type this field represents.<p>
*
* This datatype is used if the datatype cannot be derived from the field mapping.
* If this value is null, this instance will use the <code>STRING</code> data type.
* @param dataType
*/
public void setDataType( SIFDataType dataType) {
this.fDataType = dataType;
if( fNode != null ) {
if( fDataType == SIFDataType.STRING )
{
XMLUtils.removeAttribute( fNode, ATTR_DATATYPE );
} else {
XMLUtils.setAttribute( fNode, ATTR_DATATYPE, fDataType.name() );
}
}
}
/**
* Returns the datatype that this FieldMapping represents.<p>
*
* If the <code>datatype<code> attribute is not set on the
* <code><field></code> mapping, a default of {@link SIFDataType#STRING}
* is assumed. This value is primarily used for assigning default values
* to field mappings.
* @return The Datatype associated with this FieldMapping
*/
public SIFDataType getDataType() {
return fDataType;
}
public void setRule(Rule fRule) {
this.fRule = fRule;
}
@Override
public String toString()
{
return "Field: " + this.getKey() + ":" + this.getRule().toString();
}
}