//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library;
import java.lang.reflect.*;
import java.util.*;
import openadk.library.tools.mapping.FieldAdaptor;
/**
* The default ValueBuilder implementation evaluates an expression to produce
* a string value.<p>
*
* The ValueBuilder interface is used by the SIFDTD, SIFDataObject, and Mappings
* classes when evaluating XPath-like query strings. It enables developers to
* customize the way the ADK evaluates value expressions in these query strings
* to produce a value for a SIF element or attribute. The DefaultValueBuilder
* implementation supports <code>$(variable)</code> token replacement as well as
* <code>@com.class.method</code> style calls to static Java methods.
* <p>
*
* <b>Token Replacement</b><p>
*
* When a <code>$(variable)</code> token is found in an expression, it is
* replaced with a value from the Map passed to <code>evaluate</code>. For
* example, if the Map constains the entry "color=blue", calling the <code>evaluate</code>
* method with the expression "The color is $(color)" would produce the
* string "The color is blue".
* <p>
*
* <b>Java Method Calls</b><p>
*
* When a <code>@method( arg1, arg2, ... )</code> call is found in an expression,
* the static Java method is called and its return value inserted into the value
* string. Token replacement is performed before calling the method.
* If <code>method</code> is not fully-qualified, it is assumed to be a method
* declared by this DefaultValueBuilder class. The default class can be changed
* by calling the <code>setDefaultClass</code> method. When writing your own
* static method, the first parameter must be of type ValueBuilder; zero or
* more String parameters may follow. The function must return a String:
* <code>String method( ValueBuilder vb, String p1, String p2, ... )</code>.
* <p>
*
* In the following example, the toUpperCase static method is called to convert
* the $(color) variable to uppercase. This expression would yield the result
* "The color is BLUE":
*
* <code>The color is @openadk.library.DefaultValueBuilder.toUpperCase( $(color) )</code>
*
* The following static methods are defined by this class:
*
* <table>
* <tr>
* <td><b>Method</b></td>
* <td><b>Description</b></td>
* </tr>
* <tr>
* <td><code>pad( source, padding, width )</code></td>
* <td>
* Pads the <i>source</i> string with <i>padding</i> such that the
* resulting string is <i>width</i> characters in length.
* </td>
* </tr>
* <tr>
* <td><code>toUpperCase( source )</code></td>
* <td>
* Converts the <i>source</i> string to uppercase
* </td>
* </tr>
* <tr>
* <td><code>toLowerCase( source )</code></td>
* <td>
* Converts the <i>source</i> string to lowercase
* </td>
* </tr>
* <tr>
* <td><code>toMixedCase( source )</code></td>
* <td>
* Converts the <i>source</i> string to mixed case
* </td>
* </tr>
* </table>
*
*
*/
public class DefaultValueBuilder implements ValueBuilder
{
private static String sDefClass = "openadk.library.DefaultValueBuilder";
protected static Hashtable sAliases = new Hashtable();
protected FieldAdaptor fVars;
protected SIFFormatter fFormatter;
/**
* Creates an instance of DefaultValueBuilder that builds values based
* on the SIFDataMap, using the ADK's default text formatter
* @param data
*/
public DefaultValueBuilder( FieldAdaptor data )
{
this( data, ADK.getTextFormatter() );
}
/**
* Creates an instance of DefaultValueBuilder that builds values based
* on the SIFDataMap, using the specified <code>SIFFormatter</code> instance
* @param data
* @param formatter
*/
public DefaultValueBuilder( FieldAdaptor data, SIFFormatter formatter )
{
fVars = data;
fFormatter = formatter;
}
/**
* Returns the MappingsAdaptor
* @return The MappingsAdaptor passed to the constructor
*/
public FieldAdaptor getData()
{
return fVars;
}
/**
* Evaluate an expression that the implementation of this interface
* understands to return a String value.<p>
*
* @param expression The expression to evaluate
* @return The value built from the expression
*/
public String evaluate( String expression )
{
return java( replaceTokens( expression, fVars, fFormatter ) );
}
/**
* Calls all Java methods referenced in the source string to replace the
* method reference with the string representation of the method's return
* value
*/
public String java( String src )
{
if( src == null )
return null;
StringBuffer b = new StringBuffer();
int len = src.length();
int at = 0;
int mark = 0;
int x = 0;
do
{
at = src.indexOf("@",at);
if( at == -1 ) {
b.append( src.substring(mark) );
at = len;
}
if( at < len )
{
ParseResults mm = ParseResults.parse( src, at );
if( mm != null )
{
b.append( src.substring(mark,at) );
mark = mm.Position;
String method = mm.MethodName;
MyStringTokenizer params = mm.Parameters;
int paramCount = params.countTokens();
int methodParamCount = 0;
Class targetClass = null;
try
{
x = method.lastIndexOf('.');
if( x != -1 )
{
// Use the fully-qualified Java method
targetClass = Class.forName( method.substring(0,x) );
method = method.substring(x+1);
}
else
{
// Was an alias registered?
String aliasClass = (String)sAliases.get( method );
if( aliasClass != null )
targetClass = Class.forName( aliasClass );
}
if( targetClass == null )
{
// Use the default class
targetClass = Class.forName( sDefClass );
}
}
catch( ClassNotFoundException cnfe ) {
throw new RuntimeException( "Class not found: " + method );
}
Method targetMethod = null;
Method[] methods = targetClass.getMethods();
for( int m = 0; m < methods.length; m++ ) {
if( methods[m].getName().equals(method) ) {
targetMethod = methods[m];
methodParamCount = methods[m].getParameterTypes().length;
break;
}
}
if( targetMethod == null )
throw new RuntimeException( "Java method not found: " + method );
x = 1;
Object[] args = new Object[ methodParamCount ];
args[0] = this;
// Assign parameters
while( x < params.countTokens() + 1 && x < methodParamCount ) {
args[x] = params.getToken( x-1 );
x++;
}
// Fill in any remaining parameters with a blank string value
while( x < methodParamCount ) {
args[x++] = "";
}
at = mark;
try {
Object result = targetMethod.invoke( null, args );
if( result != null )
b.append( result.toString() );
} catch( Exception ex ) {
throw new RuntimeException( "Failed to call Java method '" + method + "': " + ex.toString() );
}
continue;
}
b.append( src.substring(mark,len) );
at = len;
}
}
while( at < len );
return b.toString();
}
/**
* Replaces all <code>$(variable)</code> tokens in the source string with
* the corresponding entry in the supplied Map<p>
* @param src The source string
* @param adaptor A set of data values
* @param formatter The <code>SIFFormatter</code> to use for creating String representations
* of SIF data.
*/
public static String replaceTokens( String src, FieldAdaptor adaptor, SIFFormatter formatter )
{
if( src == null )
return null;
StringBuffer b = new StringBuffer();
int len = src.length();
int at = 0;
int mark = 0;
do
{
at = src.indexOf("$(",at);
if( at == -1 )
at = len;
b.append( src.substring(mark,at) );
if( at < len )
{
int i = src.indexOf(")",at+2);
if( i != -1 )
{
mark = i+1;
String key = src.substring(at+2,i);
at = mark;
Object val = adaptor.getValue( key );
if( val != null ){
b.append( val );
}
}
else
{
b.append( src.substring(mark,len) );
at = len;
}
}
}
while( at < len );
return b.toString();
}
////////////////////////////////////////////////////////////////////////////////
/**
* "@pad( Source, PadChar, Width )"
*
* Pads the Source string with the specified PadChar character so that the
* source string is at least Width characters in length. If the Source
* string is already equal to or greater than Width, no action is taken.
*/
public static String pad( ValueBuilder vb, String source, String padding, String width )
{
try
{
String _source = source.trim();
int _width = Integer.parseInt( width.toString().trim() );
if( _source.length() >= _width )
return _source;
String _padding = padding.toString().trim();
StringBuffer b = new StringBuffer();
for( int i = _source.length(); i < _width; i++ )
b.append( _padding );
b.append( _source );
return b.toString();
}
catch( Throwable thr )
{
return source.trim();
}
}
/**
* "@toUpperCase( Source )"
*
* Converts the source string to uppercase
*/
public static String toUpperCase( ValueBuilder vb, String source )
{
try
{
return source.trim().toUpperCase();
}
catch( Throwable thr )
{
return source;
}
}
/**
* "@toLowerCase( Source )"
*
* Converts the source string to lowercase
*/
public static String toLowerCase( ValueBuilder vb, String source )
{
try
{
return source.trim().toLowerCase();
}
catch( Throwable thr )
{
return source;
}
}
/**
* "@toMixedCase( Source )"
*
* Converts the source string to mixed case
*/
public static String toMixedCase( ValueBuilder vb, String source )
{
try
{
StringBuffer b = new StringBuffer();
String _source = source.trim();
for( int i = 0; i < _source.length(); i++ )
{
if( i == 0 || _source.charAt(i-1) == ' ' )
b.append( Character.toUpperCase( _source.charAt(i) ) );
else
b.append( Character.toLowerCase( _source.charAt(i) ) );
}
return b.toString();
}
catch( Throwable thr )
{
return source;
}
}
/**
* Specifies the default class for Java method calls that do not reference
* a fully-qualified class name. <code>openadk.library.DefaultValueBuilder</code>
* is used as the default unless this method is called to change it.
* <p>
*
* @param clazz The name of a class (e.g. "openadk.library.DefaultValueBuilder")
*/
public static void setDefaultClass( String clazz )
{
sDefClass = clazz;
}
/**
* Registers an alias to a static Java method.<p>
* @param alias The alias name (e.g. "doSomething")
* @param method The fully-qualified Java method name (e.g. "com.mycompany.MyValueBuilder.doSomething")
*/
public static void addAlias( String alias, String method )
{
int i = method.lastIndexOf(".");
if( i != -1 ) {
sAliases.put( alias, method.substring(0,i) );
} else {
sAliases.put( alias, method );
}
}
/**
* A java.util.StringTokenizer replacement. We need a replacement for two
* reasons: first, Java's default does not consider empty tokens to be
* tokens. For example, ",,,blue" is considered to have one token, not 4.
* Second, we need to ignore commas in literal strings so that a parameter
* to a method can itself be comprised of delimiters. If a double-quote is
* found, all commas until the next double-quote are considered literal.
* The commas are not included in the resulting tokens.
*/
static class MyStringTokenizer
{
private String[] fTokens;
public MyStringTokenizer( String src, char delimiter )
{
// Parse the source string into an array of tokens
Vector v = new Vector();
if( src != null )
{
int i = 0;
boolean inQuote = false;
StringBuffer token = new StringBuffer();
while( i < src.length() )
{
if( src.charAt(i) == '"' )
{
inQuote = !inQuote;
}
else
if( src.charAt(i) == delimiter )
{
if( inQuote )
token.append( delimiter );
else {
v.add( token.toString() );
token.setLength(0);
}
}
else
token.append( src.charAt(i) );
i++;
}
v.add( token.toString() );
}
fTokens = new String[ v.size() ];
v.copyInto( fTokens );
}
public int countTokens()
{
return fTokens.length;
}
public String getToken( int i )
{
return fTokens[i];
}
}
/**
* A helper class to parse "@method( parameterlist )" into Java method name
* and parameter list, and to return the position in the source string where
* the caller should continue processing.
*/
static class ParseResults
{
// Position in source string immediately after closing parenthesis
public int Position;
// The name of the Java method
public String MethodName;
// The parameters to the Java method
public MyStringTokenizer Parameters;
/**
* Given a source string and an index into that string where a Java
* method begins (i.e. the location of the @ character), parse the name
* of the Java method and the list of parameters. Return a new ParseResults
* instance of both components were found, otherwise return null.<p>
*
* For example, if this string were passed: '@random() @strip("(801) 323-1131")',
* and the <i>position</i> parameter were 11, this function would return
* a new ParseResults with Position set to 32, MethodName set to 'strip',
* and Parameters having a single parameter of "(801) 323-1131".
*/
public static ParseResults parse( String src, int position )
{
ParseResults results = new ParseResults();
int i = position + 1;
boolean inQuote = false;
StringBuffer buf = new StringBuffer();
while( i < src.length() )
{
if( src.charAt(i) == '"' )
{
inQuote = !inQuote;
}
else
if( src.charAt(i) == '(' )
{
if( !inQuote ) {
results.MethodName = src.substring(position+1,i);
buf.setLength(0);
} else {
buf.append( '(' );
}
}
else
if( src.charAt(i) == ')' )
{
if( !inQuote ) {
results.Parameters = new MyStringTokenizer( buf.toString(), ',' );
results.Position = i + 1;
return results;
} else {
buf.append( ')' );
}
}
else
buf.append( src.charAt(i) );
i++;
}
return null;
}
}
}