AbstractShell.java

/*******************************************************************************
 * Copyhacked (H) 2012-2020.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss about it
 * every time possible with every body.
 * 
 * Contributors:
 *      ron190 at ymail dot com - initial implementation
 ******************************************************************************/
package com.jsql.view.swing.shell;

import com.jsql.util.LogLevelUtil;
import com.jsql.view.swing.scrollpane.LightScrollPane;
import com.jsql.view.swing.util.UiUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.*;
import java.awt.event.MouseMotionListener;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.UUID;

/**
 * A Terminal completely built from swing text pane.
 */
public abstract class AbstractShell extends JTextPane {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    /**
     * True if terminal is processing command.
     */
    private final boolean[] isEdited = {false};

    /**
     * Server name or IP to display on prompt.
     */
    private final String host;

    /**
     * User and password for database.
     */
    protected String[] loginPassword = null;

    private final UUID uuidShell;
    
    private final String urlShell;

    /**
     * Document used to append colored text.
     */
    private final transient StyledDocument styledDocument = this.getStyledDocument();

    /**
     * Style used for coloring text.
     */
    private final transient Style style = this.addStyle("Necrophagist's next album is 2014.", null);

    /**
     * Length of prompt.
     */
    private String prompt = StringUtils.EMPTY;

    /**
     * Text to display next caret.
     */
    private final String labelShell;
    
    /**
     * Build a shell instance.
     * @param uuidShell Unique identifier to discriminate beyond multiple opened terminals
     * @param urlShell URL of current shell
     * @param labelShell Type of shell to display on prompt
     * @throws MalformedURLException
     */
    protected AbstractShell(UUID uuidShell, String urlShell, String labelShell) throws MalformedURLException, URISyntaxException {
        
        this.uuidShell = uuidShell;
        this.urlShell = urlShell;
        this.labelShell = labelShell;

        var url = new URI(urlShell).toURL();
        this.host = url.getHost();

        this.setFont(new Font(UiUtil.FONT_NAME_MONO_NON_ASIAN, Font.PLAIN, ((Font) UIManager.get("TextPane.font")).getSize()));
        this.setCaret(new BlockCaret());
        this.setBackground(Color.BLACK);
        this.setForeground(Color.LIGHT_GRAY);
        this.setBorder(BorderFactory.createEmptyBorder(0, 0, LightScrollPane.THUMB_SIZE, 0));

        this.displayPrompt(true);

        this.setCursor(null);
        this.setTransferHandler(null);
        this.setHighlighter(null);

        this.addMouseListener(new EmptyFocus(this));
        this.addKeyListener(new KeyAdapterTerminal(this));
    }

    /**
     * Run when cmd is validated.
     * @param cmd Command to execute
     * @param terminalID Unique ID for terminal instance
     * @param wbhPath URL of shell
     * @param arg Additional parameters (User and password for SQLShell)
     */
    public abstract void action(String cmd, UUID terminalID, String wbhPath, String... arg);
    
    /**
     * Update terminal and use default behavior.
     */
    public void reset() {
        
        this.isEdited[0] = false;
        this.setEditable(true);
        this.displayPrompt(false);
        this.setCaretPosition(this.getDocument().getLength());
        this.setCursor(null);
    }

    /**
     * Add a text at the end of textpane.
     * @param string Text to add
     */
    public void append(String string) {
        try {
            var doc = this.getDocument();
            doc.insertString(doc.getLength(), string, null);
            
        } catch (BadLocationException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }
    
    /**
     * Append prompt to textpane, measure prompt the first time is used.
     * @param isAddingPrompt Should we measure prompt length?
     */
    public void displayPrompt(boolean isAddingPrompt) {
        
        StyleConstants.setUnderline(this.style, true);
        this.appendPrompt("jsql", Color.LIGHT_GRAY, isAddingPrompt);
        StyleConstants.setUnderline(this.style, false);

        this.appendPrompt(StringUtils.SPACE + this.labelShell, Color.LIGHT_GRAY, isAddingPrompt);
        this.appendPrompt("[", new Color(50, 191, 50), isAddingPrompt);
        this.appendPrompt(this.host, new Color(191, 191, 25), isAddingPrompt);
        this.appendPrompt("]", new Color(50, 191, 50), isAddingPrompt);
        this.appendPrompt(" >", new Color(191, 100, 100), isAddingPrompt);
        this.appendPrompt(StringUtils.SPACE, Color.LIGHT_GRAY, isAddingPrompt);
    }

    /**
     * Add a colored string to the textpane, measure prompt at the same time.
     * @param string Text to append
     * @param color Color of text
     * @param isAddingPrompt Should we measure prompt length?
     */
    private void appendPrompt(String string, Color color, boolean isAddingPrompt) {
        try {
            StyleConstants.setForeground(this.style, color);
            this.styledDocument.insertString(this.styledDocument.getLength(), string, this.style);
            
            if (isAddingPrompt) {
                this.prompt += string;
            }
        } catch (BadLocationException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }

    /**
     * NoWrap.
     */
    @Override
    public boolean getScrollableTracksViewportWidth() {
        return this.getUI().getPreferredSize(this).width <= this.getParent().getSize().width;
    }

    /**
     * Cancel every mouse movement processing like drag/drop.
     */
    @Override
    public synchronized void addMouseMotionListener(MouseMotionListener l) {
        // Do nothing
    }

    /**
     * Get index of line for current offset (generally cursor position).
     * @param offset Position on the line
     * @return Index of the line
     * @throws BadLocationException
     */
    public int getLineOfOffset(int offset) throws BadLocationException {
        
        var errorMsg = "Can't translate offset to line";
        var doc = this.getDocument();
        
        if (offset < 0) {
            throw new BadLocationException(errorMsg, -1);
        } else if (offset > doc.getLength()) {
            throw new BadLocationException(errorMsg, doc.getLength() + 1);
        } else {
            
            var map = doc.getDefaultRootElement();
            return map.getElementIndex(offset);
        }
    }

    /**
     * Get position of the beginning of the line.
     * @param line Index of the line
     * @return Offset of line
     * @throws BadLocationException
     */
    public int getLineStartOffset(int line) throws BadLocationException {
        
        var map = this.getDocument().getDefaultRootElement();
        
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= map.getElementCount()) {
            throw new BadLocationException("No such line", this.getDocument().getLength() + 1);
        } else {
            
            var lineElem = map.getElement(line);
            return lineElem.getStartOffset();
        }
    }

    
    // Getter and setter
    
    public boolean[] getIsEdited() {
        return this.isEdited;
    }

    public UUID getUuidShell() {
        return this.uuidShell;
    }

    public String getUrlShell() {
        return this.urlShell;
    }

    public String getPrompt() {
        return this.prompt;
    }
}