/* * JBoss, Home of Professional Open Source * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jboss.seam.wiki.core.wikitext.editor; import antlr.*; import org.jboss.seam.Component; import org.jboss.seam.international.Messages; import org.jboss.seam.log.Log; import org.jboss.seam.log.Logging; import org.jboss.seam.text.SeamTextLexer; import org.jboss.seam.text.SeamTextParser; import org.jboss.seam.text.SeamTextParserTokenTypes; import org.jboss.seam.wiki.core.action.Validatable; import org.jboss.seam.wiki.core.model.WikiFile; import org.jboss.seam.wiki.core.wikitext.engine.WikiLinkResolver; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; import java.util.HashSet; import java.util.Set; /** * A wiki (or plain) text editor. * * @author Christian Bauer */ public class WikiTextEditor implements Validatable, Serializable { Log log = Logging.getLog(WikiTextEditor.class); // Construction time private String key; private int valueMaxLength = 32767; private boolean valueRequired = true; private boolean allowPlaintext = false; private int rows = 20; // Editing private String value; private boolean valid = true; private boolean valuePlaintext; private boolean previewEnabled; private Set<WikiFile> linkTargets; private WikiTextEditorError lastValidationError; public WikiTextEditor(String key) { this.key = key; } public WikiTextEditor(String key, int valueMaxLength) { this.key = key; this.valueMaxLength = valueMaxLength; } public WikiTextEditor(String key, int valueMaxLength, boolean valueRequired) { this.key = key; this.valueMaxLength = valueMaxLength; this.valueRequired = valueRequired; } public WikiTextEditor(String key, int valueMaxLength, boolean valueRequired, boolean allowPlaintext) { this.key = key; this.valueMaxLength = valueMaxLength; this.valueRequired = valueRequired; this.allowPlaintext = allowPlaintext; } public WikiTextEditor(String key, int valueMaxLength, boolean valueRequired, boolean allowPlaintext, int rows) { this.key = key; this.valueMaxLength = valueMaxLength; this.valueRequired = valueRequired; this.allowPlaintext = allowPlaintext; this.rows = rows; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { // Stupid Internet Explorer textarea puts carriage returns inside the text, we don't want any of that this.value = value != null ? value.replaceAll("\r", "") : value; } public int getValueMaxLength() { return valueMaxLength; } public void setValueMaxLength(int valueMaxLength) { this.valueMaxLength = valueMaxLength; } public boolean isValueRequired() { return valueRequired; } public void setValueRequired(boolean valueRequired) { this.valueRequired = valueRequired; } public boolean isAllowPlaintext() { return allowPlaintext; } public void setAllowPlaintext(boolean allowPlaintext) { this.allowPlaintext = allowPlaintext; } public int getRows() { return rows; } public void setRows(int rows) { this.rows = rows; } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } public boolean isValuePlaintext() { return valuePlaintext; } public void setValuePlaintext(boolean valuePlaintext) { this.valuePlaintext = valuePlaintext; } public boolean isPreviewEnabled() { return previewEnabled; } public void setPreviewEnabled(boolean previewEnabled) { if (previewEnabled) { validate(); } else { setValid(true); } this.previewEnabled = previewEnabled; } public WikiTextEditorError getLastValidationError() { return lastValidationError; } public void setLastValidationError(WikiTextEditorError lastValidationError) { this.lastValidationError = lastValidationError; } public Set<WikiFile> getLinkTargets() { return linkTargets; } public void setValueAndConvertLinks(Long areaNumber, String value) { log.debug("setting value and resolving wiki://links to clear text"); WikiLinkResolver wikiLinkResolver = (WikiLinkResolver) Component.getInstance("wikiLinkResolver"); setValue(wikiLinkResolver.convertFromWikiProtocol(areaNumber, value)); } public String getValueAndConvertLinks(Long areaNumber) { log.debug("setting value and resolving clear text to wiki://links"); WikiLinkResolver wikiLinkResolver = (WikiLinkResolver)Component.getInstance("wikiLinkResolver"); linkTargets = new HashSet<WikiFile>(); return wikiLinkResolver.convertToWikiProtocol(linkTargets, areaNumber, getValue()); } public int getRemainingCharacters() { return getValue() != null ? getValueMaxLength() - getValue().length() : getValueMaxLength(); } public void switchPlaintext() { // If the user wants to switch from plain text back to wiki text, do validation if (!isValuePlaintext()) { validate(); // Allow only if valid wiki text markup setValuePlaintext(!isValid()); } else { // If the user wants plain text, then we can discard any validation errors setValid(true); lastValidationError = null; } } public void validate() { log.debug("validating value of text editor: " + key); setValid(false); if (valueRequired && (value == null || value.length() == 0)) { log.debug("validation failed for required but null or empty wiki text with key: " + key); lastValidationError = new WikiTextEditorError( Messages.instance().get("lacewiki.msg.wikiTextValidator.EmptyWikiText") ); return; } if (value != null && value.length() > getValueMaxLength()) { log.debug("validation failed for too long wiki text with key: " + key); lastValidationError = new WikiTextEditorError( Messages.instance().get("lacewiki.msg.wikiTextValidator.MaxLengthExceeded") ); return; } lastValidationError = null; setValid(true); if (!isValuePlaintext()) { try { SeamTextParser parser = getValidationParser(value); parser.startRule(); setValid(true); } // Error handling for ANTLR lexer/parser errors, see // http://www.doc.ic.ac.uk/lab/secondyear/Antlr/err.html catch (TokenStreamException tse) { setValid(false); // Problem with the token input stream throw new RuntimeException(tse); } catch (RecognitionException re) { setValid(false); lastValidationError = convertException(re); } } log.debug("completed validation of text editor value for key: " + key); } protected SeamTextParser getValidationParser(String text) { Reader r = new StringReader(text); SeamTextLexer lexer = new SeamTextLexer(r); SeamTextParser parser = new SeamTextParser(lexer); parser.setSanitizer( new SeamTextParser.DefaultSanitizer() { @Override public void validateLinkTagURI(Token token, String s) throws SemanticException { // Disable this part of the validation } @Override public String getInvalidURIMessage(String uri) { return Messages.instance().get("lacewiki.msg.wikiTextValidator.InvalidURI"); } @Override public String getInvalidElementMessage(String elementName) { return Messages.instance().get("lacewiki.msg.wikiTextValidator.InvalidElement"); } @Override public String getInvalidAttributeMessage(String elementName, String attributeName) { return Messages.instance().get("lacewiki.msg.wikiTextValidator.InvalidAttribute") + " '" + attributeName + "'"; } @Override public String getInvalidAttributeValueMessage(String elementName, String attributeName, String value) { return Messages.instance().get("lacewiki.msg.wikiTextValidator.InvalidAttributeValue") + " '" + attributeName + "'"; } } ); return parser; } // This tries to make sense of the totally useless exceptions thrown by ANTLR parser. protected WikiTextEditorError convertException(RecognitionException ex) { WikiTextEditorError error = new WikiTextEditorError(); if (ex instanceof MismatchedTokenException) { MismatchedTokenException tokenException = (MismatchedTokenException) ex; String expecting = SeamTextParser._tokenNames[tokenException.expecting]; String found = ""; if (tokenException.token.getType() != SeamTextParserTokenTypes.EOF) { error.setPosition(tokenException.getColumn()); found = ", " + Messages.instance().get("lacewiki.msg.wikiTextValidator.InsteadFound") + " " + SeamTextParser._tokenNames[tokenException.token.getType()]; } error.setFormattingErrorMessage( Messages.instance().get("lacewiki.msg.wikiTextValidator.ReachedEndAndMissing") + " " + expecting + found ); } else if (ex instanceof SeamTextParser.HtmlRecognitionException) { SeamTextParser.HtmlRecognitionException htmlException = (SeamTextParser.HtmlRecognitionException) ex; Token openingElement = htmlException.getOpeningElement(); String elementName = openingElement.getText(); String detailMsg; if (!(htmlException.getCause() instanceof MismatchedTokenException)) { detailMsg = ", " + convertException((RecognitionException)htmlException.getCause()).getMessage(); } else { detailMsg = ""; } error.setFormattingErrorMessage( Messages.instance().get("lacewiki.msg.wikiTextValidator.UnclosedInvalidHTML") + " '<"+elementName+">'" + detailMsg ); error.setPosition(openingElement.getColumn()); } else if (ex instanceof NoViableAltException) { NoViableAltException altException = (NoViableAltException) ex; String unexpected = SeamTextParser._tokenNames[altException.token.getType()]; error.setFormattingErrorMessage( Messages.instance().get("lacewiki.msg.wikiTextValidator.WrongPositionFor") + " " + unexpected ); error.setPosition(altException.getColumn()); } else { error.setFormattingErrorMessage(ex.getMessage()); error.setPosition(ex.getColumn()); } return error; } }