package org.jboss.seam.ui.validator;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
import org.jboss.seam.text.SeamTextLexer;
import org.jboss.seam.text.SeamTextParser;
import antlr.*;
/**
* Formatted Text validator
*
* Use as a JSF validator on an input control that allows entering Seam Text
* markup.
* <p>
* The Seam Text parser has a disabled default error handler, catch exceptions
* as appropriate if you display Seam Text (see <a
* href="http://www.doc.ic.ac.uk/lab/secondyear/Antlr/err.html">http://www.doc.ic.ac.uk/lab/secondyear/Antlr/err.html</a>)
* and call the static convenience method
* <tt>FormattedTextValidator.getErrorMessage(originalText, recognitionException)</tt>
* if you want to display or log a nice error message.
* </p>
* <p>
* Uses an instance of <tt>SeamTextParser</tt> by default, override if you require
* validation with your customized instance of <tt>SeamTextParser</tt>.
* </p>
*
* @author matthew.drees
* @author Christian Bauer
*/
public class FormattedTextValidator implements javax.faces.validator.Validator, Serializable {
private static final long serialVersionUID = 1L;
private static final int NUMBER_OF_CONTEXT_CHARS_AFTER = 10;
private static final int NUMBER_OF_CONTEXT_CHARS_BEFORE = 10;
private static final String END_OF_TEXT = "END OF TEXT";
String firstError;
String firstErrorDetail;
/**
* Validate the given value as well-formed Seam Text. If there are parse
* errors, throw a ValidatorException including the first parse error.
*/
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
firstError = null;
firstErrorDetail = null;
if (value == null) {
return;
}
if (!(value instanceof String)) {
throw new IllegalArgumentException("Value is not a string: "
+ value);
}
String text = (String) value;
SeamTextParser parser = getSeamTextParser(text);
try {
parser.startRule();
}
// Error handling for ANTLR lexer/parser errors, see
// http://www.doc.ic.ac.uk/lab/secondyear/Antlr/err.html
catch (TokenStreamException tse) {
// Problem with the token input stream
throw new RuntimeException(tse);
} catch (RecognitionException re) {
// A parser error
if (firstError == null) {
firstError = getParserErrorMessage(text, re);
firstErrorDetail = re.getMessage().replace("\uFFFF",END_OF_TEXT);
}
}
if (firstError != null) {
throw new ValidatorException(new FacesMessage(firstError, firstErrorDetail));
}
}
/**
* Override to instantiate a custom <tt>SeamTextLexer</tt> and <tt>SeamTextParser</tt>.
*
* @param text the raw markup text
* @return an instance of <tt>SeamTextParser</tt>
*/
public SeamTextParser getSeamTextParser(String text) {
Reader r = new StringReader(text);
SeamTextLexer lexer = new SeamTextLexer(r);
return new SeamTextParser(lexer);
}
public String getParserErrorMessage(String originalText, RecognitionException re) {
String parserErrorMsg;
if (NoViableAltException.class.isAssignableFrom(re.getClass())) {
parserErrorMsg = getNoViableAltErrorMessage(
re.getMessage(),
getErrorLocation(originalText, re, getNumberOfCharsBeforeErrorLocation(), getNumberOfCharsAfterErrorLocation())
);
} else if (MismatchedTokenException.class.isAssignableFrom(re.getClass())) {
parserErrorMsg = getMismatchedTokenErrorMessage(
re.getMessage(),
getErrorLocation(originalText, re, getNumberOfCharsBeforeErrorLocation(), getNumberOfCharsAfterErrorLocation())
);
} else if (SemanticException.class.isAssignableFrom(re.getClass())) {
parserErrorMsg = getSemanticErrorMessage(re.getMessage());
} else {
parserErrorMsg = re.getMessage();
}
return parserErrorMsg;
}
public int getNumberOfCharsBeforeErrorLocation() {
return NUMBER_OF_CONTEXT_CHARS_BEFORE;
}
public int getNumberOfCharsAfterErrorLocation() {
return NUMBER_OF_CONTEXT_CHARS_AFTER;
}
/**
* Override (e.g. for i18n) ANTLR parser error messages.
*
* @param originalMessage the ANTLR parser error message of the RecognitionException
* @param location a snippet that indicates the location in the original markup, might be null
* @return a message that is thrown by this validator
*/
public String getNoViableAltErrorMessage(String originalMessage, String location) {
return location != null
? "Text parsing error at '..." + location.trim() + "...'"
: "Text parsing error, " + originalMessage.replace("\uFFFF",END_OF_TEXT);
}
/**
* Override (e.g. for i18n) ANTLR parser error messages.
*
* @param originalMessage the ANTLR parser error message of the RecognitionException
* @param location a snippet that indicates the location in the original markup, might be null
* @return a message that is thrown by this validator
*/
public String getMismatchedTokenErrorMessage(String originalMessage, String location) {
return location != null
? "Text parsing error at '..." + location.trim() + "...'"
: "Text parsing error, " + originalMessage.replace("\uFFFF",END_OF_TEXT);
}
/**
* Override (e.g. for i18n) ANTLR parser error messages.
*
* @param originalMessage the ANTLR parser error message of the RecognitionException
* @return a message that is thrown by this validator
*/
public String getSemanticErrorMessage(String originalMessage) {
return "Text parsing error, " + originalMessage.replace("\uFFFF",END_OF_TEXT);
}
/**
* Extracts the error from the <tt>RecognitionException</tt> and generates
* a location of the error by extracting the original text at the exceptions
* line and column.
*
* @param originalText
* the original Seam Text markup as fed into the parser
* @param re
* an ANTLR <tt>RecognitionException</tt> thrown by the parser
* @param charsBefore
* characters before error location included in message
* @param charsAfter
* characters after error location included in message
* @return an error message with some helpful context about where the error
* occured
*/
public static String getErrorLocation(String originalText, RecognitionException re, int charsBefore, int charsAfter) {
int beginIndex = Math.max(re.getColumn() - 1 - charsBefore, 0);
int endIndex = Math.min(re.getColumn() + charsAfter, originalText.length());
String location = null;
// Avoid IOOBE even if what we show is wrong, we need to figure out why the indexes are off sometimes
if (beginIndex > 0 && beginIndex < endIndex && endIndex > 0 && endIndex < originalText.length())
location = originalText.substring(beginIndex, endIndex);
if (location == null) return location;
// Filter some dangerous characters we do not want in error messages
return location.replace("\n", " ").replace("\r", " ").replace("#{", "# {");
}
/**
* Extracts the error from the <tt>RecognitionException</tt> and generates
* a message including the location of the error.
*
* @param originalText
* the original Seam Text markup as fed into the parser
* @param re
* an ANTLR <tt>RecognitionException</tt> thrown by the parser
* @return an error message with some helpful context about where the error
* occured
*/
public static String getErrorMessage(String originalText, RecognitionException re) {
return re.getMessage().replace("\uFFFF",END_OF_TEXT)
+ " at '"
+ getErrorLocation(
originalText, re,
NUMBER_OF_CONTEXT_CHARS_BEFORE, NUMBER_OF_CONTEXT_CHARS_AFTER
)
+ "'";
}
}