Package org.springframework.shell.core

Source Code of org.springframework.shell.core.JLineShell$FlashInfo

/*
* Copyright 2011-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.core;

import static org.fusesource.jansi.Ansi.ansi;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import jline.WindowsTerminal;
import jline.console.ConsoleReader;
import jline.console.history.History;
import jline.console.history.MemoryHistory;

import org.apache.commons.io.input.ReversedLinesFileReader;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Attribute;
import org.fusesource.jansi.Ansi.Color;
import org.fusesource.jansi.Ansi.Erase;
import org.fusesource.jansi.AnsiConsole;
import org.springframework.shell.event.ShellStatus;
import org.springframework.shell.event.ShellStatus.Status;
import org.springframework.shell.event.ShellStatusListener;
import org.springframework.shell.support.util.IOUtils;
import org.springframework.shell.support.util.OsUtils;
import org.springframework.shell.support.util.VersionUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
* Uses the feature-rich <a href="http://sourceforge.net/projects/jline/">JLine</a> library to provide an interactive
* shell.
*
* <p>
* Due to Windows' lack of color ANSI services out-of-the-box, this implementation automatically detects the classpath
* presence of <a href="http://jansi.fusesource.org/">Jansi</a> and uses it if present. This library is not necessary
* for *nix machines, which support colour ANSI without any special effort. This implementation has been written to use
* reflection in order to avoid hard dependencies on Jansi.
*
* @author Ben Alex
* @author Jarred Li
* @author Glenn Renfro
* @since 1.0
*/
public abstract class JLineShell extends AbstractShell implements Shell, Runnable {

  // Constants
  private static final String ANSI_CONSOLE_CLASSNAME = "org.fusesource.jansi.AnsiConsole";

  private static final boolean JANSI_AVAILABLE = ClassUtils.isPresent(ANSI_CONSOLE_CLASSNAME,
      JLineShell.class.getClassLoader());

  private static final char ESCAPE = 27;

  private static final String BEL = "\007";

  // Fields
  protected ConsoleReader reader;

  private boolean developmentMode = false;

  private FileWriter fileLog;

  private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  protected ShellStatusListener statusListener; // ROO-836

  /** key: slot name, value: flashInfo instance */
  private final Map<String, FlashInfo> flashInfoMap = new HashMap<String, FlashInfo>();

  /** key: row number, value: eraseLineFromPosition */
  private final Map<Integer, Integer> rowErasureMap = new HashMap<Integer, Integer>();

  private boolean shutdownHookFired = false; // ROO-1599

  private int historySize;

  public void run() {
    reader = createConsoleReader();

    setPromptPath(null);

    JLineLogHandler handler = new JLineLogHandler(reader, this);
    JLineLogHandler.prohibitRedraw(); // Affects this thread only
    Logger mainLogger = Logger.getLogger("");
    removeHandlers(mainLogger);
    mainLogger.addHandler(handler);

    reader.addCompleter(new ParserCompleter(getParser()));

    reader.setBellEnabled(true);
    if (Boolean.getBoolean("jline.nobell")) {
      reader.setBellEnabled(false);
    }

    // reader.setDebug(new PrintWriter(new FileWriter("writer.debug", true)));

    openFileLogIfPossible();
    History history = this.reader.getHistory();
    if (history instanceof MemoryHistory) {
      ((MemoryHistory) history).setMaxSize(getHistorySize());
    }
    // Try to build previous command history from the project's log
    String[] filteredLogEntries = filterLogEntry();
    for (String logEntry : filteredLogEntries) {
      reader.getHistory().add(logEntry);
    }

    flashMessageRenderer();
    flash(Level.FINE, this.getProductName() + " " + this.getVersion(), Shell.WINDOW_TITLE_SLOT);
    printBannerAndWelcome();

    String startupNotifications = getStartupNotifications();
    if (StringUtils.hasText(startupNotifications)) {
      logger.info(startupNotifications);
    }

    setShellStatus(Status.STARTED);

    try {
      // Monitor CTRL+C initiated shutdowns (ROO-1599)
      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        public void run() {
          shutdownHookFired = true;
        }
      }, getProductName() + " JLine Shutdown Hook"));
    }
    catch (Throwable t) {
    }

    // Handle any "execute-then-quit" operation

    String rooArgs = System.getProperty("roo.args");
    if (rooArgs != null && !"".equals(rooArgs)) {
      setShellStatus(Status.USER_INPUT);
      boolean success = executeCommand(rooArgs).isSuccess();
      if (exitShellRequest == null) {
        // The command itself did not specify an exit shell code, so we'll fall back to something sensible here
        executeCommand("quit"); // ROO-839
        exitShellRequest = success ? ExitShellRequest.NORMAL_EXIT : ExitShellRequest.FATAL_EXIT;
      }
      setShellStatus(Status.SHUTTING_DOWN);
    }
    else {
      // Normal RPEL processing
      promptLoop();
    }

  }

  /**
   * read history commands from history log. the history size if determined by --histsize options.
   *
   * @return history commands
   */
  private String[] filterLogEntry() {
    ArrayList<String> entries = new ArrayList<String>();
    ReversedLinesFileReader reversedReader = null;
    try {
      reversedReader = new ReversedLinesFileReader(new File(getHistoryFileName()), 4096, Charset.forName("UTF-8"));
      int size = 0;
      String line = null;
      while ((line = reversedReader.readLine()) != null) {
        if (!line.startsWith("//")) {
          size++;
          if (size > historySize) {
            break;
          }
          else {
            entries.add(line);
          }
        }
      }
    }
    catch (IOException e) {
      logger.warning("read history file failed. Reason:" + e.getMessage());
    }
    finally {
      closeReversedReader(reversedReader);
    }
    Collections.reverse(entries);
    return entries.toArray(new String[0]);
  }

  private void closeReversedReader(ReversedLinesFileReader reversedReader) {
    if (reversedReader != null) {
      try {
        reversedReader.close();
      }
      catch (IOException ex) {
        logger.warning("Cloud not close ReversedLinesFileReader: " + ex);
      }
    }

  }

  /**
   * Creates new jline ConsoleReader. On Windows if jansi is available, uses createAnsiWindowsReader(). Otherwise,
   * always creates a default ConsoleReader. Sub-classes of this class can plug in their version of ConsoleReader by
   * overriding this method, if required.
   *
   * @return a jline ConsoleReader instance
   */
  protected ConsoleReader createConsoleReader() {
    ConsoleReader consoleReader = null;
    try {
      if (isJansiAvailable()) {
        try {
          consoleReader = createAnsiWindowsReader();
        }
        catch (Exception e) {
          // Try again using default ConsoleReader constructor
          logger.warning("Can't initialize jansi AnsiConsole, falling back to default: " + e);
        }
      }
      if (consoleReader == null) {
        consoleReader = new ConsoleReader();
      }
    }
    catch (IOException ioe) {
      throw new IllegalStateException("Cannot start console class", ioe);
    }
    consoleReader.setExpandEvents(false);
    return consoleReader;
  }

  private boolean isJansiAvailable() {
    return JANSI_AVAILABLE && OsUtils.isWindows() && System.getProperty("jline.terminal") == null;
  }

  public void printBannerAndWelcome() {
  }

  public String getStartupNotifications() {
    return null;
  }

  private void removeHandlers(final Logger l) {
    Handler[] handlers = l.getHandlers();
    if (handlers != null && handlers.length > 0) {
      for (Handler h : handlers) {
        l.removeHandler(h);
      }
    }
  }

  @Override
  public void setPromptPath(final String path) {
    setPromptPath(path, false);
  }

  @Override
  public void setPromptPath(final String path, final boolean overrideStyle) {
    if (reader.getTerminal().isAnsiSupported()) {
      // ANSIBuffer ansi = JLineLogHandler.getANSIBuffer();
      Ansi ansi = ansi();
      if (path == null || "".equals(path)) {
        shellPrompt = ansi.fg(Color.YELLOW).a(getPromptText()).reset().toString();
      }
      else {
        if (overrideStyle) {
          ansi.a(path);
        }
        else {
          ansi.fg(Color.CYAN).a(path).reset();
        }
        shellPrompt = ansi.fg(Color.YELLOW).a(" " + getPromptText()).toString();
      }
    }
    else {
      // The superclass will do for this non-ANSI terminal
      super.setPromptPath(path);
    }

    // The shellPrompt is now correct; let's ensure it now gets used
    reader.setPrompt(AbstractShell.shellPrompt);
  }

  protected ConsoleReader createAnsiWindowsReader() throws Exception {
    // Get decorated OutputStream that parses ANSI-codes
    final PrintStream ansiOut = (PrintStream) ClassUtils
        .forName(ANSI_CONSOLE_CLASSNAME, JLineShell.class.getClassLoader()).getMethod("out").invoke(null);
    WindowsTerminal ansiTerminal = new WindowsTerminal() {
      @Override
      public synchronized boolean isAnsiSupported() {
        return true;
      }
    };
    ansiTerminal.init();
    // Make sure to reset the original shell's colors on shutdown by closing the stream
    statusListener = new ShellStatusListener() {
      public void onShellStatusChange(final ShellStatus oldStatus, final ShellStatus newStatus) {
        if (newStatus.getStatus().equals(Status.SHUTTING_DOWN)) {
          ansiOut.close();
        }
      }
    };
    addShellStatusListener(statusListener);

    // return new ConsoleReader(new FileInputStream(FileDescriptor.in), new PrintWriter(new OutputStreamWriter(
    // ansiOut,
    // // Default to Cp850 encoding for Windows console output (ROO-439)
    // System.getProperty("jline.WindowsTerminal.output.encoding", "Cp850"))), null, ansiTerminal);

    OutputStream out = AnsiConsole.wrapOutputStream(ansiOut);
    return new ConsoleReader(new FileInputStream(FileDescriptor.in), out, ansiTerminal);
  }

  private void flashMessageRenderer() {
    if (!reader.getTerminal().isAnsiSupported()) {
      return;
    }
    // Setup a thread to ensure flash messages are displayed and cleared correctly
    Thread t = new Thread(new Runnable() {
      public void run() {
        while (!shellStatus.getStatus().equals(Status.SHUTTING_DOWN) && !shutdownHookFired) {
          synchronized (flashInfoMap) {
            long now = System.currentTimeMillis();

            Set<String> toRemove = new HashSet<String>();
            for (String slot : flashInfoMap.keySet()) {
              FlashInfo flashInfo = flashInfoMap.get(slot);

              if (flashInfo.flashMessageUntil < now) {
                // Message has expired, so clear it
                toRemove.add(slot);
                doAnsiFlash(flashInfo.rowNumber, Level.ALL, "");
              }
              else {
                // The expiration time for this message has not been reached, so preserve it
                doAnsiFlash(flashInfo.rowNumber, flashInfo.flashLevel, flashInfo.flashMessage);
              }
            }
            for (String slot : toRemove) {
              flashInfoMap.remove(slot);
            }
          }
          try {
            Thread.sleep(200);
          }
          catch (InterruptedException ignore) {
          }
        }
      }
    }, getProductName() + " JLine Flash Message Manager");
    t.start();
  }

  @Override
  public void flash(final Level level, final String message, final String slot) {
    Assert.notNull(level, "Level is required for a flash message");
    Assert.notNull(message, "Message is required for a flash message");
    Assert.hasText(slot, "Slot name must be specified for a flash message");

    if (Shell.WINDOW_TITLE_SLOT.equals(slot)) {
      if (reader != null && reader.getTerminal().isAnsiSupported()) {
        // We can probably update the window title, as requested
        if (!StringUtils.hasText(message)) {
          System.out.println("No text");
        }

        Ansi ansi = ansi();
        ansi.a(ESCAPE + "]0;").a(message).a(BEL);
        try {
          reader.print(ansi.toString());
          reader.flush();
        }
        catch (IOException ignored) {
        }
      }

      return;
    }
    if ((reader != null && !reader.getTerminal().isAnsiSupported())) {
      super.flash(level, message, slot);
      return;
    }
    synchronized (flashInfoMap) {
      FlashInfo flashInfo = flashInfoMap.get(slot);

      if ("".equals(message)) {
        // Request to clear the message, but give the user some time to read it first
        if (flashInfo == null) {
          // We didn't have a record of displaying it in the first place, so just quit
          return;
        }
        flashInfo.flashMessageUntil = System.currentTimeMillis() + 1500;
      }
      else {
        // Display this message displayed until further notice
        if (flashInfo == null) {
          // Find a row for this new slot; we basically take the first line number we discover
          flashInfo = new FlashInfo();
          flashInfo.rowNumber = Integer.MAX_VALUE;
          outer: for (int i = 1; i < Integer.MAX_VALUE; i++) {
            for (FlashInfo existingFlashInfo : flashInfoMap.values()) {
              if (existingFlashInfo.rowNumber == i) {
                // Veto, let's try the new candidate row number
                continue outer;
              }
            }
            // If we got to here, nobody owns this row number, so use it
            flashInfo.rowNumber = i;
            break outer;
          }

          // Store it
          flashInfoMap.put(slot, flashInfo);
        }
        // Populate the instance with the latest data
        flashInfo.flashMessageUntil = Long.MAX_VALUE;
        flashInfo.flashLevel = level;
        flashInfo.flashMessage = message;

        // Display right now
        doAnsiFlash(flashInfo.rowNumber, flashInfo.flashLevel, flashInfo.flashMessage);
      }
    }
  }

  // Externally synchronized via the two calling methods having a mutex on flashInfoMap
  private void doAnsiFlash(final int row, final Level level, final String message) {
    Ansi ansi = ansi();
    if (isAppleTerminal()) {
      ansi.a(ESCAPE + "7");
    }
    else {
      ansi.saveCursorPosition();
    }

    // Figure out the longest line we're presently displaying (or were) and erase the line from that position
    int mostFurtherLeftColNumber = Integer.MAX_VALUE;
    for (Integer candidate : rowErasureMap.values()) {
      if (candidate < mostFurtherLeftColNumber) {
        mostFurtherLeftColNumber = candidate;
      }
    }

    if (mostFurtherLeftColNumber == Integer.MAX_VALUE) {
      // There is nothing to erase
    }
    else {
      ansi.cursor(row, mostFurtherLeftColNumber);
      ansi.eraseLine(Erase.FORWARD); // Clear what was present on the line
    }

    if (("".equals(message))) {
      // They want the line blank; we've already achieved this if needed via the erasing above
      // Just need to record we no longer care about this line the next time doAnsiFlash is invoked
      rowErasureMap.remove(row);
    }
    else {
      if (shutdownHookFired) {
        return; // ROO-1599
      }
      // They want some message displayed
      int startFrom = reader.getTerminal().getWidth() - message.length() + 1;
      if (startFrom < 1) {
        startFrom = 1;
      }
      ansi.cursor(row, startFrom);
      ansi.a(Attribute.NEGATIVE_ON).a(message).a(Attribute.NEGATIVE_OFF);
      // Record we want to erase from this positioning next time (so we clean up after ourselves)
      rowErasureMap.put(row, startFrom);
    }
    if (isAppleTerminal()) {
      ansi.a(ESCAPE + "8");
    }
    else {
      ansi.reset();
    }

    try {
      reader.print(ansi.toString());
      reader.flush();
    }
    catch (IOException ignored) {
    }
  }
  /**
   * Awaits user input, executes the command and displays the prompt to the user.
   */
  public void promptLoop() {
    setShellStatus(Status.USER_INPUT);
    String line;
    String prompt = getPromptText();

    try {
      while (exitShellRequest == null && (reader != null && ((line = reader.readLine()) != null))) {
        JLineLogHandler.resetMessageTracking();
        setShellStatus(Status.USER_INPUT);

        if (!StringUtils.hasText(line)) {
          //generate prompt if empty line, the prompt maybe showing the time or something else that updates
          //independent of the lack of a command to execute.
          prompt = generatePromptUpdate(prompt);
          continue;
        }

        executeCommand(line);
        //update the prompt after the command has been executed in case an application event listener in the
        //command changes state in the prompt provider.
        prompt = generatePromptUpdate(prompt);

      }
    }
    catch (IOException ioe) {
      throw new IllegalStateException("Shell line reading failure", ioe);
    }
    setShellStatus(Status.SHUTTING_DOWN);
  }
 
  /**
   * Retrieves the latest prompt and if the latest prompt is different than the existing prompt,
   * the shellPrompt is updated.
   * @param existingPrompt The prompt that is recognized as the current prompt.
   * @return The prompt that the shellPrompt displays.
   */
  public String generatePromptUpdate(String existingPrompt) {
    String newPrompt = getPromptText();
    if (!ObjectUtils.nullSafeEquals(existingPrompt, newPrompt)) {
      setPromptPath(null);
    }
    return newPrompt;
  }
 
  public void setDevelopmentMode(final boolean developmentMode) {
    JLineLogHandler.setIncludeThreadName(developmentMode);
    JLineLogHandler.setSuppressDuplicateMessages(!developmentMode); // We want to see duplicate messages during
                                    // development time (ROO-1873)
    this.developmentMode = developmentMode;
  }

  public boolean isDevelopmentMode() {
    return this.developmentMode;
  }

  private void openFileLogIfPossible() {
    try {
      fileLog = new FileWriter(getHistoryFileName(), true);
      // First write, so let's record the date and time of the first user command
      fileLog.write("// " + getProductName() + " " + versionInfo() + " log opened at " + df.format(new Date())
          + "\n");
      fileLog.flush();
    }
    catch (IOException ignoreIt) {
    }
  }

  @Override
  protected void logCommandToOutput(final String processedLine) {
    if (fileLog == null) {
      openFileLogIfPossible();
      if (fileLog == null) {
        // Still failing, so give up
        return;
      }
    }
    try {
      fileLog.write(processedLine + "\n"); // Unix line endings only from Roo
      fileLog.flush(); // So tail -f will show it's working
      if (getExitShellRequest() != null) {
        // Shutting down, so close our file (we can always reopen it later if needed)
        fileLog.write("// " + getProductName() + " " + versionInfo() + " log closed at "
            + df.format(new Date()) + "\n");
        IOUtils.closeQuietly(fileLog);
        fileLog = null;
      }
    }
    catch (IOException ignoreIt) {
    }
  }

  /**
   * Obtains the "roo.home" from the system property, falling back to the current working directory if missing.
   *
   * @return the 'roo.home' system property
   */
  @Override
  protected String getHomeAsString() {
    String rooHome = System.getProperty("roo.home");
    if (rooHome == null) {
      try {
        rooHome = new File(".").getCanonicalPath();
      }
      catch (Exception e) {
        throw new IllegalStateException(e);
      }
    }
    return rooHome;
  }

  /**
   * Should be called by a subclass before deactivating the shell.
   */
  protected void closeShell() {
    // Notify we're closing down (normally our status is already shutting_down, but if it was a CTRL+C via the
    // o.s.r.bootstrap.Main hook)
    setShellStatus(Status.SHUTTING_DOWN);
    if (statusListener != null) {
      removeShellStatusListener(statusListener);
    }
  }

  private static class FlashInfo {
    String flashMessage;

    long flashMessageUntil;

    Level flashLevel;

    int rowNumber;
  }

  /**
   * get history file name from provider. The provider has highest order
   * <link>org.springframework.core.Ordered.getOder</link> will win.
   *
   * @return history file name
   */
  abstract protected String getHistoryFileName();

  /**
   * get prompt text from provider. The provider has highest order
   * <link>org.springframework.core.Ordered.getOder</link> will win.
   *
   * @return prompt text
   */
  abstract protected String getPromptText();

  /**
   * get product name
   *
   * @return Product Name
   */
  abstract protected String getProductName();

  /**
   * get version information
   *
   * @return Version
   */
  protected String getVersion() {
    return VersionUtils.versionInfo();
  }

  /**
   * @return the historySize
   */
  public int getHistorySize() {
    return historySize;
  }

  /**
   * @param historySize the historySize to set
   */
  public void setHistorySize(int historySize) {
    this.historySize = historySize;
  }

  private static boolean isAppleTerminal() {
    final String terminalName = System.getenv("TERM_PROGRAM");
    return ("Apple_Terminal".equalsIgnoreCase(terminalName) || Boolean.getBoolean("is.apple.terminal"));
  }

}
TOP

Related Classes of org.springframework.shell.core.JLineShell$FlashInfo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.