1 package org.vaadin.console;
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.io.OutputStream;
6 import java.io.PrintStream;
7 import java.io.Serializable;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.List;
15 import java.util.regex.Pattern;
17 import org.vaadin.console.ansi.ANSICodeConverter;
18 import org.vaadin.console.ansi.DefaultANSICodeConverter;
19 import org.vaadin.rpc.ServerSideHandler;
20 import org.vaadin.rpc.ServerSideProxy;
21 import org.vaadin.rpc.client.Method;
23 import com.vaadin.terminal.PaintException;
24 import com.vaadin.terminal.PaintTarget;
25 import com.vaadin.ui.AbstractComponent;
26 import com.vaadin.ui.Component;
29 * Server side component for the VTextConsole widget.
31 * @author Sami Ekblad / Vaadin
34 @com.vaadin.ui.ClientWidget(org.vaadin.console.client.ui.VTextConsole.class)
35 public class Console extends AbstractComponent implements Component.Focusable {
37 private static final long serialVersionUID = 590258219352859644L;
38 private Handler handler;
39 private ANSICodeConverter ansiToCSSconverter;
40 private boolean isConvertANSIToCSS = false;
41 private final HashMap<String, Command> commands = new HashMap<String, Command>();
42 private final Config config = new Config();
44 private static final String DEFAULT_PS = "}> ";
45 private static final String DEFAULT_GREETING = "Console ready.";
46 private static final int DEFAULT_BUFFER = 0;
47 private static final int DEFAULT_COLS = -1;
48 private static final int DEFAULT_ROWS = -1;
49 private static final boolean DEFAULT_WRAP = true;
50 private static final boolean DEFAULT_PRINT_PROMPT_ON_INPUT = true;
51 private static final boolean DEFAULT_SMART_SCROLL_TO_END = false;
52 private static final int MAX_COLS = 500;
53 private static final int MAX_ROWS = 200;
55 public boolean isWrap() {
59 public void setWrap(final boolean wrap) {
61 client.call("setWrap", wrap);
65 * @return true, if entered by user text immediately will print to console, false otherwise
67 public boolean isPrintPromptOnInput() {
68 return config.isPrintPromptOnInput;
72 * @param isPrintPromptOnInput if true - entered by user text immediately will be printed to
73 * console, nothing happens otherwise
75 public void setPrintPromptOnInput(final boolean isPrintPromptOnInput) {
76 config.isPrintPromptOnInput = isPrintPromptOnInput;
77 client.call("setPrintPromptOnInput", isPrintPromptOnInput);
81 * @return true, if method scrollToEnd will only work if last scroll state was "end"
82 * (like Linux KDE console emulator "Konsole"), false otherwise (by default)
84 public boolean isSmartScrollToEnd() {
85 return config.isSmartScrollToEnd;
89 * @param isSmartScrollToEnd if true - method scrollToEnd will only work if last scroll
90 * state was "end" (like Linux KDE console emulator "Konsole"); false by default
92 public void setSmartScrollToEnd(final boolean isSmartScrollToEnd) {
93 config.isSmartScrollToEnd = isSmartScrollToEnd;
94 client.call("setSmartScrollToEnd", isSmartScrollToEnd);
97 private final ServerSideProxy client = new ServerSideProxy(
98 new ClientCallback());
101 * Inner class to handle client calls.
104 public class ClientCallback implements ServerSideHandler {
106 private static final long serialVersionUID = 3992611573500588703L;
108 public void requestRepaint() {
109 Console.this.requestRepaint();
112 public Object[] initRequestFromClient() {
113 return new Object[] {
116 config.maxBufferSize,
118 config.isPrintPromptOnInput,
119 config.isSmartScrollToEnd,
125 public void callFromClient(String method, Object[] params) {
126 // TODO Auto-generated method stub
133 * The tab order number of this field.
135 private int tabIndex = 0;
136 @SuppressWarnings("unused")
137 private Integer fontw;
138 @SuppressWarnings("unused")
139 private Integer fonth;
140 private PrintStream printStream;
141 private String lastSuggestInput;
142 private List<CommandProvider> commandProviders;
145 * An inner class for holding the configuration data.
147 public static class Config implements Serializable {
149 private static final long serialVersionUID = -812601232248504108L;
151 int maxBufferSize = DEFAULT_BUFFER;
152 int cols = DEFAULT_COLS;
153 int rows = DEFAULT_ROWS;
154 boolean wrap = DEFAULT_WRAP;
155 boolean isPrintPromptOnInput = DEFAULT_PRINT_PROMPT_ON_INPUT;
156 boolean isSmartScrollToEnd = DEFAULT_SMART_SCROLL_TO_END;
157 String ps = DEFAULT_PS;
158 String greeting = DEFAULT_GREETING;
163 * Console Handler interface.
165 * Handler provides a hook to handle various console related events and
166 * override the default processing.
169 public interface Handler extends Serializable {
172 * Called when user uses TAB to complete the command entered into the
179 Set<String> getSuggestions(Console console, String lastInput);
182 * Called when user has entered input to the Console and presses enter
188 void inputReceived(Console console, String lastInput);
191 * Handle an exception during a Command execution.
198 void handleException(Console console, Exception e, Command cmd,
202 * Handle situation where a command could not be found.
207 void commandNotFound(Console console, String[] argv);
212 * Commands that can be executed against the Component instance. They
213 * provide convenient string to method mapping. Basically, a Command is a
214 * method that that can be executed in Component. It can have parameters or
218 public interface Command extends Serializable {
221 * Execute a Command with arguments.
228 public Object execute(Console console, String[] argv) throws Exception;
231 * Get usage information about this command.
237 public String getUsage(Console console, String[] argv);
241 * Interface for providing Commands to the console. One can register a
242 * command providers to console instead of individual commands to provide a
246 public interface CommandProvider extends Serializable {
249 * List all available command from this provider.
254 Set<String> getAvailableCommands(Console console);
257 * Get Command instance based on command name.
263 Command getCommand(Console console, String commandName);
267 public void addCommandProvider(final CommandProvider commandProvider) {
268 if (commandProviders == null) {
269 commandProviders = new ArrayList<CommandProvider>();
271 commandProviders.add(commandProvider);
274 public void removeCommandProvider(final CommandProvider commandProvider) {
275 if (commandProviders == null) {
278 commandProviders.remove(commandProvider);
281 public void removeAllCommandProviders() {
282 if (commandProviders == null) {
285 commandProviders.clear();
288 public Console(final Console.Handler handler) {
295 setHandler(new DefaultConsoleHandler());
296 setANSIToCSSConverter(new DefaultANSICodeConverter());
300 private void registerCallbacks() {
301 client.register("suggest", new Method() {
303 public void invoke(String methodName, Object[] params) {
304 handleSuggest((String) params[0]);
307 client.register("input", new Method() {
309 public void invoke(String methodName, Object[] params) {
310 handleInput((String) params[0]);
317 public void paintContent(final PaintTarget target) throws PaintException {
318 super.paintContent(target);
319 client.paintContent(target);
323 public void changeVariables(final Object source,
324 final Map<String, Object> variables) {
325 super.changeVariables(source, variables);
326 client.changeVariables(source, variables);
327 if (variables.containsKey("width")) {
328 setWidth((String) variables.get("width"));
330 if (variables.containsKey("height")) {
331 setHeight((String) variables.get("height"));
333 if (variables.containsKey("fontw")) {
334 fontw = (Integer) variables.get("fontw");
336 if (variables.containsKey("fonth")) {
337 fonth = (Integer) variables.get("fonth");
339 if (variables.containsKey("cols")) {
340 config.cols = (Integer) variables.get("cols");
342 if (variables.containsKey("rows")) {
343 config.rows = (Integer) variables.get("rows");
348 * Overridden to filter client-side calculation/changes and avoid loops.
352 public void setWidth(float width, int unit) {
353 if (width != getWidth() || unit != getWidthUnits()) {
354 super.setWidth(width, unit);
359 * Overridden to filter client-side calculation/changes and avoid loops.
363 public void setHeight(float height, int unit) {
364 if (height != getHeight() || unit != getHeightUnits()) {
365 super.setHeight(height, unit);
369 protected void handleSuggest(final String input) {
371 final boolean cancelIfNotASingleMatch = (input != null && !input
372 .equals(lastSuggestInput));
373 lastSuggestInput = input;
375 final Set<String> matches = handler.getSuggestions(this, input);
377 if (matches == null || matches.size() == 0) {
382 // Output the original
383 final String prefix = parseCommandPrefix(input);
384 String output = input.substring(0, input.lastIndexOf(prefix));
385 if (matches.size() == 1) {
386 // Output the only match
387 output += matches.iterator().next() + " "; // append the single
391 // We output until the common prefix
392 StringBuilder commonPrefix = new StringBuilder(prefix);
393 final int maxLen = matches.iterator().next().length();
394 for (int i = prefix.length(); i < maxLen; i++) {
396 boolean charMatch = true;
397 for (final String m : matches) {
400 } else if (i < m.length()) {
401 charMatch &= m.charAt(i) == c;
411 commonPrefix.append(c);
414 output += commonPrefix.toString();
415 if (prefix.equals(commonPrefix.toString())
416 && !cancelIfNotASingleMatch) {
417 final StringBuffer suggestions = new StringBuffer("\n");
418 for (final String m : matches) {
419 suggestions.append(" " + m);
421 print(suggestions.toString());
424 lastSuggestInput = output; // next suggest will not beep
436 protected void handleInput(final String input) {
438 // Ask registered handler
439 handler.inputReceived(this, input);
443 protected void parseAndExecuteCommand(final String input) {
444 final String[] argv = parseInput(input);
445 if (argv != null && argv.length > 0) {
446 final Command c = getCommand(argv[0]);
448 final String result = executeCommand(c, argv);
449 if (result != null) {
453 handler.commandNotFound(this, argv);
458 protected String executeCommand(final Command cmd, final String[] argv) {
460 final Object r = cmd.execute(this, argv);
461 return r != null ? "" + r : null;
462 } catch (final Exception e) {
463 handler.handleException(this, e, cmd, argv);
468 protected String parseCommandPrefix(final String input) {
472 if (!input.endsWith(" ")) {
473 final String[] argv = parseInput(input);
474 if (argv != null && argv.length > 0) {
475 return argv[argv.length - 1];
481 protected static String[] parseInput(final String input) {
482 if (input != null && !"".equals(input.trim())) {
483 final String[] temp = input.split(" ");
484 if (temp != null && temp.length > 0) {
485 final List<String> parsed = new ArrayList<String>(temp.length);
486 String current = null;
487 for (final String element : temp) {
488 final int quotCount = count(element, '\"');
489 if (quotCount > 0 && quotCount % 2 != 0) {
490 // uneven number of quotes star or end combining params
491 if (current != null) {
492 parsed.add(current + " "
493 + element.replaceAll("\"", "")); // end
496 current = element.replaceAll("\"", ""); // start
498 } else if (current != null) {
499 current += " " + element.replaceAll("\"", "");
501 parsed.add(element.replaceAll("\"", ""));
505 // TODO: actually this is not quite right: We have an open quote
506 // somewhere. Exception maybe?
507 if (current != null) {
508 parsed.add(current.replaceAll("\"", ""));
510 return parsed.toArray(new String[] {});
513 return new String[] {};
516 protected static int count(final String sourceString, final char lookFor) {
517 if (sourceString == null) {
521 for (int i = 0; i < sourceString.length(); i++) {
522 final char c = sourceString.charAt(i);
530 public void print(final String output) {
531 if(isConvertANSIToCSS){
532 client.call("print", "");
533 appendWithProcessingANSICodes(output);
535 client.call("print", output);
539 * Print text with predefined in theme CSS class.
542 * @param className CSS class name for string
544 public void print(final String output, final String className) {
545 client.call("print", output, className);
548 public String getGreeting() {
549 return config.greeting;
552 public String getPs() {
556 public int getMaxBufferSize() {
557 return config.maxBufferSize;
560 public int getRows() {
564 public void setGreeting(final String greeting) {
565 config.greeting = greeting;
566 client.call("setGreeting", greeting);
569 public void setPs(final String ps) {
570 config.ps = ps == null ? DEFAULT_PS : ps;
571 client.call("setPs", config.ps);
574 public void setMaxBufferSize(final int lines) {
575 config.maxBufferSize = lines > 0 ? lines : 0;
576 client.call("setMaxBufferSize", config.maxBufferSize);
579 public void setRows(final int rows) {
581 if (config.rows < 1) {
584 if (config.rows > MAX_ROWS) {
585 config.rows = MAX_ROWS;
587 client.call("setRows", rows);
590 public int getCols() {
594 public void setCols(final int cols) {
596 if (config.cols < 1) {
599 if (config.cols > MAX_COLS) {
600 config.cols = MAX_COLS;
602 client.call("setCols", config.cols);
605 public void prompt() {
606 client.call("prompt");
609 public void prompt(final String initialInput) {
610 client.call("prompt", initialInput);
613 public void println(final String string) {
614 if(isConvertANSIToCSS){
615 client.call("print", "");
616 appendWithProcessingANSICodes(string + "\n");
618 client.call("println", string);
622 * Print text with predefined in theme CSS class.
625 * @param className CSS class name for string
627 public void println(final String string, final String className) {
628 client.call("println", string, className);
632 * @param string text to append to the last printed line
633 * @return this Console object
635 public Console append(final String string) {
636 if(isConvertANSIToCSS)
637 appendWithProcessingANSICodes(string);
639 client.call("append", string);
643 private void appendWithProcessingANSICodes(String sOutput) {
644 String splitted[] = sOutput.split(ANSICodeConverter.ANSI_PATTERN);
645 String notPrintedYet = new String(sOutput);
646 for(int i = 0; i < splitted.length; i++){
647 String nextStr = splitted[i];
648 if(i == 0 && nextStr.length() == 0)
650 String cssClasses = "";
651 Pattern firstAnsi = Pattern.compile("^(" + ANSICodeConverter.ANSI_PATTERN + ")+\\Q" + nextStr + "\\E.*", Pattern.DOTALL);
652 if(firstAnsi.matcher(notPrintedYet).matches()){
653 while(firstAnsi.matcher(notPrintedYet).matches()){
654 String ansi = notPrintedYet.replaceAll("\\Q" + notPrintedYet.replaceAll("^(" + ANSICodeConverter.ANSI_PATTERN + "){1}", "") + "\\E", "");
655 cssClasses += ansiToCSSconverter.convertANSIToCSS(ansi) + " ";
656 notPrintedYet = notPrintedYet.replaceAll("^(" + ANSICodeConverter.ANSI_PATTERN + "){1}", "");
658 notPrintedYet = notPrintedYet.replaceAll("^\\Q" + nextStr + "\\E", "");
660 notPrintedYet = notPrintedYet.replaceFirst("\\Q" + nextStr + "\\E", "");
661 cssClasses = cssClasses.trim();
662 if(cssClasses.length() > 0)
663 client.call("append", nextStr, cssClasses);
665 client.call("append", nextStr);
670 * Append text with predefined in theme CSS class.
672 * @param string text to append to the last printed line
673 * @param className CSS class name for string
674 * @return this Console object
676 public Console append(final String string, final String className) {
677 client.call("append", string, className);
681 public void newLine() {
682 client.call("newLine");
686 * Print new line only if new line not exists at the end of console
688 public void newLineIfNotEndsWithNewLine() {
689 client.call("newLineIfNotEndsWithNewLine");
692 public void reset() {
693 client.call("reset");
696 public void clear() {
700 public void formFeed() {
704 public void carriageReturn() {
708 public void lineFeed() {
712 public void clearCommandHistory() {
713 client.call("clearHistory");
716 public void clearBuffer() {
717 client.call("clearBuffer");
720 public void scrollToEnd() {
721 client.call("scrollToEnd");
725 * Focus input element of console.
727 public void focusInput() {
728 client.call("focusInput");
732 * Gets the Tabulator index of this Focusable component.
734 * @see com.vaadin.ui.Component.Focusable#getTabIndex()
736 public int getTabIndex() {
741 * Sets the Tabulator index of this Focusable component.
743 * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
745 public void setTabIndex(final int tabIndex) {
746 this.tabIndex = tabIndex;
753 public void focus() {
757 /* PrintStream implementation for console output. */
759 public PrintStream getPrintStream() {
760 if (printStream == null) {
761 printStream = new PrintStream(new OutputStream() {
763 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
766 public void write(final int b) throws IOException {
775 public void flush() throws IOException {
778 Console.this.print(buffer.toString());
786 /* Generic command handling */
789 * Add a Command to this Console.
791 * This will override the any commands of the same name available via
792 * {@link CommandProvider}.
794 public void addCommand(final String name, final Command cmd) {
795 commands.put(name, cmd);
799 * Remove a command from this console.
801 * This does not remove Command available from {@link CommandProvider}.
805 public void removeCommand(final String cmdName) {
806 commands.remove(cmdName);
810 * Get a Command by its name.
815 public Command getCommand(final String cmdName) {
817 // Try directly registered command first
818 Command cmd = commands.get(cmdName);
823 // Ask from the providers
824 if (commandProviders != null) {
825 for (final CommandProvider cp : commandProviders) {
826 cmd = cp.getCommand(this, cmdName);
838 * Get the current Console Handler.
842 public Handler getHandler() {
847 * Set the handler for this console.
852 public void setHandler(final Handler handler) {
853 this.handler = handler != null ? handler : new DefaultConsoleHandler();
856 public ANSICodeConverter getANSIToCSSConverter() {
857 return ansiToCSSconverter;
860 public void setANSIToCSSConverter(ANSICodeConverter converter) {
861 this.ansiToCSSconverter = converter != null? converter : new DefaultANSICodeConverter();
865 * Converting raw output with ANSI escape sequences to output with CSS-classes.
869 public boolean isConvertANSIToCSS() {
870 return isConvertANSIToCSS;
874 * Converting raw output with ANSI escape sequences to output with CSS-classes.
876 * @param isConvertANSIToCSS
878 public void setConvertANSIToCSS(boolean isConvertANSIToCSS) {
879 this.isConvertANSIToCSS = isConvertANSIToCSS;
883 * Get map of available commands in this Console.
887 public Set<String> getCommands() {
888 final Set<String> res = new HashSet<String>();
889 if (commandProviders != null) {
890 for (final CommandProvider cp : commandProviders) {
891 if(cp.getAvailableCommands(this) != null)
892 res.addAll(cp.getAvailableCommands(this));
895 res.addAll(commands.keySet());
896 return Collections.unmodifiableSet(res);