cox.jmatt.java.MathTools.test
Class MathTestFormatter

java.lang.Object
  extended by cox.jmatt.java.MathTools.test.MathTestFormatter

public class MathTestFormatter
extends java.lang.Object

This class provides a more sophisticated templating engine than Question.fillTemplate(). In terms of capability it lies between that method and Apache Velocity, but far closer to the former than the latter. It uses a Velocity-like syntax but serves mostly as a text-replacement engine with a few extras.

So why not just use Velocity?

Mainly because this is included within the MathTools package and requires no external equipment, no subclassing, no re-invention of a GUI, and no custom-scripting-application construction. But if you like Velocity there is no reason whatsoever not to use it!

Okay, so why bother with this?

MathTestFormatter is written specifically to work with the other MathTools classes. It is a part of the package and it has NO external dependencies. It is included, it's easy, and it works with the other tools.

Speaking more philosophically, MathTestFormatter offers a very flexible test-centric way to format and present data. MTest does the same thing but it is much more question-centered. This class is designed to represent the test as a whole. While Questions are an essential part they are not the entirety of the test.

In addition to dealing with Question instances, this class has ample machinery to handle other aspects of the test. It can load a template String or Properties from an external source, add any other necessary data, and merge the whole thing. It can row-format a java.sql.ResultSet generated by MathDBC (think grade reports) and, since the templating machinery doesn't use any XML-dangerous characters intrinsically, generate XML output easily. MathTestFormatter offers maximum flexibility in producing a print-ready test without needing any middleware.

Finally, there is the abundance of format() methods. The basic fillTemplate() method in this class accepts a template String and a Properties object for data but there is more machinery under the hood. The formatting methods are:

The only tradeoff is complexity. MTest is considerably easier to use than this class. That is not to say using this class is hard, merely that it is not quite as easy as MTest. If the trade is worth it, use it!

Now, about Question.fillTemplate()...

The Question.fillTemplate() method was designed around the internal structure and formatting of a test question. Its replacement token syntax is both different and simpler than that used here. This was a deliberate choice meant to separate test question formatting from test formatting. One is 'internal' in intent; focusing primarily on how to deal with the data before it's ready to go on the test. The other is more 'external' in that it assumes that, by the time it is used, the internal data handling and formatting is done. The question is ready for the test and only needs to be placed appropriately.

This class is a more 'real' templating engine in that the syntax is different from Question.fillTemplate(). The templates here use an identifier-based token syntax rather than array-position-based. This makes designing the template for a whole test easier and more 'natural'; to wit, the template 'Chapter Exam ${version} ${date}' is easier to understand than 'Chapter Exam <4> <7>'.


Field Summary
static java.lang.String DEFAULT_ANSWER_KEY
          This is the default Answer key format: '@{QID}_Ans'.
static java.lang.String DEFAULT_PROBLEM_KEY
          This is the default Problem key format: '@{QID}_Prob'.
 
Constructor Summary
MathTestFormatter()
          Standard constructor for scripts.
 
Method Summary
 void addGlobalPizza()
          Add the entire contents of the Global Pizza to the internal Properties object.
 void addMap(java.util.Map pMap)
          Add a java.util.Map to the properties.
 void addProperties(java.util.Properties pNewProps)
          Add a Properties object to the internal one.
 void addProperties(java.lang.String pData, java.lang.String pLineSep, java.lang.String pKVSep)
          Add Properties to the internal Properties object.
 void addProperty(java.lang.String pKey, java.lang.String pValue)
          Add a single property to the internal Properties object.
 MathTestFormatter addQuestion(Question pQuestion)
          Add the data from a Question to the internal Properties.
 MathTestFormatter click()
          Increment the internal clicker by +1.
 void dumpDebug()
          This is a debugging tool.
static java.lang.String fillTemplate(java.lang.String pTemplate, java.util.Properties pData)
          This is the foundation method for the entire class.
 java.lang.String format()
          Format the internal template String using the internal Properties data.
 java.util.List<java.lang.String> format(java.util.List<java.lang.String> pTemplates)
          This method formats a java.util.List of template Strings into a List of filled templates using the same internal data for all.
 java.lang.String[] format(java.util.Properties[] pProps, boolean pInternal)
          Use the internal template String for an array of Properties objects, with or without the internal data.
 java.lang.String format(java.sql.ResultSet pResults)
          This method formats each row in a java.sql.ResultSet.
 java.lang.String format(java.lang.String pTemplate)
          Format the supplied template String using the internal data.
 java.lang.String[] format(java.lang.String[] pTemplates)
          This method operates exactly per the format(List) method except that it operates on a String[] array and the returned values may be null.
 void loadProperties(java.lang.String pFileName, boolean pGRAS)
          Load a Properties object from a file and add it to the currently-set internal properties.
 void loadTemplate(java.lang.String pFileName, boolean pGRAS)
          Load a template String from a file or resource.
 java.lang.String reFormat(java.lang.String pData)
          This is the basic regular-expression formatting method.
 java.lang.String[] reFormat(java.lang.String[] pData)
          This method applies reFormat() to a String[] array.
 void reset()
          Reset the internal template and Properties object to null.
 void setClicker(long pClickerValue)
          As does Question, this class maintains an internal long usable to index the key values for Questions added to the internal data.
static void setDefaultNewline(char pNewline)
          This method sets the character sequence used for the @{newline} field.
static void setDefaultTimestamp(java.lang.String pFormat)
          Set the default format for the @{timestamp} field.
 void setMathClassLoader(boolean pEnable)
          Enable or disable the Math ClassLoader for loading properties files or templates.
static void setNewDefaultNewline(char pNewline)
          Instance version.
 void setNewDefaultTimestamp(java.lang.String pFormat)
          Instance version.
 void setQuestionKeyFormat(java.lang.String pProbFormat, java.lang.String pAnsFormat)
          This method sets the key String format used when adding a Question to the internal Properties.
 boolean setRegex(java.lang.String pRegex)
          Use this method to set a regular expression for the reFormat() methods.
 boolean setRegex(java.lang.String pRegex, boolean ynLiteral, boolean ynCanonEq)
          Use this method to set a regular expression AND the two flags that cannot be set via embedded flag expressions.
 void setTemplate(java.lang.String pTemplate)
          Set (or clear) the internal template String.
 java.lang.String toString()
          Overriden to call format().
 
Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait
 

Field Detail

DEFAULT_PROBLEM_KEY

public static final java.lang.String DEFAULT_PROBLEM_KEY
This is the default Problem key format: '@{QID}_Prob'.

See Also:
Constant Field Values

DEFAULT_ANSWER_KEY

public static final java.lang.String DEFAULT_ANSWER_KEY
This is the default Answer key format: '@{QID}_Ans'.

See Also:
Constant Field Values
Constructor Detail

MathTestFormatter

public MathTestFormatter()
Standard constructor for scripts.

Method Detail

fillTemplate

public static final java.lang.String fillTemplate(java.lang.String pTemplate,
                                                  java.util.Properties pData)

This is the foundation method for the entire class. It does the actual work of filling out a template. It accepts as input a String template containing replacement tokens and a java.util.Properties object containing the values for the tokens. If either is null or empty the template String is returned unaltered. Any errors are reported at Error level.

Standard replacement tokens are of the form '${someKeyName}' where someKeyName is a key within the Properties object. When invoked this method replaces the entire replacement token with the value of the key between the curly braces. For example if the template is 'I like ${food}!' and the Properties contained the mapping 'food = gyros' the completed template would be 'I like gyros!'.

NOTE: This method makes excessive use of String replaceAll() which uses regular expressions for key-matching. This can cause problems for key-value mappings whose key contains forbidden characters. Since the dot is common it and it alone is escaped. Using a dot in a property key will work, no other collision characters will! Use with care.

In addition to the data fields defined by the Properties object, MathTestFormatter defines a group of special tokens (or fields). These are handled internally and automatically, though some can be configured. The special field tokens are:

The @{timestamp} field is configured via the setDefaultTimestamp() methods. The formatting String is per the SimpleDateFormat class; it might not be available. If not the hard-wired timestamp format is DateFormat.FULL. The @{newline} field can be configured through the setDefaultNewline() methods. The only values accepted are newline ('/n'), carriage return ('/r'), or both ('/r/n'). Once set these parameters remain in effect until they are changed. Text within the comment container ('#{' to '}#') does not appear when the template is processed. Also note that unlike other tokens the comment delimiter ('#') appears on both sides of the curly braces.

The '@{blank}' token is replaced with a single space character. This token is replaced after all other tokens have been handled, whether regular, special, or numeric. This allows templates to be 'blanked out' if desired by setting a the value of one or more keys in the Properties object to '@{blank}'. First the standard token is replaced with the value '@{blank}' which is then replaced with a single space. For those interested, the order in which tokens are replaced is:

  1. Standard tokens: values from the Properties object 'pData'.
  2. Numeric tokens: ResultSet fields or regex capture groups.
  3. Pre-defined tokens: those beginning with '@{'.
  4. Comments; '#{Comment text}#'.
  5. The '@{blank}' token.

Token order replacement should not be relevant other than for blanking, but occasional edge conditions may make it relevant.

Any errors occurring during template merging are reported at Error level. The process halts at that point.

Parameters:
pTemplate - A String template containing one or more replacement tokens.
pData - A Properties object whose keys match the replacement characters and whose mapped values will replace their template tokens.
Returns:
A new String consisting of the template with all tokens in pData replaced.

setDefaultTimestamp

public static void setDefaultTimestamp(java.lang.String pFormat)
Set the default format for the @{timestamp} field. This is per SimpleDateFormat and may not work. If not, the timestamp is formatted per DateFormat.FULL for both date and time.

Parameters:
pFormat - The formatting String for the timestamp field.

setNewDefaultTimestamp

public void setNewDefaultTimestamp(java.lang.String pFormat)
Instance version.


setDefaultNewline

public static void setDefaultNewline(char pNewline)

This method sets the character sequence used for the @{newline} field. Since there are only three choices they are represented by char:

Upper-case letters also work. Any character other than the ones listed are silently ignored.


setNewDefaultNewline

public static void setNewDefaultNewline(char pNewline)
Instance version.


reset

public void reset()
Reset the internal template and Properties object to null. This affecs only instance data, not global default values.


setTemplate

public void setTemplate(java.lang.String pTemplate)
Set (or clear) the internal template String.


addProperties

public void addProperties(java.util.Properties pNewProps)
Add a Properties object to the internal one. If pNewProps is null or empty this method returns silently.


addProperties

public void addProperties(java.lang.String pData,
                          java.lang.String pLineSep,
                          java.lang.String pKVSep)
Add Properties to the internal Properties object. The data is represented as a String containing property (line) and key-value separators. It is first split by the property separator and then each line is split (maximum 2) on the key-value separator. The resulting property is then added. If the data String is null or empty nothing happens. If either the property separator or key-value separator is null the default values are used.

Parameters:
pData - The String containing the properties data to be added.
pLineSep - The properties (line) separator to be used. Defaults to a newline.
pKVSep - The String used to separate the key from the value. Defaults to an equals sign.

addGlobalPizza

public void addGlobalPizza()
Add the entire contents of the Global Pizza to the internal Properties object.


addProperty

public void addProperty(java.lang.String pKey,
                        java.lang.String pValue)
Add a single property to the internal Properties object. If pKey is null or blank nothing happens. If pValue is null or blank the property is removed.

Parameters:
pKey - The key used to store or remove an internal mapping.
pValue - The value to store under pKey, or null to remove any stored value.

addMap

public void addMap(java.util.Map pMap)
Add a java.util.Map to the properties. This method uses the widest cast on the Map interface to allow for maximum flexibility. Null or invalid keys or values are silently ignored.

Parameters:
pMap - The Map to be added to the internal properties.

setMathClassLoader

public void setMathClassLoader(boolean pEnable)
Enable or disable the Math ClassLoader for loading properties files or templates. This method causes immediate action so changing it between load calls WILL affect them.

Parameters:
pEnable - true to use CapCom's ClassLoader, false not to.

loadProperties

public void loadProperties(java.lang.String pFileName,
                           boolean pGRAS)
Load a Properties object from a file and add it to the currently-set internal properties. Any errors are reported at Error level and nothing is loaded or added if one occurs. If the filename supplied is null or empty the method returns silently.

Parameters:
pFileName - The name of the file to load.
pGRAS - true to getResourceAsStream(), false to open as a standard File.

loadTemplate

public void loadTemplate(java.lang.String pFileName,
                         boolean pGRAS)
Load a template String from a file or resource. Parameters are per loadProperties() as is error reporting.


format

public java.lang.String format()
Format the internal template String using the internal Properties data.


format

public java.lang.String format(java.lang.String pTemplate)
Format the supplied template String using the internal data.


format

public java.lang.String format(java.sql.ResultSet pResults)

This method formats each row in a java.sql.ResultSet. The internal Properties object is available as are all the automatic fields. The column data from the ResultSet is accessed via a special set of fields defined in this method: '${X}' where X is the number of the column containing the data. Since SQL indexing is one-based the first data value is '${1}' for column 1, etc. The '${0}' token is the row count which starts at one and moves up by one with each row. The '@{count}' token is used here: it starts at zero and moves up by one.

In use, this method calls the ResultSet getObject() method to retrieve the data then casts it as String, or blank if the value is null. Each row in the ResultSet is processed and the result for each appended to a StringBuffer, whose String value is then returned. There is no end-of-line processing done so the template String should include an embedded newline or the ${newline} field.

Any errors are reported at Error level and the first such will terminate the entire process. Use with care. If pResult is null or closed the method returns null and nothing else happens.

Parameters:
pResults - The ResultSet to be processed.

format

public java.util.List<java.lang.String> format(java.util.List<java.lang.String> pTemplates)

This method formats a java.util.List of template Strings into a List of filled templates using the same internal data for all. If any template is null or blank the returned String will be empty but not null. If the List sent in is null or empty an empty (but non-null) List is returned.

The '@{count}' token is used here. It holds the (zero-based) number of the current template. That is, the first template is '0', the second is '1', etc. NOTE: '@{count}' is available to the other format() methods but its value will always be zero!

Parameters:
pTemplates - A List<String> of templates.
Returns:
A List<String> resulting from filling pTemplates with the current data.

format

public java.lang.String[] format(java.lang.String[] pTemplates)
This method operates exactly per the format(List) method except that it operates on a String[] array and the returned values may be null. The '@{count} token equals the array index of the current template String. If the array sent in is null or empty an empty array is returned.


format

public java.lang.String[] format(java.util.Properties[] pProps,
                                 boolean pInternal)

Use the internal template String for an array of Properties objects, with or without the internal data. If any element in the Properties[] array is null or empty a blank (but not null) String is returned for that entry. The returned array will be one element longer than the one sent in: element zero is the result from the internal data or blank if it is not used. If the array sent in is null or empty an empty array is returned. If the internal template is null or empty the result is an empty array.

The '@{count}' token for this method equals the index of the result array. That is, element 0 is the result of the internal data or blank, element 1 is from the first (element 0) Properties object in the array sent in, etc.

Parameters:
pProps - The Properties[] array to apply to the internal template, in order.
pInternal - true to include the internal data, false to exclude it.
Returns:
A String[] array one element longer than the data sent in.

setRegex

public boolean setRegex(java.lang.String pRegex)
Use this method to set a regular expression for the reFormat() methods. When set the regex is compiled into a Pattern which is stored internally. If the regex cannot be compiled it is reported at Debug level and no action is taken.

Parameters:
pRegex - The regular expression to compile. If null or blank false is returned immediately.
Returns:
true if it compiled, false otherwise.

setRegex

public boolean setRegex(java.lang.String pRegex,
                        boolean ynLiteral,
                        boolean ynCanonEq)
Use this method to set a regular expression AND the two flags that cannot be set via embedded flag expressions.

Parameters:
pRegex - The regex to compile into a Pattern.
ynLiteral - true to set the Pattern.LITERAL flag, false to leave it out.
ynCanonEq - true to set Pattern.CANON_EQ, false not to.

reFormat

public java.lang.String reFormat(java.lang.String pData)

This is the basic regular-expression formatting method. It uses the previously-set regex, applies it to the data String sent in, and formats the internal template against it. It is tacitly assumed that the regex contains one or more capture groups. These groups are accessed by the '${N}' fields where the number N is the (one-based) number of the capture group. The '${0}' field matches the entire data String. The replacement fields are guaranteed to be non-null; if a capture group did not match its (null) value becomes a blank String.

NOTE: All internal properties as well as the special fields are also available. If the regex does not match the only valid field will be '${0}'. If pData is null or empty '' is returned. If no regex is set the return value is the same as format(); the '${0}' field will not be replaced.

Parameters:
pData - The data String to match against the internal regex.
Returns:
The formatted internal template with capture groups replaced.

reFormat

public java.lang.String[] reFormat(java.lang.String[] pData)
This method applies reFormat() to a String[] array. This is essentially the same method but the '@{count}' field is also available; it holds the index of the element being formatted. If the array sent in is null or empty an empty array is returned. The elements of the array returned may be blank but will never be null. Each String is trimmed before it is sent in.

Parameters:
pData - The String[] array to apply the reFormat() method to.
Returns:
A non-null String[] array holding the results of the formatting.

setQuestionKeyFormat

public void setQuestionKeyFormat(java.lang.String pProbFormat,
                                 java.lang.String pAnsFormat)

This method sets the key String format used when adding a Question to the internal Properties. Each format is a template String but these are considerably simpler than the ones available to the Question class. Each template has exactly two special fields available to it:

Each format has a default value. Setting either to null or blank resets it to its default value. CRITICAL NOTE: These formats must be set properly or the data will not be accessible from the template!

Format Setting Guide

There are two ways to handle Question formatting. The first way is to set the default format for the Question class before generating any instances and using its Clicker. Alternately the ID may be set explicitly for each instance. This places the responsibility for proper identification on the Question but, since two elements are added to the internal pizza for each Question added. The default key formats handle this by appending '_Prob' or '_Ans' to the Question's ID. This is the best procedure if the Question IDs are descriptive in nature.

The second way to handle things is to ignore the Question's ID and handle everything internally here. Set the formats to something like 'Q@{INDX}_P' and 'Q@{INDX}_A' and use the internal Clicker. When using this method the Clicker MUST be initialized to the first number used on the template and the click() method MUST be called (on MathTestFormatter and NOT the Question!!!) after each Question is added.

Parameters:
pProbFormat - The template String used to format the key under which the Problem is added.
pAnsFormat - The template used to format the key for the Answer.

setClicker

public void setClicker(long pClickerValue)
As does Question, this class maintains an internal long usable to index the key values for Questions added to the internal data. Use this method to set it. Any value less than zero becomes zero.

Parameters:
pClickerValue - The new value for the internal index clicker.

click

public MathTestFormatter click()
Increment the internal clicker by +1. Returns a self-reference to allow method chaining.


addQuestion

public MathTestFormatter addQuestion(Question pQuestion)

Add the data from a Question to the internal Properties. The Problem and Answer components are each added under their respective keys, set using the setQuestionKeyFormat() method, and the @{QID} and @{INDX} fields will be set appropriately. A self-reference is returned to allow method chaining. If the pQuestion is null this method returns silently. If the Problem or Answer is null or blank it is not added.

The Question data is extracted via formatProblem() and formatAnswer() so it is critical to make sure the formats cooperate with each other. The best thing to do is set the Question ID format to identify the Question (as it should) and nothing else. Set the Problem and Answer formats to '<1>' and '<2>' respectively and don't worry about key-value-formatting. Include the '@{QID}' token when setting Problem- and Answer-key formats here.

Parameters:
pQuestion - The Question to be added to the internal data.
Returns:
A reference to this.

dumpDebug

public void dumpDebug()
This is a debugging tool. It prints all internal data at the Debug level.


toString

public java.lang.String toString()
Overriden to call format(). If nothing is set a default String is printed.

Overrides:
toString in class java.lang.Object