@SuppressWarnings("WeakerAccess")
public class AppUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(AppUtils.class);
    private final static String KEYSTORE = "javax.net.ssl.keyStore";
    private final static String KEYSTORE_PASSWORD = "javax.net.ssl.keyStorePassword";
    private final static String TRUSTSTORE = "javax.net.ssl.trustStore";
    private final static String TRUSTSTORE_PASSWORD = "javax.net.ssl.trustStorePassword";
    private static char[] lastPassword = null;

    private AppUtils() {
        super();
    }

    public static String askForPassword(JFrame parent, Object message, String title, String submitText, String cancelText) {
        final JDialog dialog = new JDialog(parent, true);

        final JPasswordField field = new JPasswordField();
        field.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                    lastPassword = field.getPassword();
                    dialog.setVisible(false);
                }
            }
        });
        if (lastPassword != null)
            field.setText(StringUtils.trimToEmpty(String.valueOf(lastPassword)));

        final JPanel panel = new JPanel(new BorderLayout(5, 5));
        panel.setOpaque(false);
        panel.add(field, BorderLayout.SOUTH);

        if (message instanceof JComponent) {
            panel.add((JComponent) message, BorderLayout.NORTH);
        } else if (message instanceof String) {
            panel.add(new JLabel((String) message), BorderLayout.NORTH);
        }

        final JButton submitButton = new JButton();
        submitButton.setText(StringUtils.defaultIfBlank(submitText, "Submit"));
        submitButton.addActionListener(e -> {
            lastPassword = field.getPassword();
            dialog.setVisible(false);
        });

        final JButton cancelButton = new JButton();
        cancelButton.setText(StringUtils.defaultIfBlank(cancelText, "Cancel"));
        cancelButton.addActionListener(e -> {
            field.setText(StringUtils.EMPTY);
            dialog.setVisible(false);
        });

        final JPanel buttonBar = new JPanel(new FlowLayout(FlowLayout.CENTER));
        buttonBar.add(submitButton);
        buttonBar.add(cancelButton);
        buttonBar.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));

        dialog.setResizable(false);
        dialog.setTitle(title);
        dialog.setMinimumSize(new Dimension(300, 0));
        dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        dialog.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                field.setText(StringUtils.EMPTY);
                dialog.setVisible(false);
            }
        });
        dialog.getRootPane().setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        dialog.getRootPane().setLayout(new BorderLayout());
        dialog.getRootPane().add(panel, BorderLayout.NORTH);
        dialog.getRootPane().add(buttonBar, BorderLayout.SOUTH);

        dialog.pack();
        dialog.setLocationRelativeTo(parent);
        field.requestFocus();
        field.selectAll();
        dialog.setVisible(true);

        dialog.dispose();
        return StringUtils.trimToNull(String.valueOf(field.getPassword()));
    }

    public static Boolean askQuestion(Component parent, String message, String title) {
        JTextPane content = new JTextPane();
        content.setEditable(false);
        content.setOpaque(false);
        content.setText(message);

        int response = JOptionPane.showConfirmDialog(
                parent, content, title, JOptionPane.YES_NO_OPTION);

        if (response == JOptionPane.OK_OPTION)
            return true;
        if (response == JOptionPane.NO_OPTION)
            return false;
        return null;
    }

    public static void browse(String url) {
        if (StringUtils.isBlank(url)) return;
        try {
            browse(new URI(url));
        } catch (URISyntaxException ex) {
            LOGGER.warn("Can't parse URL '" + url + "'!", ex);
        }
    }

    public static void browse(URL url) {
        if (url == null) return;
        try {
            browse(url.toURI());
        } catch (URISyntaxException ex) {
            LOGGER.error("Can't convert URL '" + url.toString() + "'!", ex);
        }
    }

    public static void browse(URI url) {
        if (url == null) return;
        if (Desktop.isDesktopSupported()) {
            Desktop desktop = Desktop.getDesktop();
            if (desktop.isSupported(Desktop.Action.BROWSE)) {
                try {
                    desktop.browse(url);
                    return;
                } catch (Exception ex) {
                    LOGGER.error("Can't open the default web browser!", ex);
                }
            }
        }

        try {
            if (SystemUtils.IS_OS_WINDOWS) {
                Runtime.getRuntime()
                        .exec("rundll32 url.dll,FileProtocolHandler " + url)
                        .waitFor();
            } else if (SystemUtils.IS_OS_MAC_OSX) {
                Runtime.getRuntime()
                        .exec(new String[]{"open", url.toString()})
                        .waitFor();
            } else if (SystemUtils.IS_OS_UNIX) {
                Runtime.getRuntime()
                        .exec(new String[]{"xdg-open", url.toString()})
                        .waitFor();
            } else {
                throw new Exception("No browser found for this operating system (" + SystemUtils.OS_NAME + ")!");
            }
        } catch (Exception ex) {
            LOGGER.error("Can't execute a browser command!", ex);
        }
    }

    public static String getDefaultSshKey() {
        File idRsa = new File(new File(SystemUtils.getUserHome(), ".ssh"), "id_rsa");
        if (idRsa.isFile())
            return idRsa.getAbsolutePath();

        File idDsa = new File(new File(SystemUtils.getUserHome(), ".ssh"), "id_dsa");
        if (idDsa.isFile())
            return idDsa.getAbsolutePath();

        return null;
    }

    public static String getHumanReadableByteCount(long size) {
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        numberFormat.setMinimumFractionDigits(0);
        numberFormat.setMaximumFractionDigits(2);
        return getHumanReadableByteCount(size, numberFormat);
    }

    public static String getHumanReadableByteCount(long size, NumberFormat numberFormat) {
        if (size / FileUtils.ONE_GB > 0) {
            double value = (double) size / (double) FileUtils.ONE_GB;
            return numberFormat.format(value) + " GB";
        } else if (size / FileUtils.ONE_MB > 0) {
            double value = (double) size / (double) FileUtils.ONE_MB;
            return numberFormat.format(value) + " MB";
        } else if (size / FileUtils.ONE_KB > 0) {
            double value = (double) size / (double) FileUtils.ONE_KB;
            return numberFormat.format(value) + " KB";
        }
        return numberFormat.format(size) + " bytes";
    }

    public static File getKeystore() {
        String keystore = System.getProperty(KEYSTORE);
        return (StringUtils.isNotBlank(keystore)) ?
                new File(keystore) : null;
    }

    public static String getKeystorePassword() {
        return StringUtils.trimToNull(System.getProperty(KEYSTORE_PASSWORD));
    }

    public static void initKeystore(File defaultKeystore, String defaultPassword) {
        System.setProperty(KEYSTORE, StringUtils.defaultIfBlank(
                System.getProperty(KEYSTORE), defaultKeystore.getAbsolutePath()));
        System.setProperty(KEYSTORE_PASSWORD, StringUtils.defaultIfBlank(
                System.getProperty(KEYSTORE_PASSWORD), defaultPassword));
    }

    public static void initTruststore(File defaultTruststore, String defaultPassword) {
        System.setProperty(TRUSTSTORE, StringUtils.defaultIfBlank(
                System.getProperty(TRUSTSTORE), defaultTruststore.getAbsolutePath()));
        System.setProperty(TRUSTSTORE_PASSWORD, StringUtils.defaultIfBlank(
                System.getProperty(TRUSTSTORE_PASSWORD), defaultPassword));
    }

    public static URL resource(String file) {
        return AppUtils.class.getResource("resources/" + file);
    }

    public static void showError(Component parent, String message, String title) {
        showMessage(parent, message, title, JOptionPane.ERROR_MESSAGE);
    }

    public static void showInformation(Component parent, String message, String title) {
        showMessage(parent, message, title, JOptionPane.INFORMATION_MESSAGE);
    }

    public static void showMessage(Component parent, String message, String title, int type) {
        JTextPane content = new JTextPane();
        content.setEditable(false);
        content.setOpaque(false);
        content.setText(message);
        JOptionPane.showMessageDialog(parent, content, title, type);
    }
} @SuppressWarnings("WeakerAccess")
public class ImageUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(ImageUtils.class);

    private ImageUtils() {
        super();
    }

    public static BufferedImage crop(BufferedImage image, int x, int y, int width, int height) {
        return Scalr.crop(image, x, y, width, height);
    }

    public static boolean equals(BufferedImage image1, BufferedImage image2) {
        final int image1Width = image1.getWidth();
        final int image1Height = image1.getHeight();

        final int image2Width = image2.getWidth();
        final int image2Height = image2.getHeight();

        if (image1Width != image2Width || image1Height != image2Height)
            return false;

        final DataBuffer image1Buffer = image1.getData().getDataBuffer();
        final DataBuffer image2Buffer = image2.getData().getDataBuffer();

        final int image1BufferSize = image1Buffer.getSize();
        final int image2BufferSize = image2Buffer.getSize();
        if (image1BufferSize != image2BufferSize)
            return false;

        if (image1Buffer instanceof DataBufferInt && image2Buffer instanceof DataBufferInt) {
            // compare according to https://stackoverflow.com/a/11006984
            final DataBufferInt image1BufferInt = (DataBufferInt) image1Buffer;
            final DataBufferInt image2BufferInt = (DataBufferInt) image2Buffer;
            if (image1BufferInt.getNumBanks() != image2BufferInt.getNumBanks())
                return false;

            for (int bank = 0; bank < image1BufferInt.getNumBanks(); bank++) {
                int[] actual = image1BufferInt.getData(bank);
                int[] expected = image2BufferInt.getData(bank);
                if (!Arrays.equals(actual, expected))
                    return false;
            }
        } else {
            // compare according to https://stackoverflow.com/a/51497360
            for (int i = 0; i < image1BufferSize; i++) {
                if (image1Buffer.getElem(i) != image2Buffer.getElem(i)) {
                    return false;
                }
            }
        }

        //for (int x = 0; x < image1Width; x++) {
        //    for (int y = 0; y < image1Height; y++) {
        //        if (image1.getRGB(x, y) != image2.getRGB(x, y)) {
        //            return false;
        //        }
        //    }
        //}

        return true;
    }

    public static BufferedImage[] getSlices(BufferedImage image, int sliceWidth, int sliceHeight) {
        final List<BufferedImage> slices = new ArrayList<>();
        final int imageWidth = image.getWidth();
        final int imageHeight = image.getHeight();

        for (int y = 0; y < imageHeight; y += sliceHeight) {
            for (int x = 0; x < imageWidth; x += sliceWidth) {

                int w = ((x + sliceWidth) < imageWidth) ?
                        sliceWidth : imageWidth - x;
                int h = ((y + sliceHeight) < imageHeight) ?
                        sliceHeight : imageHeight - y;

                BufferedImage slice = crop(image, x, y, w, h);
                slices.add(slice);
            }
        }

        return slices.toArray(new BufferedImage[0]);
    }

    public static ImageIcon loadIcon(URL image) {
        BufferedImage img = loadImage(image);
        return (img != null) ? new ImageIcon(img) : null;
    }

    public static BufferedImage loadImage(URL image) {
        try {
            return (image != null) ? ImageIO.read(image) : null;
        } catch (Exception ex) {
            LOGGER.error("Can't read image from '" + image + "'!", ex);
            return null;
        }
    }

    public static BufferedImage read(InputStream input) throws IOException {
        ImageReader reader = ImageIO.getImageReadersByFormatName("jpg").next();
        reader.setInput(ImageIO.createImageInputStream(input), false);
        //LOGGER.debug("number of images: " + reader.getNumImages(true));
        return reader.read(0);
    }

    public static BufferedImage resize(BufferedImage image, int maxWidth, int maxHeight) {
        return Scalr.resize(image, Scalr.Method.BALANCED, Scalr.Mode.BEST_FIT_BOTH, maxWidth, maxHeight);
    }

    public static BufferedImage toGrayScale(BufferedImage image) {
        BufferedImage result = new BufferedImage(
                image.getWidth(),
                image.getHeight(),
                BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = (Graphics2D) result.getGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        return result;
    }

    public static void write(BufferedImage image, OutputStream output, float compression) throws IOException {
        ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
        ImageOutputStream jpgStream = ImageIO.createImageOutputStream(output);

        // Configure JPEG compression
        ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
        jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        jpgWriteParam.setCompressionQuality(compression);

        jpgWriter.setOutput(jpgStream);
        jpgWriter.write(null, new IIOImage(image, null, null), jpgWriteParam);
        jpgWriter.dispose();
        output.flush();
    }
} @SuppressWarnings("WeakerAccess")
public class SwingUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(SwingUtils.class);

    private SwingUtils() {
        super();
    }

    public static UIManager.LookAndFeelInfo getLookAndFeelInfo(String name) {
        for (UIManager.LookAndFeelInfo lnf : UIManager.getInstalledLookAndFeels()) {
            if (lnf.getName().equalsIgnoreCase(name)) {
                return lnf;
            }
        }
        return null;
    }

    public static void installLookAndFeel() {
        try {
            if (SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
                // Always use system look & feel on Windows & Mac.
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } else {
                // Prefer Nimbus or Metal look & feel on other systems.
                UIManager.LookAndFeelInfo lnf = SwingUtils.getLookAndFeelInfo("Nimbus");
                if (lnf == null) lnf = SwingUtils.getLookAndFeelInfo("Metal");
                UIManager.setLookAndFeel((lnf != null) ?
                        lnf.getClassName() :
                        UIManager.getSystemLookAndFeelClassName());
            }
        } catch (Exception ex) {
            LOGGER.warn("Can't set look & feel!", ex);
        }
    }

    /**
     * Change application title for certain Linux desktop environments (e.g. Gnome 3, Ubuntu Unity).
     * <p>
     * This method may not work with future Java version. In this case it might be replaced with the
     * <a href="https://github.com/jelmerk/window-matching-agent">window-matching-agent</a> approach.
     *
     * @param value application title to set
     * @see <a href="https://stackoverflow.com/a/29218320">stackoverflow.com</a>
     */
    public static void setAwtAppClassName(String value) {
        try {
            Toolkit toolkit = Toolkit.getDefaultToolkit();
            if (toolkit.getClass().getName().equals("sun.awt.X11.XToolkit")) {
                Field awtAppClassNameField = toolkit.getClass().getDeclaredField("awtAppClassName");
                awtAppClassNameField.setAccessible(true);
                awtAppClassNameField.set(toolkit, value);
            }
        } catch (Exception ex) {
            LOGGER.warn("Can't set awtAppClassName!", ex);
        }
    }
} public class CopyTextRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final String text;

    public CopyTextRequest() {
        this(null);
    }

    public CopyTextRequest(String text) {
        super();
        this.text = text;
    }
} public class KeyPressRequest implements Serializable {
    private static final long serialVersionUID = 2;
    public final int keyCode;

    public KeyPressRequest() {
        this(KeyEvent.VK_UNDEFINED);
    }

    public KeyPressRequest(int keyCode) {
        super();
        this.keyCode = keyCode;
    }
} public class KeyReleaseRequest implements Serializable {
    private static final long serialVersionUID = 2;
    public final int keyCode;

    public KeyReleaseRequest() {
        this(KeyEvent.VK_UNDEFINED);
    }

    public KeyReleaseRequest(int keyCode) {
        super();
        this.keyCode = keyCode;
    }
} public class KeyTypeRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final char keyChar;

    public KeyTypeRequest() {
        this(KeyEvent.CHAR_UNDEFINED);
    }

    public KeyTypeRequest(char keyChar) {
        super();
        this.keyChar = keyChar;
    }
} public class MouseMoveRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final int x;
    public final int y;

    public MouseMoveRequest() {
        this(-1, -1);
    }

    public MouseMoveRequest(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
} public class MousePressRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final int buttons;

    public MousePressRequest() {
        this(MouseEvent.NOBUTTON);
    }

    public MousePressRequest(int buttons) {
        super();
        this.buttons = buttons;
    }
} public class MouseReleaseRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final int buttons;

    public MouseReleaseRequest() {
        this(MouseEvent.NOBUTTON);
    }

    public MouseReleaseRequest(int buttons) {
        super();
        this.buttons = buttons;
    }
} public class MouseWheelRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final int wheelAmt;

    public MouseWheelRequest() {
        this(0);
    }

    public MouseWheelRequest(int wheelAmt) {
        super();
        this.wheelAmt = wheelAmt;
    }
} public class ScreenRequest implements Serializable {
    private static final long serialVersionUID = 1;
    public final int maxWidth;
    public final int maxHeight;

    public ScreenRequest() {
        this(0, 0);
    }

    public ScreenRequest(int maxWidth, int maxHeight) {
        super();
        this.maxWidth = maxWidth;
        this.maxHeight = maxHeight;
    }
} public class ScreenResponse implements Serializable {
    private static final long serialVersionUID = 1;
    public final ScreenTile[] tiles;
    public final int screenWidth;
    public final int screenHeight;
    public final int imageWidth;
    public final int imageHeight;
    public final int tileWidth;
    public final int tileHeight;

    public ScreenResponse() {
        this(null, 0, 0, 0, 0, 0, 0);
    }

    public ScreenResponse(ScreenTile[] tiles, int screenWidth, int screenHeight, int imageWidth, int imageHeight, int tileWidth, int tileHeight) {
        super();
        this.tiles = tiles;
        this.screenWidth = screenWidth;
        this.screenHeight = screenHeight;
        this.imageWidth = imageWidth;
        this.imageHeight = imageHeight;
        this.tileWidth = tileWidth;
        this.tileHeight = tileHeight;
    }
} public class ScreenTile implements Serializable {
    private static final long serialVersionUID = 1;
    public final byte[] data;

    public ScreenTile() {
        this(null);
    }

    public ScreenTile(byte[] data) {
        super();
        this.data = data;
    }
} public abstract class SocketHandler {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(SocketHandler.class);
    private final Socket socket;
    protected final ConcurrentLinkedQueue<Object> outbox = new ConcurrentLinkedQueue<>();
    private final Object outboxLock = new Object();

    public SocketHandler(Socket socket) {
        super();
        this.socket = socket;
    }

    protected ObjectInputStream createObjectInputStream(InputStream input) throws IOException {
        return new ObjectInputStream(input);
    }

    protected ObjectOutputStream createObjectOutputStream(OutputStream output) throws IOException {
        return new ObjectOutputStream(output);
    }

    public abstract void processReceivedObject(Serializable object);

    public void send(Object object) {
        if (object == null) return;
        this.outbox.add(object);

        try {
            // unblock the socket output thread
            synchronized (outboxLock) {
                outboxLock.notifyAll();
            }
        } catch (Exception ex) {
            LOGGER.warn("Can't unblock socket output!", ex);
        }
    }

    public void stop() {
        this.outbox.clear();
        try {
            this.socket.close();
        } catch (IOException ex) {
            LOGGER.warn("Can't close socket!", ex);
        }
        try {
            // unlock the socket output thread
            synchronized (outboxLock) {
                outboxLock.notifyAll();
            }
        } catch (Exception ex) {
            LOGGER.warn("Can't unblock socket output!", ex);
        }
    }

    public void start() {

        // process socket input
        new Thread(() -> {
            try (ObjectInputStream input = createObjectInputStream(socket.getInputStream())) {
                while (!socket.isClosed() && socket.isConnected()) {
                    try {
                        //LOGGER.debug("processing inbox");
                        processReceivedObject((Serializable) input.readObject());
                    } catch (ClassNotFoundException ex) {
                        LOGGER.error("Received an unsupported object!", ex);
                    }
                }
                //LOGGER.debug("finished inbox thread");
            } catch (Exception ex) {
                LOGGER.error("Can't process socket input!", ex);
            }
            stop();
        }).start();

        // process socket output
        new Thread(() -> {
            try (ObjectOutputStream output = createObjectOutputStream(socket.getOutputStream())) {
                while (!socket.isClosed() && socket.isConnected()) {
                    synchronized (outboxLock) {
                        // might have changed while waiting to synchronize on outbox lock
                        if (socket.isClosed() || !socket.isConnected())
                            break;

                        // send this thread into pause until new requests occur in the outbox
                        if (outbox.isEmpty()) {
                            //LOGGER.debug("pausing outbox");
                            outboxLock.wait();
                        }
                    }

                    // running might have changed since we paused
                    if (socket.isClosed() || !socket.isConnected())
                        break;

                    //LOGGER.debug("processing outbox");
                    Object item = outbox.poll();
                    if (item instanceof ResponseFactory) {
                        try {
                            item = ((ResponseFactory) item).create();
                        } catch (Exception ex) {
                            LOGGER.error("Can't create response item!", ex);
                            continue;
                        }
                    }
                    if (item != null) {
                        output.writeObject(item);
                        output.flush();
                        output.reset();
                    }
                }
                //LOGGER.debug("finished outbox thread");
            } catch (Exception ex) {
                LOGGER.error("Can't process socket output!", ex);
            }
            stop();
        }).start();
    }
} @SuppressWarnings("WeakerAccess")
public class DataMonitor {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(DataMonitor.class);
    protected Vector<DataSample> samples;
    protected final Date epoch;

    public DataMonitor() {
        samples = new Vector<>();
        epoch = new Date();
    }

    public void addSample(long byteCount, Date start, Date end) {
        samples.addElement(new DataSample(byteCount, start, end));
    }

    public float getAverageRate() {
        long msCount = 0;
        long byteCount = 0;
        Date start;
        Date finish;
        int sampleCount = samples.size();
        for (int i = 0; i < sampleCount; i++) {
            DataSample ds = samples.elementAt(i);

            if (ds.start != null)
                start = ds.start;
            else if (i > 0) {
                //DataSample prev = samples.elementAt(i - 1);
                start = ds.end;
            } else
                start = epoch;

            if (ds.end != null)
                finish = ds.end;
            else if (i < sampleCount - 1) {
                //DataSample next = samples.elementAt(i + 1);
                finish = ds.start;
            } else
                finish = new Date();

            // Only include this sample if we could figure out a start
            // and finish time for it.
            if (start != null && finish != null) {
                byteCount += ds.byteCount;
                msCount += finish.getTime() - start.getTime();
            }
        }

        float rate = 0;
        if (msCount > 0) {
            rate = 1000 * (float) byteCount / (float) msCount;
        }

        return rate;
    }

    @SuppressWarnings("unused")
    public float getLastRate() {
        int sampleCount = samples.size();
        return getRateFor(sampleCount - 1);
    }

    public float getRateFor(int sampleIndex) {
        float rate = 0.0f;
        int sampleCount = samples.size();
        if (sampleCount > sampleIndex && sampleIndex >= 0) {
            DataSample s = samples.elementAt(sampleIndex);
            Date start = s.start;
            Date end = s.end;
            if (start == null && sampleIndex >= 1) {
                DataSample prev = samples.elementAt(sampleIndex - 1);
                start = prev.end;
            }

            if (start != null && end != null) {
                long msec = end.getTime() - start.getTime();
                rate = 1000 * (float) s.byteCount / (float) msec;
            }
        }

        return rate;
    }

    public void removeOldSamples(Date olderThan) {
        Vector<DataSample> oldSamples = samples;
        samples = new Vector<>();
        for (DataSample sample : oldSamples) {
            if (!sample.end.before(olderThan))
                samples.add(sample);
        }
    }
} public class MonitoringInputStream extends FilterInputStream {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(MonitoringInputStream.class);
    private final DataMonitor monitor;

    @SuppressWarnings("unused")
    public MonitoringInputStream(InputStream in) {
        this(in, null);
    }

    public MonitoringInputStream(InputStream in, DataMonitor monitor) {
        super(in);
        this.monitor = ObjectUtils.defaultIfNull(monitor, new DataMonitor());
    }

    @SuppressWarnings("unused")
    public final DataMonitor getMonitor() {
        return monitor;
    }

    public int read() throws IOException {
        Date start = new Date();
        int b = super.read();
        monitor.addSample(1, start, new Date());
        return b;
    }

    public int read(byte data[]) throws IOException {
        Date start = new Date();
        int cnt = super.read(data);
        monitor.addSample(cnt, start, new Date());
        return cnt;
    }

    public int read(byte data[], int off, int len) throws IOException {
        Date start = new Date();
        int cnt = super.read(data, off, len);
        monitor.addSample(cnt, start, new Date());
        return cnt;
    }
} public class MonitoringOutputStream extends FilterOutputStream {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(MonitoringOutputStream.class);
    private final DataMonitor monitor;

    @SuppressWarnings("unused")
    public MonitoringOutputStream(OutputStream out) {
        this(out, null);
    }

    public MonitoringOutputStream(OutputStream out, DataMonitor monitor) {
        super(out);
        this.monitor = ObjectUtils.defaultIfNull(monitor, new DataMonitor());
    }

    @SuppressWarnings("unused")
    public DataMonitor getMonitor() {
        return monitor;
    }

    public void write(int b) throws IOException {
        Date start = new Date();
        super.write(b);
        monitor.addSample(1, start, new Date());
    }

    public void write(byte data[]) throws IOException {
        Date start = new Date();
        super.write(data);
        monitor.addSample(data.length, start, new Date());
    }

    public void write(byte data[], int off, int len) throws IOException {
        Date start = new Date();
        super.write(data, off, len);
        monitor.addSample(len, start, new Date());
    }
}
import ch.qos.logback.classic.LoggerContext;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import de.openindex.support.core.AppUtils;
import de.openindex.support.core.ImageUtils;
import de.openindex.support.core.SwingUtils;
import de.openindex.support.core.io.CopyTextRequest;
import de.openindex.support.core.io.KeyPressRequest;
import de.openindex.support.core.io.KeyReleaseRequest;
import de.openindex.support.core.io.KeyTypeRequest;
import de.openindex.support.core.io.MouseMoveRequest;
import de.openindex.support.core.io.MousePressRequest;
import de.openindex.support.core.io.MouseReleaseRequest;
import de.openindex.support.core.io.MouseWheelRequest;
import de.openindex.support.core.io.ScreenRequest;
import de.openindex.support.core.io.ScreenResponse;
import de.openindex.support.core.io.ScreenTile;
import de.openindex.support.core.io.SocketHandler;
import de.openindex.support.core.monitor.DataMonitor;
import de.openindex.support.core.monitor.MonitoringInputStream;
import de.openindex.support.core.monitor.MonitoringOutputStream;
import java.awt.Desktop;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.imageio.ImageIO;
import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocketFactory;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

        @Override
        protected ObjectOutputStream createObjectOutputStream(OutputStream output) throws IOException {
            uploadMonitor = new DataMonitor();
            return super.createObjectOutputStream(
                    new MonitoringOutputStream(output, uploadMonitor));
        }

        @Override
        public void processReceivedObject(Serializable object) {
            if (object instanceof ScreenResponse) {
                //LOGGER.debug("RECEIVE SCREEN RESPONSE");

                final ScreenResponse response = (ScreenResponse) object;
                serverScreenWidth = response.screenWidth;
                serverScreenHeight = response.screenHeight;

                //int byteCount = 0;
                //int sliceCount = 0;
                List<BufferedImage> slices = new ArrayList<>();
                for (ScreenTile tile : response.tiles) {
                    if (tile == null) {
                        slices.add(null);
                        continue;
                    }
                    //byteCount += tile.data.length;
                    //sliceCount++;
                    //LOGGER.debug("received slice (" + tile.data.length + " bytes)");
                    try (InputStream input = new ByteArrayInputStream(tile.data)) {
                        BufferedImage slice = ImageUtils.read(input);
                        if (slice == null) {
                            LOGGER.warn("Can't read tile!");
                            slices.add(null);
                        } else {
                            slices.add(slice);
                        }

                    } catch (Exception ex) {
                        LOGGER.warn("Can't read tile!", ex);
                        slices.add(null);
                    }
                }

                //float bytesPerSlice = (float) byteCount / (float) sliceCount;
                //LOGGER.debug("update screen ("
                //        + sliceCount + " slices, "
                //        + byteCount + " bytes, "
                //        + NumberFormat.getIntegerInstance().format(bytesPerSlice) + " bytes per slice)"
                //);
                frame.updateScreen(
                        slices,
                        response.imageWidth,
                        response.imageHeight,
                        response.tileWidth,
                        response.tileHeight
                );
            } else {
                LOGGER.warn("Received an unsupported object (" + object.getClass().getName() + ")!");
            }
        }

        private void sendCopyText(String text) {
            send(new CopyTextRequest(text));
        }

        private void sendKeyPress(int keyCode) {
            //LOGGER.debug("sendKeyPress | code: " + keyCode);
            send(new KeyPressRequest(keyCode));
        }

        private void sendKeyRelease(int keyCode) {
            //LOGGER.debug("sendKeyRelease | code: " + keyCode);
            send(new KeyReleaseRequest(keyCode));
        }

        private void sendKeyType(char keyChar) {
            //LOGGER.debug("sendKeyType | char: " + keyChar);
            send(new KeyTypeRequest(keyChar));
        }

        private void sendMouseMove(int x, int y) {
            final int viewWidth = frame.getScreenWidth();
            final int viewHeight = frame.getScreenHeight();
            final int imageWidth = frame.getScreenImageWidth();
            final int imageHeight = frame.getScreenImageHeight();
            final int offsetLeft = (viewWidth > imageWidth) ?
                    (int) (((double) (viewWidth - imageWidth)) / 2d) :
                    0;
            final int offsetTop = (viewHeight > imageHeight) ?
                    (int) (((double) (viewHeight - imageHeight)) / 2d) :
                    0;

            if (x < offsetLeft || y < offsetTop) return;

            x -= offsetLeft;
            y -= offsetTop;

            if (x > imageWidth || y > imageHeight) return;

            final double scaleX = (double) serverScreenWidth / (double) imageWidth;
            final double scaleY = (double) serverScreenHeight / (double) imageHeight;

            x = (int) (scaleX * ((double) x));
            y = (int) (scaleY * ((double) y));

            //LOGGER.debug("mouse move    : " + x + " / " + y);
            //LOGGER.debug("> server size : " + serverScreenWidth + " / " + serverScreenHeight);
            //LOGGER.debug("> view size   : " + viewWidth + " / " + viewHeight);
            //LOGGER.debug("> image size  : " + viewWidth + " / " + viewHeight);
            //LOGGER.debug("> offset      : " + offsetLeft + " / " + offsetTop);
            //LOGGER.debug("> coordinates : " + x + " / " + y);

            send(new MouseMoveRequest(x, y));
        }

        private void sendMousePress(int buttons) {
            send(new MousePressRequest(buttons));
        }

        private void sendMouseRelease(int buttons) {
            send(new MouseReleaseRequest(buttons));