//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library.tools.mapping;
import java.io.Serializable;
import java.util.*;
import openadk.util.ADKStringUtils;
import openadk.util.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* A ValueSet is an arbitrary mapping table used to map an application's
* proprietary codes and constants to SIF codes and constants. For example, an
* agent might define a ValueSet to map grade levels, ethnicity codes, english
* proficiency codes, and so on.
* <p>
*
* ValueSet stores its data in a HashMap. For each entry in the map, the key is
* a value defined by the application and the value is a ValueSetEntry object
* that encapsulates the associated SIF value and other fields like a display
* title for user interfaces.<p>
*
* For example, a ValueSet that maps grade levels might be comprised of the
* following entries:
* <p>
*
* <table>
* <tr><td><b>Key</b></td><td><b>Value</b></td></tr>
* <tr><td><b>PREK</b></td><td><b>PK</b></td></tr>
* <tr><td><b>K</b></td><td><b>0K</b></td></tr>
* <tr><td><b>1</b></td><td><b>01</b></td></tr>
* <tr><td><b>2</b></td><td><b>02</b></td></tr>
* <tr><td><b>3</b></td><td><b>03</b></td></tr>
* <tr><td><b>4</b></td><td><b>04</b></td></tr>
* <tr><td><b>5</b></td><td><b>05</b></td></tr>
* <tr><td><b>6</b></td><td><b>06</b></td></tr>
* <tr><td><b>7</b></td><td><b>07</b></td></tr>
* <tr><td><b>8</b></td><td><b>08</b></td></tr>
* <tr><td><b>9</b></td><td><b>09</b></td></tr>
* </table>
* <p>
*
* To translate an application-defined value to its SIF equivalent, call the
* <code>translate</code> method. To translate a SIF-defined value to its
* application-defined equivalent, call the <code>translateReverse</code>
* method.
* <p>
*
* @author Edustructures LLC
* @version ADK 1.0
*/
public class ValueSet implements Comparator, Serializable
{
/**
*
*/
private static final long serialVersionUID = -498576493089957332L;
/**
* The values of this ValueSet
*/
protected HashMap fTable = new HashMap();
/**
* Reverse lookup table
*/
protected HashMap fReverseTable = new HashMap();
/**
* The unique ID of this ValueSet
*/
protected String fId;
/**
* Optional display title for this ValueSet
*/
protected String fTitle;
/**
* Optional DOM Node that defines this ValueSet
*/
protected transient Node fNode;
/**
* The default ValueSetEntry to use if no match is found
*/
protected ValueSetEntry fDefaultAppEntry;
/**
* True to render the app default if the value being translated is null
*/
protected boolean fRenderAppDefaultIfNull;
/**
* The default ValueSetEntry to use if no match is found
*/
protected ValueSetEntry fDefaultSifEntry;
/**
* True to render SIF default if the value being translated is null
*/
protected boolean fRenderSifDefaultIfNull;
/**
* Constructs a ValueSet with an ID
*/
public ValueSet( String id )
{
this( id, null );
}
/**
* Constructs a ValueSet with an ID and display title
*/
public ValueSet( String id, String title )
{
this( id, title, null );
}
/**
* Constructs a ValueSet with an ID, display title, and associated DOM Node
*/
public ValueSet( String id, String title, Node node )
{
fId = id;
fTitle = title;
fNode = node;
// TODO: Consider changing the constructor to require an Element instance
if( node instanceof Element ){
toXml( (Element)node );
}
}
public ValueSet copy( Mappings newParent )
{
Document newParentDOM = null;
if( newParent.fNode != null ) {
newParentDOM = newParent.fNode.getOwnerDocument();
}
ValueSet copy = new ValueSet( fId, fTitle );
if( fNode != null && newParentDOM != null ) {
copy.fNode = newParentDOM.importNode( fNode, false );
newParent.fNode.appendChild( copy.fNode );
}
// Copy the ValueSetEntry's
ValueSetEntry[] entries = getEntries();
for( int i = 0; i < entries.length; i++ )
{
copy.define( entries[i].name, entries[i].value, entries[i].title );
}
try
{
if( fDefaultAppEntry != null ){
copy.setAppDefault( fDefaultAppEntry.name, fRenderAppDefaultIfNull );
}
if( fDefaultSifEntry != null ){
copy.setSifDefault( fDefaultSifEntry.value, fRenderSifDefaultIfNull );
}
}
catch( ADKMappingException adkme ){
// We're kind of stuck here.
throw new RuntimeException( adkme.toString() + ADKStringUtils.getStackTrace( adkme ), adkme );
}
return copy;
}
public String getId()
{
return fId;
}
public void setId( String id )
{
fId = id;
}
public String getTitle()
{
return fTitle == null ? fId : fTitle;
}
public void setTitle( String title )
{
fTitle = title;
}
public Node getNode()
{
return fNode;
}
public void setNode( Node node )
{
fNode = node;
}
public String toString()
{
return getTitle();
}
/**
* Return a sorted array of the ValueSet's entries
* @return An array of ValueSetEntry objects sorted by display order
*/
public ValueSetEntry[] getEntries()
{
int i = 0;
ValueSetEntry[] entries = new ValueSetEntry[ fTable.size() ];
for( Iterator it = fTable.values().iterator(); it.hasNext(); )
entries[i++] = (ValueSetEntry)it.next();
Arrays.sort( entries, this );
return entries;
}
/**
* Compares two ValueSetEntry objects for order
*/
public int compare( Object o1, Object o2 )
{
int i1 = ((ValueSetEntry)o1).displayOrder;
int i2 = ((ValueSetEntry)o2).displayOrder;
if( i1 < i2 )
return -1;
if( i1 == i2 )
return 0;
return 1;
}
/**
* Sets a value
*/
public void define( String appValue, String sifValue, String title )
{
Element element = null;
if( fNode != null ){
element = fNode.getOwnerDocument().createElement( "value" );
fNode.appendChild( element );
}
define( appValue, sifValue, title, element );
}
/**
* Sets a value
*/
public void define( String appValue, String sifValue, String title, Node node )
{
if( appValue != null ) {
ValueSetEntry entry = new ValueSetEntry( appValue, sifValue, title );
entry.displayOrder = fTable.size();
entry.node = node;
if( node != null ){
entry.toXml( (Element)node );
}
fTable.put( appValue, entry );
fReverseTable.put( sifValue, entry );
}
}
/**
* Gets a value
*/
public String lookup( String appValue )
{
ValueSetEntry e = appValue == null ? null : (ValueSetEntry)fTable.get(appValue);
return e == null ? null : e.value;
}
/**
* Translates a value.<p>
*
* This method differs from <code>lookup</code> in that it returns a default value
* if it is available and no mapping is defined in the ValueSet. If there is no
* default value, the <code>appValue</code> passed in is returned.
*
* The <code>lookup</code> method
* returns null if no mapping is defined.
* <p>
*
* @param appValue An application-defined value
* @return The corresponding SIF-defined value
*/
public String translate( String appValue )
{
ValueSetEntry e = getEntry( appValue );
if( e != null ){
return e.value;
} else {
return evaluateDefault(
fDefaultSifEntry == null ? null : fDefaultSifEntry.value,
fRenderSifDefaultIfNull, appValue );
}
}
/**
* Encapsulates the logic for returning default values for a valueset
* @param defaultValue
* @param renderIfNull
* @param srcValue
* @return
*/
private String evaluateDefault(
String defaultValue,
boolean renderIfNull,
String srcValue )
{
if( srcValue == null && !renderIfNull ){
return null;
}
if( defaultValue != null ){
return defaultValue;
}
return srcValue;
}
/**
* Translates a value.<p>
*
* If there is no mapping defined, the default value passed in is returned.
* If, however, the default value passed in is NULL, the valueset will be searched
* for a default value and that value will be returned. If there is no default,
* the <code>appValue</code> passed in is returned.
*
* The <code>lookup</code> method
* returns null if no mapping is defined.
* <p>
*
* @param appValue An application-defined value
* @return The corresponding SIF-defined value
*/
public String translate( String appValue, String defaultValue )
{
ValueSetEntry e = getEntry( appValue );
if( e != null ){
return e.value;
}
if( defaultValue != null ){
return defaultValue;
}
return evaluateDefault(
fDefaultSifEntry == null ? null : fDefaultSifEntry.value,
fRenderSifDefaultIfNull, appValue );
}
/**
* Performs a reverse translation.<p>
*
* If there is no match found, but there is a default value found that
* applies to inbound mappings, the default value will be returned.
*
* If no default value is found, the sifValue will be returned
*
* @param sifValue An SIF-defined value
* @return The corresponding application-defined value
*/
public String translateReverse( String sifValue )
{
ValueSetEntry e = getReverseEntry( sifValue );
if( e!= null ){
return e.name;
} else {
return evaluateDefault(
fDefaultAppEntry == null ? null : fDefaultAppEntry.name,
fRenderAppDefaultIfNull, sifValue );
}
}
/**
* Performs a reverse translation.<p>
*
* If there is no match found, the <code>defaultValue</code> is returned.
* If, however, the defaultValue is NULL, the Valueset's outbound default value
* will be used. If the valueset does not have a default outbound value, the
* <code>sifValue</code> will be returned
*
* @param sifValue An SIF-defined value
* @return The corresponding application-defined value
*/
public String translateReverse( String sifValue, String defaultValue )
{
ValueSetEntry e = getReverseEntry( sifValue );
if( e!= null ){
return e.name;
}
if( defaultValue != null ){
return defaultValue;
}
return evaluateDefault(
fDefaultAppEntry == null ? null : fDefaultAppEntry.name,
fRenderAppDefaultIfNull, sifValue );
}
/**
* Gets a ValueSet entry for the specified application value
* @param appValue
* @return The ValueSetEntry that matches, if found, or null
*/
private ValueSetEntry getEntry( String appValue )
{
ValueSetEntry e = appValue != null ? (ValueSetEntry)fTable.get(appValue) : null;
return e;
}
/**
* Looks up a ValueSetEntry by a SIF value
* @param sifValue
* @return The ValueSetEntry that matches, if found, or null
*/
private ValueSetEntry getReverseEntry( String sifValue )
{
ValueSetEntry e = sifValue != null ? (ValueSetEntry)fReverseTable.get(sifValue) : null;
return e;
}
/**
* Sets the default application value that will be returned if no match
* is found during a valueset translation
* @param appValue The value to return if there is no match. Pass in <code>Null</code> if the
* previously-set default is to be removed
* @param renderIfNull True if the default value should be returned even if the SIF Value being
* translated is NULL. If false, NULL will be returned.
* @throws ADKMappingException Thrown if the value has not yet been defined in this valueset
* by calling <code>define</code>
*/
public void setAppDefault( String appValue, boolean renderIfNull )
throws ADKMappingException
{
fRenderAppDefaultIfNull = renderIfNull;
if( fDefaultAppEntry != null ){
ValueSetEntry oldEntry = fDefaultAppEntry;
fDefaultAppEntry = null;
toXml(oldEntry, (Element)oldEntry.node );
}
if( appValue != null ){
ValueSetEntry entry = getEntry( appValue );
if( entry == null ){
throw new ADKMappingException( "Value: '" + appValue + "' is not defined.", null );
}
fDefaultAppEntry = entry;
toXml( fDefaultAppEntry, (Element)fDefaultAppEntry.node );
}
}
/**
* Sets the default SIF value that will be returned if no match
* is found during a valueset translation
* @param sifValue The value to return if there is no match. Pass in <code>Null</code> if the
* previously-set default is to be removed
* @param renderIfNull True if the default value should be returned even if the app value
* being translated is null. If false, NULL will be returned.
* @throws ADKMappingException Thrown if the value has not yet been defined in this valueset
* by calling <code>define</code>
*
* @see #define(String, String, String)
* @see #define(String, String, String, Node)
*/
public void setSifDefault( String sifValue, boolean renderIfNull )
throws ADKMappingException
{
fRenderSifDefaultIfNull = renderIfNull;
if( fDefaultSifEntry != null ){
ValueSetEntry oldEntry = fDefaultSifEntry;
fDefaultSifEntry = null;
toXml(oldEntry, (Element)oldEntry.node );
}
if( sifValue != null ){
ValueSetEntry entry = getReverseEntry( sifValue );
if( entry == null ){
throw new ADKMappingException( "Value: '" + sifValue + "' is not defined.", null );
}
fDefaultSifEntry = entry;
toXml( fDefaultSifEntry, (Element)fDefaultSifEntry.node );
}
}
/**
* Clears the table
*/
public void clear()
{
fTable.clear();
fReverseTable.clear();
fDefaultAppEntry = null;
fDefaultSifEntry = null;
fRenderAppDefaultIfNull = false;
fRenderSifDefaultIfNull = false;
}
/**
* Removes a value
*/
public void remove( String appValue )
{
if( appValue != null ) {
ValueSetEntry entry = (ValueSetEntry)fTable.get( appValue );
if( entry != null ) {
fTable.remove( appValue );
fReverseTable.remove( entry.value );
if (fNode != null) {
fNode.removeChild( entry.node );
}
if( fDefaultAppEntry == entry ){
fDefaultAppEntry = null;
}
if( fDefaultSifEntry == entry ){
fDefaultSifEntry = null;
}
}
}
}
public Map getMap() {
return fTable;
}
public Map getReverseMap() {
return fReverseTable;
}
/**
* Writes this valueset to an XML element.
* @param element
*/
public void toXml( Element element )
{
if( element == null ){
return;
}
XMLUtils.setAttribute( element, "id", fId );
XMLUtils.setOrRemoveAttribute( element, "title", fTitle );
// Add <value> elements to the <valueset>...
ValueSetEntry[] entries = getEntries();
for( int i = 0; i < entries.length; i++ )
{
Element vsElement = element.getOwnerDocument().createElement( "value" );
element.appendChild( vsElement );
entries[i].node = vsElement;
toXml( entries[i], vsElement );
}
}
private void toXml( ValueSetEntry entry, Element element )
{
// If the element passed in is null, this ValueSet doesn't currently have an
// XML Element that it is associated with. This is OK. Exit Gracefully.
if( element == null ){
return;
}
entry.toXml( element );
// Since this class controls the notion of defaults, write the
// attributes controlling defaults here
boolean isDefaultAppValue = fDefaultAppEntry == entry;
boolean isDefaultSifValue = fDefaultSifEntry == entry;
if( isDefaultAppValue ){
if( isDefaultSifValue ){
element.setAttribute( "default", "both" );
element.setAttribute( "ifnull", getIfNull( fRenderSifDefaultIfNull ) );
} else {
element.setAttribute( "default", "inbound" );
element.setAttribute( "ifnull", getIfNull( fRenderAppDefaultIfNull ) );
}
}
else if ( isDefaultSifValue ){
element.setAttribute( "default", "outbound" );
element.setAttribute( "ifnull", getIfNull( fRenderSifDefaultIfNull ) );
}
else {
element.removeAttribute( "default" );
element.removeAttribute( "ifnull" );
}
}
private String getIfNull( boolean value ){
return value ? "default" : "suppress";
}
}