package ca.sqlpower.util;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* The LongMessageFormat class is intended to work identically to the
* java.text.MessageFormat class, except it allows any number of
* substitution parameters instead of just the single digits 0-9. I
* looked into overriding MessageFormat, but it makes private
* assumptions about the number of {} substitutions not exceeding 10.
* The extended format syntax like {3,date} is <em>not</em> currently
* supported (except for {x,number} and
* {x,number:<customFormat>} which are special cases for the
* Dashboard application).</p>
*
* <p>See java.text.MessageFormat in the J2SE API docs for details.
*
* @author Jonathan Fuerth
* @version $Id$
*
*/
public class LongMessageFormat extends Format {
/**
* A state for the parser's FSM.
*/
protected static final int OUTSIDE_BRACKETS=0;
/**
* A state for the parser's FSM.
*/
protected static final int INSIDE_BRACKETS=1;
/**
* The chunks of text outside the {} brackets. The String at index
* 0 comes before the first {, and the String at index 1 comes
* between the matching } and the next opening brace. Empty parts
* are stored as the empty String, not <code>null</code>.
*/
protected List stringChunks;
/**
* Instances of Format classes that will be used to render each of
* the substituted objects. If one of these is null, the argument
* object will be converted to a string implicitly by
* <code>StringBuffer.append(Object)</code>.
*/
protected Format[] formats;
/**
* There's no guarantee that the format specifiers will be given
* in ascending order; this array maps positional breaks to the
* given numbers in the original pattern string. For instance:
* <pre>
* pattern == "Hello {1}. Pleased to {2} you. Have a nice {0}!"
* formatNumber == {1, 2, 0}
* </pre>
*/
protected int[] formatNumber;
/**
* Constructs a new <code>LongMessageFormat</code> with the given pattern.
*
* @param pattern The new format pattern to parse and use.
* @throws IllegalArgumentException if the pattern format is invalid.
*/
public LongMessageFormat(String pattern) throws IllegalArgumentException {
applyPattern(pattern);
}
/**
* Formats the current pattern, substituting the objects in the
* given array for the {} substitution specifiers in the pattern.
* It's probably handier to use {@link #format(Object[],
* StringBuffer, FieldPosition)} in most cases.
*
* @param objArray An array of Objects
* @param toAppendTo
* @param ignore Not used. It is safe to pass in <code>null</code>.
* @throws IllegalArgumentException if the objArray is not of type
* Object[].
*/
public StringBuffer format(Object objArray,
StringBuffer toAppendTo,
FieldPosition ignore)
throws IllegalArgumentException {
return format((Object[])objArray, toAppendTo, ignore);
}
/**
* Formats the current pattern, substituting the objects in the
* given array for the {} substitution specifiers in the pattern.
*
* @param objArray An array of Objects
* @param toAppendTo
* @param ignore Not used. It is safe to pass in <code>null</code>.
*/
public StringBuffer format(
Object[] objArray,
StringBuffer toAppendTo,
FieldPosition ignore) {
int chunkNum = 0;
Iterator chunkIterator = stringChunks.iterator();
while (chunkIterator.hasNext()) {
toAppendTo.append(chunkIterator.next());
if (chunkIterator.hasNext()) {
Object insertMe = objArray[formatNumber[chunkNum]];
Format thisFieldFormat = formats[chunkNum];
String parsedString = null;
if (thisFieldFormat != null && insertMe != null) {
Object parsedObject = null;
try {
if (thisFieldFormat instanceof NumberFormat) {
parsedObject = new Float((String) insertMe);
} else {
parsedObject = thisFieldFormat.parseObject((String) insertMe);
}
parsedString = thisFieldFormat.format(parsedObject);
} catch (ParseException e) {
// XXX: We dump these exceptions to allow non-numerics in a number field
parsedString = insertMe.toString();
} catch (NumberFormatException e) {
// XXX: We dump these exceptions to allow non-numerics in a number field
parsedString = insertMe.toString();
}
toAppendTo.append(parsedString);
} else {
toAppendTo.append(insertMe);
}
}
chunkNum++;
}
return toAppendTo;
}
/**
* It's not feasible to parse a formatted message; the process is
* not reliable. Therefore, this method is not implemented.
*
* @param source Unused.
* @param ignore Unused.
* @throws UnsupportedOperationException when called.
*/
public Object parseObject(String source, ParsePosition ignore)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Sets the Format object to be used for formatting the
* <code>i</code>th Object in the array passed to
* <code>format()</code>. A value of <code>null</code> is
* allowed, and means to use no Format object.
*/
public void setFormat(int i, Format theFormat) {
formats[i]=theFormat;
}
/**
* Gets the Format object that will be used for formatting the
* <code>i</code>th Object in the array passed to
* <code>format()</code>. A value of <code>null</code> means no
* Format object will be used.
*/
public Format getFormat(int i) {
return formats[i];
}
/**
* Parses the given format pattern so it's ready for use in
* subsequent calls to <code>format()</code>.
*
* @param pattern The new format pattern to parse and use.
* @throws IllegalArgumentException if the pattern format is invalid.
*/
public void applyPattern(String pattern)
throws IllegalArgumentException {
int numSpecifiers=countBraces(pattern);
formatNumber=new int[numSpecifiers];
stringChunks=new LinkedList();
formats=new Format[numSpecifiers];
parsePattern(pattern);
}
/**
* Counts the number of braces in a string. Does not accept
* nested or unbalanced brace brackets.
*
* @param pattern The string which should be syntax-checked and
* counted for balanced, unnested brace bracket pairs.
* @throws IllegalArgumentException If there are nested or
* unbalanced brace brackets.
*/
protected int countBraces(String pattern) throws IllegalArgumentException {
int count=0;
int state=OUTSIDE_BRACKETS;
for(int i=0; i<pattern.length(); i++) {
char ch=pattern.charAt(i);
switch(state) {
case OUTSIDE_BRACKETS:
switch(ch) {
case '}':
throw new IllegalArgumentException(
"Found '}' without matching '{' at position "+i);
case '{':
state=INSIDE_BRACKETS;
break;
default:
break;
}
break;
case INSIDE_BRACKETS:
switch(ch) {
case '}':
state=OUTSIDE_BRACKETS;
count++;
break;
case '{':
throw new IllegalArgumentException
("Found '{' after '{' at position "+i);
default:
break;
}
}
}
if(state==INSIDE_BRACKETS) {
throw new IllegalArgumentException("Unterminated '{'");
}
return count;
}
/**
* Parses the given pattern and populates the member variables
* stringChunks and formatNumber. Assumes balanced braces in the
* string, so make sure you've already cheched the string with
* {@link #countBraces}. Also assumes the formatNumber array is
* big enough to hold the whole format mapping.
*
* <p>The implementation essentially counts the pattern like
* pegging a cribbage board. It starts at the beginning, then
* finds the first '{'. It adds the chunk between the beginning
* and the '{'. Then it pegs to the matching '}' and parses the
* number in between. Then it sets the start to be right after the
* '}' and loops.
*
* @param pattern The pattern you want to parse.
*/
protected void parsePattern(String pattern) {
int pos=0; // Current parse position in the pattern string.
int nextPos=0; // Next parse position
int blockNum=0;
nextPos=pattern.indexOf('{', pos);
while(nextPos >= 0) {
stringChunks.add(pattern.substring(pos, nextPos));
int formatNumStart=nextPos+1;
int formatNumEnd = 0;
formatNumEnd=pattern.indexOf('}', formatNumStart);
String formatNumStr=pattern.substring(formatNumStart, formatNumEnd);
String formatType = null;
int commaPos = formatNumStr.indexOf(',');
if (commaPos != -1) {
formatType = formatNumStr.substring(commaPos+1);
formatNumStr = formatNumStr.substring(0,commaPos);
}
int parsedFormatNum=0;
try {
parsedFormatNum=Integer.parseInt(formatNumStr);
} catch(NumberFormatException e) {
throw new IllegalArgumentException
("The format argument '"+formatNumStr+"' is not a number.");
}
if (formatType != null) {
if (formatType.startsWith("number")) {
if (formatType.length() == "number".length()) {
setFormat(blockNum, new ca.sqlpower.util.NaanSafeNumberFormat("#,##0.##"));
} else {
// custom DecimalFormat pattern was specified
int formatIdx = formatType.indexOf(':');
if (formatIdx < 0) {
throw new IllegalArgumentException
("Custom number format '"+formatType+"' incorrect. "
+"You must use the form \"{number:\"<format>\"}\" "
+"where <format> is a DecimalFormat pattern.");
}
String formatStr = formatType.substring(formatIdx+1);
setFormat(blockNum, new ca.sqlpower.util.NaanSafeNumberFormat(formatStr));
}
} else {
throw new IllegalArgumentException
("The format argument '"+formatType+"' is unknown.");
}
}
formatNumber[blockNum]=parsedFormatNum;
blockNum++;
pos=pattern.indexOf('}', nextPos)+1;
nextPos=pattern.indexOf('{', pos);
}
stringChunks.add(pattern.substring(pos));
}
/**
* Just a quick demonstration of usage.
*/
public static void main(String args[]) {
String pattern="0: {0,number}, 1: {1}, 2: {2}, 5: {5}, 6: {6}, 7: {7}, 8: {8}, 9: {9}, 10: {10}, 4: {4}, 11: {11}, 3: {3}, 12: {12}.";
String[] numbersEN = {
"234972349587.23947523947",
null,
null,
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
};
Object[] numbersES = {
"234972349587.23947523947",
"uno",
"dos",
"tres",
"cuatro",
"cinquo",
"seis",
"siete",
"ocho",
"nueve",
"diez",
"once",
"doce",
};
LongMessageFormat lmf=new LongMessageFormat(pattern);
System.out.println("stringChunks="+lmf.stringChunks);
System.out.print("formatNumber=[");
for(int i=0; i<lmf.formatNumber.length; i++) {
System.out.print(lmf.formatNumber[i]);
System.out.print(", ");
}
System.out.println("]");
System.out.print("In English: ");
System.out.println(lmf.format(numbersEN, new StringBuffer(), null));
System.out.print("En Espanol: ");
System.out.println(lmf.format(numbersES, new StringBuffer(), null));
}
}