org.ebayopensource.twin.ElementImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.ebayopensource.twin.ElementImpl.java

Source

// [Twin] Copyright eBay Inc., Twin authors, and other contributors.
// This file is provided to you under the terms of the Apache License, Version 2.0.
// See LICENSE.txt and NOTICE.txt for license and copyright information.

package org.ebayopensource.twin;

import java.lang.reflect.*;
import java.util.*;

import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;

import org.apache.commons.codec.binary.Base64;

import org.ebayopensource.twin.ScrollBar.Orientation;
import org.ebayopensource.twin.element.*;
import org.ebayopensource.twin.pattern.*;

/**
 * This class actually implements the behaviour of Element and its subinterfaces.
 * All Elements returned from Twin are generated by Element.create(). This returns proxy objects
 * that reflectively delegate to an instance of ElementImpl. Note that these are not themselves ElementImpl instances. 
 * The only reason ElementImpl implements Element is so we get a static check that all methods will be present at runtime.
 */
class ElementImpl extends RemoteResource implements Element {
    /** Win32 window class */
    private String className;
    /** UIAutomation AutomationId */
    private String id;
    /** Name property, cached from last fetch, for use in toString() */
    private String cachedName;

    /** 
     * For internal use only. Creates an Element wrapping the given RemoteObject 
     * This should be used instead of new Element(), as it will instantiate the correct subclass.
     */
    public static Element create(RemoteObject o) {
        if (o == null)
            return null;

        List<Class<?>> interfaces = new ArrayList<Class<?>>();
        interfaces.add(Element.class);
        interfaces.add(RemoteResourceInterface.class);

        final Class<? extends ControlType> controlTypeInterface = NameMappings
                .getTypeInterface((String) o.properties.get("controlType"));
        if (controlTypeInterface.equals(Desktop.class))
            return new DesktopImpl(o.session);
        if (controlTypeInterface != null)
            interfaces.add(controlTypeInterface);

        List<Class<? extends ControlPattern>> controlPatternInterfaces = getControlPatternInterfaces(o);
        interfaces.addAll(controlPatternInterfaces);

        final ElementImpl impl = new ElementImpl(o, controlTypeInterface, controlPatternInterfaces);
        if (interfaces.isEmpty())
            return impl;

        final HashSet<Class<?>> implementedPatterns = new HashSet<Class<?>>();
        for (Class<?> iface : interfaces)
            if (isInterfaceExtending(iface, ControlPattern.class))
                implementedPatterns.add(iface);

        return (Element) Proxy.newProxyInstance(ElementImpl.class.getClassLoader(),
                interfaces.toArray(new Class[interfaces.size()]), new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Method implMethod = null;
                        try {
                            implMethod = impl.getClass().getMethod(method.getName(), method.getParameterTypes());
                        } catch (NoSuchMethodException e) {
                            implMethod = impl.getClass().getDeclaredMethod(method.getName(),
                                    method.getParameterTypes());
                        }
                        Require requirement = implMethod.getAnnotation(Require.class);
                        if (requirement != null) {
                            for (Class<?> pattern : requirement.pattern())
                                if (!implementedPatterns.contains(pattern))
                                    throw new TwinException("This "
                                            + (impl.getControlType() == null ? "Unknown" : impl.getControlType())
                                            + " does not implement the control pattern " + pattern.getSimpleName());
                            if (requirement.type() != Void.class)
                                if (controlTypeInterface != requirement.type())
                                    throw new TwinException("This "
                                            + (impl.getControlType() == null ? "Unknown" : impl.getControlType())
                                            + " is not of ControlType " + requirement.type().getSimpleName());
                        }
                        try {
                            return implMethod.invoke(impl, args);
                        } catch (InvocationTargetException e) {
                            throw e.getCause();
                        }
                    }
                });
    }

    private static List<Class<? extends ControlPattern>> getControlPatternInterfaces(RemoteObject o) {
        List<Class<? extends ControlPattern>> result = new ArrayList<Class<? extends ControlPattern>>();
        if (o.properties.containsKey("controlPatterns")) {
            for (Object patternObj : (List<?>) o.properties.get("controlPatterns")) {
                String pattern = String.valueOf(patternObj);
                Class<? extends ControlPattern> matchingIface = NameMappings.getPatternInterface(pattern);
                if (matchingIface != null)
                    result.add(matchingIface);
            }
        }
        return result;
    }

    /** Is c an interface that directly extends i? */
    static final boolean isInterfaceExtending(Class<?> c, Class<?> i) {
        if (!c.isInterface())
            return false;
        for (Class<?> d : c.getInterfaces())
            if (i.equals(d))
                return true;
        return false;
    }

    /** 
     * For internal use only. Creates an Element wrapping the given RemoteObject 
     * Element.create() should be used instead, as it will instantiate the correct subclass.
     */
    protected ElementImpl(RemoteObject o, Class<? extends ControlType> controlType,
            List<Class<? extends ControlPattern>> controlPatterns) {
        super(o);
        this.controlType = controlType;
        this.controlPatterns = controlPatterns;
        this.className = (String) o.properties.get("className");
        this.id = (String) o.properties.get("id");
        this.cachedName = (String) o.properties.get("name");
    }

    /** 
     * For internal use only. Creates an element without an associated RemoteObject. 
     * Use with caution!
     */
    protected ElementImpl(Application session, Class<? extends ControlType> controlType,
            List<Class<? extends ControlPattern>> controlPatterns) {
        super(session);
        this.controlType = controlType;
        this.controlPatterns = controlPatterns;
    }

    public String getCachedName() {
        return cachedName;
    }

    public String getName() throws TwinException {
        return cachedName = (String) session.request("GET", getPath() + "/name", null);
    }

    public String getId() throws TwinException {
        return id;
    }

    public String getClassName() throws TwinException {
        return className;
    }
    //   /** Get the ControlType of this element. This is an abstract description of the function performed by the element */
    //   public ControlTypeEnum getControlType() {
    //      return controlType;
    //   }

    private Class<? extends ControlType> controlType;
    private List<Class<? extends ControlPattern>> controlPatterns;

    public Class<? extends ControlType> getControlType() {
        return controlType;
    }

    public List<Class<? extends ControlPattern>> getControlPatterns() {
        return controlPatterns;
    }

    public boolean is(Class<? extends Element> pattern) {
        if (pattern.isInterface())
            for (Class<?> iface : pattern.getInterfaces()) {
                if (iface == ControlPattern.class)
                    return controlPatterns.contains(pattern);
                else if (iface == ControlType.class)
                    return controlType == pattern;
            }
        // if it's not a control type or control pattern, return instanceof
        return pattern.isInstance(this);
    }

    public <T extends Element> T as(Class<T> pattern) {
        T t = pattern.cast(this); // generate exception if regular type doesn't match
        if (!is(pattern))
            throw new ClassCastException("Object does not support " + pattern.getSimpleName() + ": " + this);
        return t;
    }

    @Require(pattern = Editable.class)
    public String getValue() throws TwinException {
        return (String) session.request("GET", getPath() + "/value", null);
    }

    @Require(pattern = Editable.class)
    public void setValue(String s) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("value", s);
        session.request("POST", getPath() + "/value", data);
    }

    @Require(pattern = Editable.class)
    public boolean isReadOnly() throws TwinException {
        return session.options(getPath() + "/value").contains("POST");
    }

    public boolean isEnabled() throws TwinException {
        return (boolean) (Boolean) session.request("GET", getPath() + "/enabled", null);
    }

    @Require(pattern = Expandable.class)
    public boolean isExpanded() throws TwinException {
        return (boolean) (Boolean) session.request("GET", getPath() + "/expanded", null);
    }

    @Require(pattern = Expandable.class)
    public void setExpanded(boolean expanded) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("expanded", expanded);
        session.request("POST", getPath() + "/expanded", data);
    }

    @Require(pattern = Selectable.class)
    public boolean isSelected() throws TwinException {
        return (boolean) (Boolean) session.request("GET", getPath() + "/selected", null);
    }

    @Require(pattern = Selectable.class)
    public void setSelected(boolean selected) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("selected", selected);
        session.request("POST", getPath() + "/selected", data);
    }

    @Require(pattern = Selectable.class)
    public SelectionContainer getContainer() throws TwinException {
        RemoteObject container = (RemoteObject) session.request("GET", getPath() + "/selection-container", null);
        return (SelectionContainer) ElementImpl.create(container);
    }

    @Require(pattern = SelectionContainer.class)
    public boolean isMultipleSelectionAllowed() throws TwinException {
        Map<String, Object> result = session.requestObject("GET", getPath() + "/selection", null);
        return (boolean) (Boolean) result.get("multiple");
    }

    @Require(pattern = SelectionContainer.class)
    public boolean isSelectionRequired() throws TwinException {
        Map<String, Object> result = session.requestObject("GET", getPath() + "/selection", null);
        return (boolean) (Boolean) result.get("required");
    }

    @Require(pattern = SelectionContainer.class)
    public List<Selectable> getSelection() throws TwinException {
        Map<String, Object> result = session.requestObject("GET", getPath() + "/selection-info", null);
        List<?> selectionObjects = (List<?>) result.get("values");
        List<Selectable> ret = new ArrayList<Selectable>();
        for (Object obj : selectionObjects) {
            if (!(obj instanceof Selectable))
                throw new IllegalStateException(
                        "getSelection() /selection-info contained " + obj + " which is not selectable");
            ret.add((Selectable) obj);
        }
        return ret;
    }

    @Require(pattern = SelectionContainer.class)
    public Selectable getSelectedItem() throws TwinException {
        List<Selectable> selection = getSelection();
        if (selection.size() == 0)
            return null;
        if (selection.size() == 1)
            return selection.get(0);
        throw new TwinException("Expected 0 or 1 result, got " + selection);
    }

    public Dimension getSize() throws TwinException {
        Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null);
        int width = ((Number) results.get("width")).intValue();
        int height = ((Number) results.get("height")).intValue();
        return new Dimension(width, height);
    }

    public Point getLocation() throws TwinException {
        Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null);
        int x = ((Number) results.get("x")).intValue();
        int y = ((Number) results.get("y")).intValue();
        return new Point(x, y);
    }

    public Rectangle getBounds() throws TwinException {
        Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null);
        int width = ((Number) results.get("width")).intValue();
        int height = ((Number) results.get("height")).intValue();
        int x = ((Number) results.get("x")).intValue();
        int y = ((Number) results.get("y")).intValue();
        return new Rectangle(x, y, width, height);
    }

    @Require(pattern = Transformable.class)
    public void setSize(int width, int height) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("width", width);
        data.put("height", height);
        session.request("POST", getPath() + "/size", data);
    }

    @Require(pattern = Transformable.class)
    public void setLocation(int x, int y) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("x", x);
        data.put("y", y);
        session.request("POST", getPath() + "/location", data);
    }

    @Require(pattern = Transformable.class)
    public void setBounds(int x, int y, int width, int height) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("x", x);
        data.put("y", y);
        data.put("width", width);
        data.put("height", height);
        session.request("POST", getPath() + "/bounds", data);
    }

    @Require(pattern = Toggle.class)
    public boolean getState() throws TwinException {
        return (boolean) (Boolean) session.request("GET", getPath() + "/toggle", null);
    }

    @Require(pattern = Toggle.class)
    public void setState(boolean b) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("state", b);
        session.request("POST", getPath() + "/toggle", data);
    }

    @Require(pattern = Toggle.class)
    public boolean toggle() throws TwinException {
        return (boolean) (Boolean) session.request("POST", getPath() + "/toggle", null);
    }

    public void click() throws TwinException {
        session.request("POST", getPath() + "/click", null);
    }

    public void click(MouseButton button) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("button", button.toString().toLowerCase());
        session.request("POST", getPath() + "/click", data);
    }

    public void click(int x, int y) throws TwinException {
        click(x, y, MouseButton.Left);
    }

    public void click(int x, int y, MouseButton button) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("button", button.toString().toLowerCase());
        data.put("x", x);
        data.put("y", y);
        session.request("POST", getPath() + "/click", data);
    }

    public Menu contextMenu() throws TwinException {
        click(MouseButton.Right);
        return (Menu) session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1);
    }

    public Menu contextMenu(int x, int y) throws TwinException {
        click(x, y, MouseButton.Right);
        return (Menu) session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1);
    }

    public String getStructure(boolean verbose) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("verbose", verbose);
        return (String) session.request("GET", getPath() + "/structure", data);
    }

    public String getStructure() throws TwinException {
        return getStructure(false);
    }

    public Screenshot getScreenshot() throws TwinException {
        return decodeScreenshot(session.requestObject("GET", getPath() + "/screenshot", null));
    }

    public Screenshot getScreenshot(Rectangle bounds) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("x", bounds.x);
        data.put("y", bounds.y);
        data.put("width", bounds.width);
        data.put("height", bounds.height);
        return decodeScreenshot(session.requestObject("GET", getPath() + "/screenshot", data));
    }

    public Screenshot getBoundsScreenshot() throws TwinException {
        return getApplication().getDesktop().getScreenshot(getBounds());
    }

    public void sendKeys(String text) throws TwinException {
        Map<String, Object> keys = new HashMap<String, Object>();
        keys.put("keys", text);
        session.request("POST", getPath() + "/keyboard", keys);
    }

    public void type(String text) throws TwinException {
        text = text.replaceAll("[\\+\\^\\%\\(\\)\\{\\}\\[\\]]", "{$0}");
        text = text.replace("\n", "~");
        text = text.replace("\b", "{BS}");
        text = text.replace("\t", "{TAB}");
        sendKeys(text);
    }

    private Element cachedParent;

    public Element getCachedParent() {
        if (cachedParent == null)
            return getParent();
        return cachedParent;
    }

    public Element getParent() throws TwinException {
        return cachedParent = ElementImpl
                .create((RemoteObject) session.request("GET", getPath() + "/parent", null));
    }

    public List<Element> getChildren() throws TwinException {
        return getChildren(null);
    }

    public <T extends Element> List<T> getChildren(Criteria criteria) throws TwinException {
        return getElements("children", criteria, 0, 0, false);
    }

    public List<Element> getDescendants(Criteria criteria) throws TwinException {
        return getElements("descendants", criteria, 0, 0, false);
    }

    public <T extends Element> T getChild(Criteria criteria) throws TwinException {
        return single(this.<T>getElements("children", criteria, 0, 0, true));
    }

    public <T extends Element> T waitForChild(Criteria criteria) throws TwinException {
        return this.<T>waitForChild(criteria, getApplication().getTimeout());
    }

    public <T extends Element> T waitForChild(Criteria criteria, double timeout) throws TwinException {
        return single(this.<T>getElements("children", criteria, 0, timeout, true));
    }

    public <T extends Element> T getDescendant(Criteria criteria) throws TwinException {
        return single(this.<T>getElements("descendants", criteria, 0, 0, true));
    }

    public <T extends Element> T waitForDescendant(Criteria criteria) throws TwinException {
        return this.<T>waitForDescendant(criteria, getApplication().getTimeout());
    }

    public <T extends Element> T waitForDescendant(Criteria criteria, double timeout) throws TwinException {
        return single(this.<T>getElements("descendants", criteria, 0, timeout, true));
    }

    public <T extends Element> List<T> getClosestDescendants(Criteria criteria) throws TwinException {
        return getElements("descendants", criteria, 1, 0, false);
    }

    public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria) throws TwinException {
        return waitForClosestDescendants(criteria, getApplication().getTimeout());
    }

    public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria, double timeout)
            throws TwinException {
        return getElements("descendants", criteria, 1, timeout, false);
    }

    /** 
     * Internal impl behind {get,waitFor}{Closest,}{Child,Children,Descendant,Descendants} methods 
     * @param path what to append to getPath(), e.g. "/children" or "/descendants"
     * @param criteria the criteria to apply (process-ID matching is added by the server)
     * @param count if 0, return all results. Else BFS layer-by-layer until we have at least count
     * @param timeout if 0, return results immediately. Else don't return an empty result set until this timeout has elapsed
     * @param shouldThrow should throw a TwinNoSuchElementException on empty list
     */
    @SuppressWarnings("unchecked")
    private <T extends Element> List<T> getElements(String subpath, Criteria criteria, int count, double timeout,
            boolean shouldThrow) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        if (criteria != null)
            data.put("criteria", criteria);
        if (count > 0)
            data.put("count", count);
        if (timeout > 0) {
            if (Double.isInfinite(timeout))
                data.put("waitForResults", true);
            else
                data.put("waitForResults", timeout);
        }
        List<Object> searchResults = session.requestArray("GET", getPath() + "/" + subpath, data);
        List<T> result = new ArrayList<T>();
        for (Object remote : searchResults)
            result.add((T) ElementImpl.create((RemoteObject) remote));
        if (shouldThrow && result.isEmpty()) {
            String message = "Found no " + subpath + " of " + this;
            if (criteria != null)
                message += " matching " + criteria;
            if (timeout > 0)
                message += " after waiting " + timeout;
            throw TwinError.NoSuchElement.create(message);
        }
        return result;
    }

    /** Return the single element in a single list, null for an empty list, and throw for a list with multiple entries */
    private <T> T single(List<T> list) throws TwinException {
        if (list.size() == 1)
            return list.get(0);
        throw new TwinException("Expected 1 result, found " + list.size());
    }

    public void focus() throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("focusedElement", remote);
        session.request("POST", "/element/active", data);
    }

    /** Decode a screenshot from the result object */
    private Screenshot decodeScreenshot(Map<String, Object> results) {
        String contentType = (String) results.get("contentType");
        String base64data = (String) results.get("data");
        byte[] data = new Base64().decode(base64data);
        return new Screenshot(data, contentType);
    }

    /** Get the server path for this element */
    public String getPath() {
        return "/element/" + remote.uuid;
    }

    /** Return a string including the controltype, last knownname, className, id, and server UUID of this element */
    public String toString() {
        StringBuffer sb = new StringBuffer(NameMappings.getTypeName(controlType));
        sb.append("(");
        if (cachedName != null)
            sb.append("name=").append(cachedName).append(' ');
        if (className != null)
            sb.append("class=").append(className).append(' ');
        sb.append("id=").append(id).append(")");
        sb.append("@").append(remote);
        return sb.toString();
    }

    public ScrollBar getHorizontalScrollBar() throws TwinException {
        return getScrollBar(ScrollBar.Orientation.Horizontal);
    }

    public ScrollBar getVerticalScrollBar() throws TwinException {
        return getScrollBar(ScrollBar.Orientation.Vertical);
    }

    public ScrollBar getScrollBar(ScrollBar.Orientation orientation) throws TwinException {
        String resource = (orientation == ScrollBar.Orientation.Horizontal) ? "axisX" : "axisY";
        RemoteObject o = (RemoteObject) session.request("GET", getPath() + "/" + resource, null);
        if (o == null)
            throw TwinError.NoSuchElement.create("Couldn't get " + orientation + " scrollbar for element " + this);
        return new ScrollBarImpl(o, this, orientation);
    }

    public Element button(String name) throws TwinException {
        return single(this.getClosestDescendants(Criteria.type(Button.class).and(Criteria.name(name))));
    }

    public boolean exists() throws TwinException {
        return (Boolean) session.request("GET", getPath() + "/exists", null);
    }

    public void waitForNotExists() throws TwinException {
        waitForNotExists(getApplication().getTimeout());
    }

    public void waitForNotExists(double timeout) throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("timeout", timeout);
        data.put("value", false);

        session.request("POST", getPath() + "/exists", data);
    }

    public void scrollVisible(Element child) throws TwinException {
        Rectangle bounds = getBounds();
        Rectangle childBounds = child.getBounds();
        scrollVisible(child, Orientation.Horizontal, bounds, childBounds);
        scrollVisible(child, Orientation.Vertical, bounds, childBounds);
    }

    public void scrollVisible(Element child, Orientation orientation) {
        scrollVisible(child, orientation, getBounds(), child.getBounds());
    }

    private void scrollVisible(Element child, Orientation orientation, Rectangle bounds, Rectangle childBounds) {
        // if we're in bounds, we're done and don't need to check for a bar
        if (orientation.contains(bounds, childBounds))
            return;

        // sanity check: child may be too big
        if (orientation.getSize(childBounds) > orientation.getSize(bounds))
            throw new TwinException("Cannot scroll element " + child + " visible inside " + this
                    + " for orientation " + orientation + ": child is bigger than parent");

        ScrollBar bar = getScrollBar(orientation);
        if (bar == null)
            throw new TwinException("Cannot scroll element " + child + " visible inside " + this
                    + " for orientation " + orientation + ": no scrollbar");

        // test positions at 1.0 and 0.0, if neither match then we can use the results to interpolate
        bar.setPosition(1);
        Rectangle maxBounds = child.getBounds();
        if (orientation.contains(bounds, maxBounds)) // cheap client side check, maybe we're done
            return;

        bar.setPosition(0);
        Rectangle minBounds = child.getBounds();
        if (orientation.contains(bounds, minBounds)) // cheap client side check, maybe we're done
            return;

        // the pixel difference between max-scroll and min-scroll
        double scrollHeight = orientation.getMid(maxBounds) - orientation.getMid(minBounds);
        if (scrollHeight == 0) // scroll bar does nothing
            throw new TwinException("Cannot scroll element " + child + " visible inside " + this
                    + " for orientation " + orientation + ": it does not move in response to scrollbar action");

        // the difference between centre-of-child-at-minscroll and centre-of-this
        double offsetFromMinInPixels = orientation.getMid(bounds) - orientation.getMid(minBounds);
        // same thing, but scaled to scroll height
        double offsetFromMinAsFraction = offsetFromMinInPixels / scrollHeight;

        // clamp to [0,1] range
        if (offsetFromMinAsFraction < 0)
            offsetFromMinAsFraction = 0;
        if (offsetFromMinAsFraction > 1)
            offsetFromMinAsFraction = 1;

        bar.setPosition(offsetFromMinAsFraction);
        // now we should be done... maybe we should do some sanity check here
    }

    @Require(type = Menu.class)
    public MenuItem item(int index) throws TwinException {
        List<Element> elements = getDescendants(Criteria.type(MenuItem.class));
        return (MenuItem) elements.get(index);
    }

    @Require(type = Menu.class)
    public MenuItem item(String name) throws TwinException {
        return (MenuItem) getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name)));
    }

    @Require(type = MenuItem.class)
    public Menu openMenu() throws TwinException {
        click();
        try {
            return (Menu) waitForChild(Criteria.type(Menu.class), 1);
        } catch (TwinNoSuchElementException e) {
            Menu open = session.getOpenMenu();
            if (open == null)
                throw TwinError.NoSuchElement.create("Couldn't find child menu");
            return open;
        }
    }

    @Require(type = Window.class)
    public MenuItem menu(String name) throws TwinException {
        return (MenuItem) getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name)));
    }

    @Require(type = Window.class)
    public void close() throws TwinException {
        remote.session.request("DELETE", getPath(), null);
    }

    @Require(type = Window.class)
    public boolean isMaximized() throws TwinException {
        return "maximized"
                .equalsIgnoreCase((String) remote.session.request("GET", getPath() + "/window-state", null));
    }

    @Require(type = Window.class)
    public boolean isMinimized() throws TwinException {
        return "minimized"
                .equalsIgnoreCase((String) remote.session.request("GET", getPath() + "/window-state", null));
    }

    @Require(type = Window.class)
    public void maximize() throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("state", "maximized");
        remote.session.request("POST", getPath() + "/window-state", data);
    }

    @Require(type = Window.class)
    public void minimize() throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("state", "minimized");
        remote.session.request("POST", getPath() + "/window-state", data);
    }

    @Require(type = Window.class)
    public void restore() throws TwinException {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("state", "normal");
        remote.session.request("POST", getPath() + "/window-state", data);
    }
}