org.sikuli.script.App.java Source code

Java tutorial

Introduction

Here is the source code for org.sikuli.script.App.java

Source

/*
 * Copyright (c) 2010-2016, Sikuli.org, sikulix.com
 * Released under the MIT License.
 *
 */
package org.sikuli.script;

import org.sikuli.basics.Debug;
import org.sikuli.natives.OSUtil;
import org.sikuli.natives.SysUtil;

import java.awt.*;
import java.awt.datatransfer.*;
import java.io.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.sikuli.basics.FileManager;

/**
 * App implements features to manage (open, switch to, close) applications. on the system we are running on and to
 * access their assets like windows
 * <br>
 * TAKE CARE: function behavior differs depending on the running system (cosult the docs for more info)
 */
public class App {

    static RunTime runTime = RunTime.get();

    private static final OSUtil _osUtil = SysUtil.getOSUtil();
    private String appNameGiven;
    private String appOptions;
    private String appName;
    private String appWindow;
    private int appPID;
    private boolean isImmediate = false;
    private boolean notFound = false;
    private static final Map<Type, String> appsWindows;
    private static final Map<Type, String> appsMac;
    private static final Region aRegion = new Region();

    static {
        //TODO Sikuli hangs if App is used before Screen
        new Screen();
        _osUtil.checkFeatureAvailability();

        appsWindows = new HashMap<Type, String>();
        appsWindows.put(Type.EDITOR, "Notepad");
        appsWindows.put(Type.BROWSER, "Google Chrome");
        appsWindows.put(Type.VIEWER, "");
        appsMac = new HashMap<Type, String>();
        appsMac.put(Type.EDITOR, "TextEdit");
        appsMac.put(Type.BROWSER, "Safari");
        appsMac.put(Type.VIEWER, "Preview");

    }

    //<editor-fold defaultstate="collapsed" desc="features based on org.apache.httpcomponents.httpclient">
    private static CloseableHttpClient httpclient = null;

    /**
     * create a HTTP Client (for use of wwGet, ... multiple times as session)
     * @return true on success, false otherwise
     */
    public static boolean wwwStart() {
        if (httpclient != null) {
            return true;
        }
        httpclient = HttpClients.createDefault();
        if (httpclient != null) {
            return true;
        }
        return false;
    }

    /**
     * stop a started HTTP Client
     */
    public static void wwwStop() {
        if (httpclient != null) {
            try {
                httpclient.close();
            } catch (IOException ex) {
            }
            httpclient = null;
        }
    }

    /**
     * issue a http(s) request
     * @param url a valid url as used in a browser
     * @return textual content of the response or empty (UTF-8)
     * @throws IOException
     */
    public static String wwwGet(String url) throws IOException {
        HttpGet httpget = new HttpGet(url);
        CloseableHttpResponse response = null;
        ResponseHandler rh = new ResponseHandler() {
            @Override
            public String handleResponse(final HttpResponse response) throws IOException {
                StatusLine statusLine = response.getStatusLine();
                HttpEntity entity = response.getEntity();
                if (statusLine.getStatusCode() >= 300) {
                    throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
                }
                if (entity == null) {
                    throw new ClientProtocolException("Response has no content");
                }
                InputStream is = entity.getContent();
                ByteArrayOutputStream result = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int length;
                while ((length = is.read(buffer)) != -1) {
                    result.write(buffer, 0, length);
                }
                return result.toString("UTF-8");
            }
        };
        boolean oneTime = false;
        if (httpclient == null) {
            wwwStart();
            oneTime = true;
        }
        Object content = httpclient.execute(httpget, rh);
        if (oneTime) {
            wwwStop();
        }
        return (String) content;
    }

    /**
     * same as wwwGet(), but the content is also saved to a file
     * @param url a valid url as used in a browser
     * @param pOut absolute path to output file (overwritten) (if null: bundlePath/wwwSave.txt is taken)
     * @return textual content of the response or empty (UTF-8)
     * @throws IOException
     */
    public static String wwwSave(String url, String pOut) throws IOException {
        String content = wwwGet(url);
        File out = null;
        if (pOut == null) {
            out = new File(ImagePath.getBundleFolder(), "wwwSave.txt");
        } else {
            out = new File(pOut);
        }
        FileManager.writeStringToFile(content, out);
        return content;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="special app features">
    public static enum Type {

        EDITOR, BROWSER, VIEWER
    }

    public static Region start(Type appType) {
        App app = null;
        Region win;
        try {
            if (Type.EDITOR.equals(appType)) {
                if (runTime.runningMac) {
                    app = new App(appsMac.get(appType));
                    if (app.window() != null) {
                        app.focus();
                        aRegion.wait(0.5);
                        win = app.window();
                        aRegion.click(win);
                        aRegion.write("#M.a#B.");
                        return win;
                    } else {
                        app.open();
                        win = app.waitForWindow();
                        app.focus();
                        aRegion.wait(0.5);
                        aRegion.click(win);
                        return win;
                    }
                }
                if (runTime.runningWindows) {
                    app = new App(appsWindows.get(appType));
                    if (app.window() != null) {
                        app.focus();
                        aRegion.wait(0.5);
                        win = app.window();
                        aRegion.click(win);
                        aRegion.write("#C.a#B.");
                        return win;
                    } else {
                        app.open();
                        win = app.waitForWindow();
                        app.focus();
                        aRegion.wait(0.5);
                        aRegion.click(win);
                        return win;
                    }
                }
            } else if (Type.BROWSER.equals(appType)) {
                if (runTime.runningWindows) {
                    app = new App(appsWindows.get(appType));
                    if (app.window() != null) {
                        app.focus();
                        aRegion.wait(0.5);
                        win = app.window();
                        aRegion.click(win);
                        //            aRegion.write("#C.a#B.");
                        return win;
                    } else {
                        app.open();
                        win = app.waitForWindow();
                        app.focus();
                        aRegion.wait(0.5);
                        aRegion.click(win);
                        return win;
                    }
                }
                return null;
            } else if (Type.VIEWER.equals(appType)) {
                return null;
            }
        } catch (Exception ex) {
        }
        return null;
    }

    public Region waitForWindow() {
        return waitForWindow(5);
    }

    public Region waitForWindow(int seconds) {
        Region win = null;
        while ((win = window()) == null && seconds > 0) {
            aRegion.wait(0.5);
            seconds -= 0.5;
        }
        return win;
    }

    public static boolean openLink(String url) {
        if (!Desktop.isDesktopSupported()) {
            return false;
        }
        try {
            Desktop.getDesktop().browse(new URI(url));
        } catch (Exception ex) {
            return false;
        }
        return true;
    }

    private static Region asRegion(Rectangle r) {
        if (r != null) {
            return Region.create(r);
        } else {
            return null;
        }
    }

    public static void pause(int time) {
        try {
            Thread.sleep(time * 1000);
        } catch (InterruptedException ex) {
        }
    }

    public static void pause(float time) {
        try {
            Thread.sleep((int) (time * 1000));
        } catch (InterruptedException ex) {
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="AppEntry">
    public static class AppEntry {

        public String name;
        public String execName;
        public String options;
        public String window;
        public int pid;

        public AppEntry(String theName, String thePID, String theWindow, String theExec, String theOptions) {
            name = theName;
            window = theWindow;
            options = theOptions;
            pid = -1;
            execName = theExec;
            try {
                pid = Integer.parseInt(thePID);
            } catch (Exception ex) {
            }
        }
    }

    public AppEntry makeAppEntry() {
        String name = appName;
        String window = appWindow;
        if (name.isEmpty() && appOptions.isEmpty()) {
            name = appNameGiven;
        }
        if (isImmediate && !window.startsWith("!")) {
            window = "!" + window;
        }
        if (notFound) {
            name = "!" + name;
        }
        String pid = getPID().toString();
        AppEntry appEntry = new AppEntry(name, pid, window, appNameGiven, appOptions);
        return appEntry;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="constructors">
    /**
     * creates an instance for an app with this name (nothing done yet)
     *
     * @param name name
     */
    public App(String name) {
        appNameGiven = name;
        appName = name;
        appPID = -1;
        appWindow = "";
        appOptions = "";
        String execName = "";
        if (appNameGiven.startsWith("+")) {
            isImmediate = true;
            appNameGiven = appNameGiven.substring(1);
            Debug.log(3, "App.immediate: %s", appNameGiven);
            appName = appNameGiven;
            String[] parts;
            if (appName.startsWith("\"")) {
                parts = appName.substring(1).split("\"");
                if (parts.length > 1) {
                    appOptions = appName.substring(parts[0].length() + 3);
                    appName = "\"" + parts[0] + "\"";
                }
            } else {
                parts = appName.split(" ");
                if (parts.length > 1) {
                    appOptions = appName.substring(parts[0].length() + 1);
                    appName = parts[0];
                }
            }
            if (appName.startsWith("\"")) {
                execName = appName.substring(1, appName.length() - 1);
            } else {
                execName = appName;
            }
            appName = new File(execName).getName();
            File checkName = new File(execName);
            if (checkName.isAbsolute()) {
                if (!checkName.exists()) {
                    appName = "";
                    appOptions = "";
                    appWindow = "!";
                    notFound = true;
                }
            }
        } else {
            init(appNameGiven);
        }
        Debug.log(3, "App.create: %s", toStringShort());
    }

    private void init(String name) {
        AppEntry app = null;
        if (!(isImmediate && notFound)) {
            app = _osUtil.getApp(-1, name);
        }
        if (app != null) {
            appName = app.name;
            if (app.options.isEmpty()) {
                appPID = app.pid;
                if (!app.window.contains("N/A")) {
                    appWindow = app.window;
                    if (notFound) {
                        notFound = false;
                    }
                }
            } else {
                appOptions = app.options;
                appNameGiven = appName;
            }
        }
    }

    public App(int pid) {
        appNameGiven = "FromPID";
        appName = "";
        appPID = pid;
        appWindow = "";
        init(pid);
    }

    private void init(int pid) {
        AppEntry app = _osUtil.getApp(pid, appName);
        if (app != null) {
            appName = app.name;
            appPID = app.pid;
            if (!app.window.contains("N/A")) {
                appWindow = app.window;
            }
        } else {
            appPID = -1;
        }
    }

    private void init() {
        if (appPID > -1) {
            init(appPID);
        } else {
            String name = appName;
            if (name.isEmpty() && appOptions.isEmpty()) {
                name = appNameGiven;
            }
            init(name);
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="getter/setter">
    public static void getApps(String name) {
        Map<Integer, String[]> theApps = _osUtil.getApps(name);
        int count = 0;
        String[] item;
        for (Integer pid : theApps.keySet()) {
            item = theApps.get(pid);
            if (pid < 0) {
                pid = -pid;
                Debug.logp("%d:%s (N/A)", pid, item[0]);
            } else {
                Debug.logp("%d:%s (%s)", pid, item[0], item[1]);
                count++;
            }
        }
        Debug.logp("App.getApps: %d apps (%d having window)", theApps.size(), count);
    }

    public static void getApps() {
        getApps(null);
    }

    public App setUsing(String options) {
        if (options != null) {
            appOptions = options;
        } else {
            appOptions = "";
        }
        return this;
    }

    public Integer getPID() {
        return appPID;
    }

    public String getName() {
        return appName;
    }

    public String getWindow() {
        return appWindow;
    }

    public boolean isValid() {
        return !notFound;
    }

    public boolean isRunning() {
        return isRunning(1);
    }

    public boolean isRunning(int maxTime) {
        if (!isValid()) {
            return false;
        }
        long wait = -1;
        for (int n = 0; n < maxTime; n++) {
            wait = new Date().getTime();
            int retVal = _osUtil.isRunning(makeAppEntry());
            if (retVal > 0) {
                init();
                break;
            }
            if (n == 0) {
                continue;
            }
            wait = 1000 - new Date().getTime() + wait;
            if (wait > 0) {
                RunTime.pause(wait / 1000f);
            }
        }
        return appPID > -1;
    }

    public boolean hasWindow() {
        if (!isValid()) {
            return false;
        }
        init(appName);
        return !getWindow().isEmpty();
    }

    @Override
    public String toString() {
        if (!appWindow.startsWith("!")) {
            init();
        }
        return String.format("[%d:%s (%s)] %s", appPID, appName, appWindow, appNameGiven);
    }

    public String toStringShort() {
        return String.format("[%d:%s]", appPID, appName);
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="open">
    /**
     * creates an instance for an app with this name and tries to open it
     *
     * @param appName name
     * @return the App instance or null on failure
     */
    public static App open(String appName) {
        return new App("+" + appName).open();
    }

    /**
     * tries to open the app defined by this App instance<br>
     * do not wait for the app to get running
     *
     * @return this or null on failure
     */
    public App open() {
        return openAndWait(0);
    }

    /**
     * tries to open the app defined by this App instance
     *
     * @return this or null on failure
     */
    public App open(int waitTime) {
        return openAndWait(waitTime);
    }

    public App openAndWait(int waitTime) {
        if (isImmediate) {
            appPID = _osUtil.open(appNameGiven);
        } else {
            AppEntry appEntry = makeAppEntry();
            init(_osUtil.open(appEntry));
        }
        if (appPID < 0) {
            Debug.error("App.open failed: " + appNameGiven + " not found");
            notFound = true;
        } else {
            Debug.action("App.open " + this.toStringShort());
        }
        if (isImmediate && notFound) {
            return null;
        }
        if (waitTime > 0) {
            if (!isRunning(waitTime)) {
                return null;
            }
        }
        return this;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="close">
    /**
     * tries to identify a running app with the given name and then tries to close it
     *
     * @param appName name
     * @return 0 for success -1 otherwise
     */
    public static int close(String appName) {
        return new App("+" + appName).close();
    }

    /**
     * tries to close the app defined by this App instance
     *
     * @return this or null on failure
     */
    public int close() {
        if (!isValid()) {
            return 0;
        }
        if (appPID > -1) {
            init(appPID);
        } else if (isImmediate) {
            init();
        }
        int ret = _osUtil.close(makeAppEntry());
        if (ret > -1) {
            Debug.action("App.close: %s", this.toStringShort());
            appPID = -1;
            appWindow = "";
        } else {
            Debug.error("App.close %s did not work", this);
        }
        return ret;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="focus">
    /**
     * tries to identify a running app with name and if not running tries to open it and tries to make it the foreground
     * application bringing its topmost window to front
     *
     * @param appName name
     * @return the App instance or null on failure
     */
    public static App focus(String appName) {
        return focus(appName, 0);
    }

    /**
     * tries to identify a running app with name and if not running tries to open it and tries to make it the foreground
     * application bringing its window with the given number to front
     *
     * @param appName name
     * @param num window
     * @return the App instance or null on failure
     */
    public static App focus(String appName, int num) {
        return (new App("+" + appName)).focus(num);
    }

    /**
     * tries to make it the foreground application bringing its topmost window to front
     *
     * @return the App instance or null on failure
     */
    public App focus() {
        if (appPID > -1) {
            init(appPID);
        }
        return focus(0);
    }

    /**
     * tries to make it the foreground application bringing its window with the given number to front
     *
     * @param num window
     * @return the App instance or null on failure
     */
    public App focus(int num) {
        if (!isValid()) {
            if (!appWindow.startsWith("!")) {
                return this;
            }
        }
        if (isImmediate) {
            appPID = _osUtil.switchto(appNameGiven, num);
        } else {
            init(_osUtil.switchto(makeAppEntry(), num));
        }
        if (appPID < 0) {
            Debug.error("App.focus failed: " + (num > 0 ? " #" + num : "") + " " + this.toString());
            return null;
        } else {
            Debug.action("App.focus: " + (num > 0 ? " #" + num : "") + " " + this.toStringShort());
            if (appPID < 1) {
                init();
            }
        }
        return this;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="window">
    /**
     * evaluates the region currently occupied by the topmost window of this App instance. The region might not be fully
     * visible, not visible at all or invalid with respect to the current monitor configuration (outside any screen)
     *
     * @return the region
     */
    public Region window() {
        if (appPID != 0) {
            return asRegion(_osUtil.getWindow(appPID));
        }
        return asRegion(_osUtil.getWindow(appNameGiven));
    }

    /**
     * evaluates the region currently occupied by the window with the given number of this App instance. The region might
     * not be fully visible, not visible at all or invalid with respect to the current monitor configuration (outside any
     * screen)
     *
     * @param winNum window
     * @return the region
     */
    public Region window(int winNum) {
        if (appPID != 0) {
            return asRegion(_osUtil.getWindow(appPID, winNum));
        }
        return asRegion(_osUtil.getWindow(appNameGiven, winNum));
    }

    /**
     * evaluates the region currently occupied by the systemwide frontmost window (usually the one that has focus for
     * mouse and keyboard actions)
     *
     * @return the region
     */
    public static Region focusedWindow() {
        return asRegion(_osUtil.getFocusedWindow());
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="run">
    public static int lastRunReturnCode = -1;
    public static String lastRunStdout = "";
    public static String lastRunStderr = "";
    public static String lastRunResult = "";

    /**
     * the given text is parsed into a String[] suitable for issuing a Runtime.getRuntime().exec(args). quoting is
     * preserved/obeyed. the first item must be an executable valid for the running system.<br>
     * After completion, the following information is available: <br>
     * App.lastRunResult: a string containing the complete result according to the docs of the run() command<br>
     * App.lastRunStdout: a string containing only the output lines that went to stdout<br>
     * App.lastRunStderr: a string containing only the output lines that went to stderr<br>
     * App.lastRunReturnCode: the value, that is returnd as returncode
     *
     * @param cmd the command to run starting with an executable item
     * @return the final returncode of the command execution
     */
    public static int run(String cmd) {
        lastRunResult = runTime.runcmd(cmd);
        String NL = runTime.runningWindows ? "\r\n" : "\n";
        String[] res = lastRunResult.split(NL);
        try {
            lastRunReturnCode = Integer.parseInt(res[0].trim());
        } catch (Exception ex) {
        }
        lastRunStdout = "";
        lastRunStderr = "";
        boolean isError = false;
        for (int n = 1; n < res.length; n++) {
            if (isError) {
                lastRunStderr += res[n] + NL;
                continue;
            }
            if (RunTime.runCmdError.equals(res[n])) {
                isError = true;
                continue;
            }
            lastRunStdout += res[n] + NL;
        }
        return lastRunReturnCode;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="clipboard">
    /**
     * evaluates the current textual content of the system clipboard
     *
     * @return the textual content or empty string if not possible
     */
    public static String getClipboard() {
        Transferable content = null;
        try {
            content = Clipboard.getSystemClipboard().getContents(null);
        } catch (Exception ex) {
            Debug.error("Env.getClipboard: clipboard not available:\n%s", ex.getMessage());
        }
        if (content != null) {
            try {
                if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                    return (String) content.getTransferData(DataFlavor.stringFlavor);
                }
            } catch (UnsupportedFlavorException ex) {
                Debug.error("Env.getClipboard: UnsupportedFlavorException: " + content);
            } catch (IOException ex) {
                Debug.error("Env.getClipboard: IOException:\n%s", ex.getMessage());
            }
        }
        return "";
    }

    /**
     * sets the current textual content of the system clipboard to the given text
     *
     * @param text text
     */
    public static void setClipboard(String text) {
        Clipboard.putText(Clipboard.PLAIN, Clipboard.UTF8, Clipboard.CHAR_BUFFER, text);
    }

    private static class Clipboard {

        public static final TextType HTML = new TextType("text/html");
        public static final TextType PLAIN = new TextType("text/plain");

        public static final Charset UTF8 = new Charset("UTF-8");
        public static final Charset UTF16 = new Charset("UTF-16");
        public static final Charset UNICODE = new Charset("unicode");
        public static final Charset US_ASCII = new Charset("US-ASCII");

        public static final TransferType READER = new TransferType(Reader.class);
        public static final TransferType INPUT_STREAM = new TransferType(InputStream.class);
        public static final TransferType CHAR_BUFFER = new TransferType(CharBuffer.class);
        public static final TransferType BYTE_BUFFER = new TransferType(ByteBuffer.class);

        private Clipboard() {
        }

        /**
         * Dumps a given text (either String or StringBuffer) into the Clipboard, with a default MIME type
         */
        public static void putText(CharSequence data) {
            StringSelection copy = new StringSelection(data.toString());
            getSystemClipboard().setContents(copy, copy);
        }

        /**
         * Dumps a given text (either String or StringBuffer) into the Clipboard with a specified MIME type
         */
        public static void putText(TextType type, Charset charset, TransferType transferType, CharSequence data) {
            String mimeType = type + "; charset=" + charset + "; class=" + transferType;
            TextTransferable transferable = new TextTransferable(mimeType, data.toString());
            getSystemClipboard().setContents(transferable, transferable);
        }

        public static java.awt.datatransfer.Clipboard getSystemClipboard() {
            return Toolkit.getDefaultToolkit().getSystemClipboard();
        }

        private static class TextTransferable implements Transferable, ClipboardOwner {

            private String data;
            private DataFlavor flavor;

            public TextTransferable(String mimeType, String data) {
                flavor = new DataFlavor(mimeType, "Text");
                this.data = data;
            }

            @Override
            public DataFlavor[] getTransferDataFlavors() {
                return new DataFlavor[] { flavor, DataFlavor.stringFlavor };
            }

            @Override
            public boolean isDataFlavorSupported(DataFlavor flavor) {
                boolean b = this.flavor.getPrimaryType().equals(flavor.getPrimaryType());
                return b || flavor.equals(DataFlavor.stringFlavor);
            }

            @Override
            public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                if (flavor.isRepresentationClassInputStream()) {
                    return new StringReader(data);
                } else if (flavor.isRepresentationClassReader()) {
                    return new StringReader(data);
                } else if (flavor.isRepresentationClassCharBuffer()) {
                    return CharBuffer.wrap(data);
                } else if (flavor.isRepresentationClassByteBuffer()) {
                    return ByteBuffer.wrap(data.getBytes());
                } else if (flavor.equals(DataFlavor.stringFlavor)) {
                    return data;
                }
                throw new UnsupportedFlavorException(flavor);
            }

            @Override
            public void lostOwnership(java.awt.datatransfer.Clipboard clipboard, Transferable contents) {
            }
        }

        /**
         * Enumeration for the text type property in MIME types
         */
        public static class TextType {

            private String type;

            private TextType(String type) {
                this.type = type;
            }

            @Override
            public String toString() {
                return type;
            }
        }

        /**
         * Enumeration for the charset property in MIME types (UTF-8, UTF-16, etc.)
         */
        public static class Charset {

            private String name;

            private Charset(String name) {
                this.name = name;
            }

            @Override
            public String toString() {
                return name;
            }
        }

        /**
         * Enumeration for the transferScriptt type property in MIME types (InputStream, CharBuffer, etc.)
         */
        public static class TransferType {

            private Class dataClass;

            private TransferType(Class streamClass) {
                this.dataClass = streamClass;
            }

            public Class getDataClass() {
                return dataClass;
            }

            @Override
            public String toString() {
                return dataClass.getName();
            }
        }

    }
    //</editor-fold>
}