package com.delcyon.capo.webapp.widgets; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import com.delcyon.capo.parsers.GrammarParser; import eu.webtoolkit.jwt.Orientation; import eu.webtoolkit.jwt.Signal1; import eu.webtoolkit.jwt.TextFormat; import eu.webtoolkit.jwt.WApplication; import eu.webtoolkit.jwt.WCompositeWidget; import eu.webtoolkit.jwt.WContainerWidget; import eu.webtoolkit.jwt.WContainerWidget.Overflow; import eu.webtoolkit.jwt.WLayout; import eu.webtoolkit.jwt.WLength; import eu.webtoolkit.jwt.WMouseEvent; import eu.webtoolkit.jwt.WText; /** * This class can be called from other threads to enable console logging to be pushed to the client. * You must call WApplication.getInstance().enableUpdates(true); for this to work. * WConsoleWidget.append is the method that you will always want to use for this. * @author jeremiah * */ public class WConsoleWidget extends WCompositeWidget { //wrapped implementation widget private WBoundedContainerWidget implemetationWidget = new WBoundedContainerWidget(); private WContainerWidget textContainerWidget = new WContainerWidget(); //we should always have a copy of the app that created us, as it's the one that we will always want to update private WApplication application = WApplication.getInstance(); private int bufferSize = 100; private boolean autoscroll = true; private ArrayList<PatternStyleHolder> regexStylePatterns = new ArrayList<>(); private ArrayList<Object[]> grammerStyles = new ArrayList<>(); private ArrayList<PatternHolder> testPatternList = new ArrayList<>(); private WText empty = new WText(); public WConsoleWidget() { setImplementation(implemetationWidget); textContainerWidget.addStyleClass("console-msgs"); textContainerWidget.setInline(false); implemetationWidget.addLayoutWidget(empty,0); implemetationWidget.addLayoutWidget(textContainerWidget,1); empty.addStyleClass("empty-console-msgs"); textContainerWidget.setOverflow(Overflow.OverflowAuto,Orientation.Vertical); textContainerWidget.setOverflow(Overflow.OverflowVisible,Orientation.Horizontal); } public void setScrollWidth(WLength wLength) { implemetationWidget.setScrollWidth(wLength); } public void setScrollHeight(WLength wLength) { implemetationWidget.setScrollHeight(wLength); } /** * set the overflow properties of the text widget contained by this console. * @param overflow * @param orientation * @param orientations */ public void setTextOverflow(Overflow overflow,Orientation orientation, Orientation...orientations) { textContainerWidget.setOverflow(overflow,orientation,orientations); } /** * sets the title of the console widget * @param title */ public void setTitle(String title) { implemetationWidget.setTitle(title); } /** * This will add a button to the toolbar that requires tha associated permission, and will call the associated click listener on clicked() * @param buttonName * @param permission * @param clickListener * @throws Exception */ public void addToolButton(String buttonName, String permission, Signal1.Listener<WMouseEvent> clickListener) throws Exception { implemetationWidget.addToolButton(buttonName, permission, clickListener); } /** * text to be displayed while there is no data on the console. * @param emptyText */ public void setEmptyText(String emptyText) { empty.setText(emptyText); } /** * This is a convenience method to run a block of code against the WApplication that this class was created by. * @param runnable */ public void execute(Runnable runnable) { WApplication.UpdateLock lock = application.getUpdateLock(); runnable.run(); if(lock != null) { application.triggerUpdate(); lock.release(); } } /** * The regex does not need to match the whole line, the tail will be automatically appended. * @param regex a regular expression with grouping, that will be matched against any appended message * @param styleClasses the CSS classes that will be applied on a per matching group basis. */ public void addRegexStyler(String regex, String...styleClasses) { regexStylePatterns.add(new PatternStyleHolder(Pattern.compile(regex),styleClasses)); } /** * Main method. Adds a pure pile of data to the widget surrounded by a div tag * @param message */ public void append(String message,TextFormat textFormat) { //must match all tests, if any for (PatternHolder patternHolder : testPatternList) { if(patternHolder.pattern.matcher(message).matches() != patternHolder.passingMatchRule) { return; } } //check to see if we have any simple regex styles to add for (PatternStyleHolder patternStyleHolder : regexStylePatterns) { if(patternStyleHolder.matches(message)) { textFormat = TextFormat.XHTMLText; message = patternStyleHolder.format(message); break; } } //process an complex grammer styles for (Object[] objects : grammerStyles) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); GrammarParser grammarParser = (GrammarParser) objects[0]; Transformer transformer = (Transformer) objects[1]; try { transformer.transform(new DOMSource(grammarParser.parse(new ByteArrayInputStream(message.getBytes()))), new StreamResult(byteArrayOutputStream)); if(byteArrayOutputStream.size() > 0) { message = new String(byteArrayOutputStream.toByteArray()); textFormat = TextFormat.XHTMLText; break; } } catch (Exception e) { e.printStackTrace(); } } //WApplication.getInstance(); this will NOT work. //You can only call getInsatnce on widget creation from that app that will need to be notified. WApplication.UpdateLock lock = application.getUpdateLock();; if(textContainerWidget.getCount() == 0) { empty.setHidden(false); } else if(empty.isHidden() == false) { empty.setHidden(true); } /* * Format and append the line to the conversation. * */ WText w = new WText(message,textFormat); w.setInline(isInline()); w.setStyleClass("console-msg-"+textFormat); textContainerWidget.addWidget(w); /* * Leave not more than getBufferSize messages in the back-log */ if (textContainerWidget.getCount() > getBufferSize()) { textContainerWidget.getChildren().get(0).remove(); } /* * Little javascript trick to make sure we scroll along with new content */ if(isAutoscroll() == true) { if(textContainerWidget.isVisible()) { application.doJavaScript("if ("+textContainerWidget.getJsRef()+" != null) {"+textContainerWidget.getJsRef() + ".scrollTop += "+ textContainerWidget.getJsRef() + ".scrollHeight;}"); } } /* * This is where the "server-push" happens. This method is called when a * new event or message needs to be notified to the user. It is being posted * from another session, but within the context of this sesssion, i.e. * with proper locking of this session. */ if(lock != null) { application.triggerUpdate(); lock.release(); } } public void destroy() { application = null; } /** * Sets number of appends allowed before we start removing the first thing appended, * Basically scroll size, except appended items can take up more than one line. * @param bufferSize */ public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } public int getBufferSize() { return bufferSize; } /** * determines whether or not we automatically scroll the window to the position of the newly appended text * @param autoscroll */ public void setAutoscroll(boolean autoscroll) { this.autoscroll = autoscroll; } public boolean isAutoscroll() { return autoscroll; } /** * Set contents margins (in pixels). * <p> * The default contents margins are 9 pixels in all directions. * </p> * * @see WLayout#setContentsMargins(int left, int top, int right, int bottom) */ public void setContentsMargins(int left, int top, int right, int bottom) { implemetationWidget.setContentsMargins(left, top, right, bottom); } /** * This takes a regex and will only append messages that match the regex * @param regex */ public void addFilter(String regex) { addFilter(regex, true); } /** * This will cause each message to be appened to be run though a grammer checker, which, if matching, * will then run the result against an xsl style sheet associated with that grammer. * @param grammarParser * @param xslDocument * @throws TransformerConfigurationException */ public void addGrammerStyle(GrammarParser grammarParser, Document xslDocument) throws TransformerConfigurationException { TransformerFactory tFactory = TransformerFactory.newInstance(); grammerStyles.add(new Object[]{grammarParser,tFactory.newTransformer(new DOMSource(xslDocument))}); } /** * expected match can be used to invert the match as opposed to writing an complicated inverse regex * * @param string * @param expected */ public void addFilter(String regex, boolean match) { Pattern pattern = Pattern.compile(regex); testPatternList.add(new PatternHolder(match, pattern)); } /** * This associates a pattern with a match rule * @author jeremiah * */ private class PatternHolder { boolean passingMatchRule = true; Pattern pattern = null; /** * @param passingMatchRule * @param pattern */ private PatternHolder(boolean passingMatchRule, Pattern pattern) { super(); this.passingMatchRule = passingMatchRule; this.pattern = pattern; } } /** * this associates a pattern with an array of style classes * @author jeremiah * */ private class PatternStyleHolder { private Pattern pattern; private String[] styleClasses; private Matcher matcher = null; private PatternStyleHolder(Pattern pattern, String[] styleClasses) { this.pattern = pattern; this.styleClasses = styleClasses; } public boolean matches(String message) { matcher = pattern.matcher(message); return matcher.find(); } /** * wraps any matched message into a number of spans based grouping from the regex. You must have a positive match test first * @param message * @return */ public String format(String message) { if(matcher == null) { return message; } StringBuffer buffer = new StringBuffer(); for(int group = 0; group < styleClasses.length && group < matcher.groupCount(); group++) { buffer.append("<span class='"+styleClasses[group]+"'>"); //matching always starts at group one as opposed to zero, zero is the whole input buffer.append(matcher.group(group+1)); buffer.append("</span>"); } //tack on anything left over on the end buffer.append(message.substring(matcher.end())); return buffer.toString(); } } }