You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@myfaces.apache.org by Cale Scholl <ca...@oracle.com> on 2008/10/30 18:50:16 UTC
[Trinidad] Resolving differences between client-side and server-side
message formatter?
Hello,
I'm pretty new to the Trinidad community, so let me apologize in advance
if there's something I've misunderstood.
What I'd like to talk about are the differences between the client-side
message formatter:
trinidad-impl\src\main\javascript\META-INF\adf\jsLibs\Core.js ->
function _formatErrorString(errorFormat, tokens)
and the server-side message formatter:
trinidad-api\src\main\java\org\apache\myfaces\trinidad\util\FastMessageFormat.java
-> public String format(Object[] source)
The problem occurs when the application developer provides a custom
error message, such as messageDetailXYZ="Isn't {0} cool?". When this is
formatted by the client formatter, everything works fine. But let's say,
for whatever reason, client validation is now disabled, and therefore
this error message is formatted by the server formatter. When it hits
the apostrophe, the rest of the message is interpreted literally, so the
resulting formatted error message is "Isnt {0} cool?". This is bound to
confuse the app developer. If the app developer instead provides
messageDetailXYZ="Isn''t {0} cool?", then both the client and server
formatter return "Isn't token0 cool?" (that is, it's formatted
correctly), but I think requiring special escaping complicates things as
well. Ultimately, I think messageDetailXYZ="Isn't {0} cool?" should be
formatted correctly by both the client and server formatter.
I realize that since the server formatter is part of the public api, it
may be difficult to change now. However, if others think it might be
possible to resolve the client and server formatter differences, then
please see the code pasted below, where I have inserted several
questions and comments of the form:
///
// my comment here
///
Thank you,
Cale
---
Let's start with the server formatter. Personally, I prefer the server
formatter's method of replacing patterns with tokens. However, if a
pattern doesn't have an associated token, I'd prefer the server
formatter just copy the pattern to the output string (in other words,
interpret it as literal text), instead of throwing an exception. This
way, there's no need to use single quotes for special escaping behavior.
trinidad-api\src\main\java\org\apache\myfaces\trinidad\util\FastMessageFormat.java:
/**
* Formats the given array of strings based on the initial
* pattern. It is legal for this array to be shorter
* than that indicated by the pattern, or to have null
* entries - these will simply be ignored.
* <p>
* @param source an array of strings
*/
public String format(Object[] source)
{
int formatLength = _formatText.length;
int length = 0;
int sourceCount = source.length;
for (int i = 0; i < sourceCount; i++)
{
Object sourceString = source[i];
if (sourceString != null)
{
length += sourceString.toString().length();
}
}
StringBuffer buffer = new StringBuffer(length + formatLength);
*///
// The above buffer size assumes that each pattern is only replaced
once, i.e.
// if {0} occurs two or more times, only one instance is replaced. If we
are
// making this assumption, then it should be enforced when replacing
patterns below.
// If it's perfectly legal for {0} to be replaced in several spots, then
we should comment
// that the above buffer size is just a rough initial estimate.
// Furthermore, if this function is only accessed by a single thread, we
should
// instead use StringBuilder for performance purposes.
/// *
int lastStart = 0;
boolean inQuote = false;
for (int i = 0; i < formatLength; i++)
{
char ch = _formatText[i];
if (inQuote)
{
if (ch == '\'')
{
buffer.append(_formatText, lastStart, i - lastStart);
i++;
lastStart = i;
inQuote = false;
}
}
else
{
if (ch == '\'')
{
buffer.append(_formatText, lastStart, i - lastStart);
i++;
lastStart = i;
// Check for doubled-up quotes
if ((i < formatLength) && (_formatText[i] == '\''))
{
// Do nothing; we'll add the doubled-up quote later
;
}
else
{
inQuote = true;
}
}
*///
// I think using single quotes to escape text is unnecessary. We should
only try to
// replace patters of the type "{number}" for which 'number' is a valid
token index.
///*
else if (ch == '{')
{
buffer.append(_formatText, lastStart, i - lastStart);
int sourceIndex = 0;
int j = i + 1;
for (; j < formatLength; j++)
{
char patternChar = _formatText[j];
if (patternChar == '}')
{
break;
}
else
{
if ((patternChar < '0') || (patternChar > '9'))
throw new
IllegalArgumentException(_LOG.getLogger().getResourceBundle().getString("FASTMESSAGEFORMAT_ONLY_SUPPORT_NUMERIC_ARGUMENTS"));
sourceIndex = (sourceIndex * 10) + (patternChar - '0');
}
}
*///
// This seems overly complex. Can't we put a hard-coded limit on the
pattern number?
// For example, are we ever going to have more than 10 tokens? It would
be much simpler
// if we only had to look for a single digit number followed by a '}'.
///*
if (j == formatLength)
throw new
IllegalArgumentException(_LOG.getLogger().getResourceBundle().getString("END_OF_PATTERN_NOT_FOUND"));
if (j == i + 1)
throw new
IllegalArgumentException(_LOG.getLogger().getResourceBundle().getString("FASTMESSAGEFORMAT_FIND_EMPTY_ARGUMENT"));
*///
// I think for the above cases we should just interpret the text
literally instead
// of throwing exceptions.
///*
if (sourceIndex < sourceCount)
{
Object sourceString = source[sourceIndex];
if (sourceString != null)
buffer.append(sourceString.toString());
}
*///
// If the pattern "{*}" contains anything besides a
// non-negative number, we throw an exception, but if it contains a
number >= the
// number of tokens, the whole "{*}" is replaced by the empty string
(I'm surprised
// that for consistency purposes that an exception isn't thrown
instead). However,
// I think that in both of these cases the pattern should just be
interpreted as literal text,
// and that the pattern should only be replaced with the empty string
when it has an
// associated token and the token is null or empty.
///*
i = j;
lastStart = i + 1;
}
else
{
// Do nothing. The character will be added in later
;
}
}
}
buffer.append(_formatText, lastStart, formatLength - lastStart);
return new String(buffer);
}
---
Now let's talk about the client formatter. Using regular expressions to
match patterns is elegant, but personally I think it's overkill for what
we need here. The server formatter's method of replacing patterns with
tokens is faster, plus it completely avoids the issue of infinite
pattern replacement, should the token itself contain "{0}" or something
like that.
trinidad-impl\src\main\javascript\META-INF\adf\jsLibs\Core.js:
/**
* Performs token replacement on the the error format, replacing each
* token found in the token Object with the value for that token.
*/
function _formatErrorString(
errorFormat, // error format string with embedded tokens to be replaced
tokens // tokens Object containin token names and values to be
replaced
)
{
var currString = errorFormat;
*///
// We have a fundamental difference in behavior between this formatter
and the
// server formatter. This formatter only tries to replace patterns for
which we
// actually have a token; the server formatter throws exceptions.
///*
// loop through all of the tokens, replacing them if they are present
for (var currToken in tokens)
{
var currValue = tokens[currToken];
// if the token has no value
if (!currValue)
{
currValue = "";
}
// TRINIDAD-829:
// we replace '{' and '}' to ensure, that tokens containing values
// like {3} aren't parsed more than once...
// Only do this if it is typeof string (see TRINIDAD-873)
if (typeof currValue == "string")
{
currValue = currValue.replace("{","{'");
currValue = currValue.replace("}","'}");
}
*///
// 1) - footnote for above text
// This is a good way to avoid the infinite pattern replacement problem;
however,
// even better would be to use the server formatter's replacement method :)
///*
// the tokens are delimited by '{' before and '}' after the token
var currRegExp = "{" + currToken + "}";
// support tokens of the form %token% as well as {token}
currString = currString.replace(new RegExp('%' + currToken + '%', 'g'),
currRegExp);
*///
// Do we still need to support tokens of the type %number%? The server
formatter
// only supports tokens of the type {number}. If one supports both
types, so should
// the other.
///*
// Replace the token. Don't use String.replace, as the value may
// include dollar signs, which leads Netscape astray (bug 2242675)
var indexOf = currString.indexOf(currRegExp);
*///
// Doesn't footnote 1) make the following check unnecessary?
/// *
if (currValue.indexOf && currValue.indexOf(currRegExp) >= 0)
{
var b1 = '';
for (i=0; i<currValue.length; i++)
{
b1 = b1 + 'placeHolderString';
}
while (indexOf >= 0)
{
currString=(currString.substring(0,indexOf)
+ b1
+ currString.substring(indexOf+currRegExp.length));
indexOf = currString.indexOf(currRegExp);
}
indexOf = currString.indexOf(b1);
while (indexOf >= 0)
{
currString =(currString.substring(0,indexOf)
+ currValue
+ currString.substring(indexOf+b1.length));
indexOf = currString.indexOf(b1);
}
}
else
while (indexOf >= 0)
{
currString = (currString.substring(0, indexOf)
+ currValue
+ currString.substring(indexOf + currRegExp.length));
indexOf = currString.indexOf(currRegExp);
}
}
// TRINIDAD-829:
// we finally re-replace the '{' and '}'...
while(currString.indexOf("{'")!=-1)
{
currString= currString.replace("{'","{");
currString= currString.replace("'}","}");
}
*///
// Having the above behavior means already existing "{'" will be
replaced as well;
// if we must keep this behavior, it should be well documented.
///*
// And now take any doubled-up single quotes down to one,
// to handle escaping
var twoSingleQuotes = /''/g;
*///
// Having the above behavior means already existing "''" will be
replaced as well;
// if we must keep this behavior, it should be well documented.
///*
return currString.replace(twoSingleQuotes, "'");
}