All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.machinepublishers.jbrowserdriver.ElementServer Maven / Gradle / Ivy

/* 
 * jBrowserDriver (TM)
 * Copyright (C) 2014-2016 Machine Publishers, LLC
 * 
 * Sales and support: [email protected]
 * Updates: https://github.com/MachinePublishers/jBrowserDriver
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.machinepublishers.jbrowserdriver;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.ElementNotVisibleException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.FindsByClassName;
import org.openqa.selenium.internal.FindsByCssSelector;
import org.openqa.selenium.internal.FindsById;
import org.openqa.selenium.internal.FindsByLinkText;
import org.openqa.selenium.internal.FindsByName;
import org.openqa.selenium.internal.FindsByTagName;
import org.openqa.selenium.internal.FindsByXPath;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.html.HTMLFormElement;
import org.w3c.dom.html.HTMLInputElement;
import org.w3c.dom.html.HTMLOptionElement;

import com.machinepublishers.jbrowserdriver.AppThread.Sync;
import com.machinepublishers.jbrowserdriver.Robot.MouseButton;

import javafx.stage.Stage;
import netscape.javascript.JSObject;

class ElementServer extends RemoteObject implements ElementRemote, WebElement,
    JavascriptExecutor, FindsById, FindsByClassName, FindsByLinkText, FindsByName,
    FindsByCssSelector, FindsByTagName, FindsByXPath {

  private static final String IS_VISIBLE;

  static {
    StringBuilder builder = new StringBuilder();
    builder.append("var me = this;");
    builder.append("(function(){");
    //The following JavaScript is Copyright 2011-2015 Software Freedom Conservancy and Copyright 2004-2011 Selenium committers.
    //Adapted and modified from https://github.com/SeleniumHQ/selenium/blob/master/javascript/selenium-core/scripts/selenium-api.js
    builder.append("var findEffectiveStyle = function(element) {");
    builder.append("  if (element.style == undefined) {");
    builder.append("    return undefined;");
    builder.append("  }");
    builder.append("  if (window.getComputedStyle) {");
    builder.append("    return window.getComputedStyle(element, null);");
    builder.append("  }");
    builder.append("  if (element.currentStyle) {");
    builder.append("    return element.currentStyle;");
    builder.append("  }");
    builder.append("  if (window.document.defaultView && window.document.defaultView.getComputedStyle) {");
    builder.append("    return window.document.defaultView.getComputedStyle(element, null);");
    builder.append("  }");
    builder.append("  return undefined;");
    builder.append("};");
    builder.append("var findEffectiveStyleProperty = function(element, property) {");
    builder.append("  var effectiveStyle = findEffectiveStyle(element);");
    builder.append("  var propertyValue = effectiveStyle[property];");
    builder.append("  if (propertyValue == 'inherit' && element.parentNode.style) {");
    builder.append("    return findEffectiveStyleProperty(element.parentNode, property);");
    builder.append("  }");
    builder.append("  return propertyValue;");
    builder.append("};");
    builder.append("var isDisplayed = function(element) {");
    builder.append("  var display = findEffectiveStyleProperty(element, \"display\");");
    builder.append("  if (display == \"none\") return false;");
    builder.append("  if (element.parentNode.style) {");
    builder.append("    return isDisplayed(element.parentNode);");
    builder.append("  }");
    builder.append("  return true;");
    builder.append("};");
    builder.append("var isVisible = function(element) {");
    builder.append("  if (element.tagName) {");
    builder.append("    var tagName = new String(element.tagName).toLowerCase();");
    builder.append("    if (tagName == \"input\") {");
    builder.append("      if (element.type) {");
    builder.append("        var elementType = new String(element.type).toLowerCase();");
    builder.append("        if (elementType == \"hidden\") {");
    builder.append("          return false;");
    builder.append("        }");
    builder.append("      }");
    builder.append("    }");
    builder.append("  }");
    builder.append("  var visibility = findEffectiveStyleProperty(element, \"visibility\");");
    builder.append("  return (visibility != \"hidden\" && isDisplayed(element));");
    builder.append("};");
    builder.append("return isVisible(me);");
    builder.append("})();");
    IS_VISIBLE = builder.toString();
  }

  private static final Pattern rgb = Pattern.compile(
      "rgb\\(([0-9]{1,3}), ([0-9]{1,3}), ([0-9]{1,3})\\)");
  private static final Map map = new HashMap();

  private final JSObject node;
  private final Context context;
  private final AtomicLong frameId = new AtomicLong();

  ElementServer(final JSObject node, final Context context) throws RemoteException {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(node, context.item());
            node.getMember("");
            return null;
          }
        });
    this.node = node;
    this.context = context;
  }

  JSObject node() {
    return node;
  }

  void setFrameId(long frameId) {
    this.frameId.set(frameId);
  }

  long frameId() {
    return frameId.get();
  }

  static ElementServer create(final Context context) {
    final JSObject doc = AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public JSObject perform() {
            JSObject node;
            ElementServer selectedFrame = context.item().selectedFrame();
            if (selectedFrame == null) {
              node = (JSObject) context.item().engine.get().getDocument();
            } else {
              node = selectedFrame.node;
            }
            return node;
          }
        });
    try {
      return new ElementServer(doc, context);
    } catch (RemoteException e) {
      Util.handleException(e);
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void activate() {
    context.item().selectFrame(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void scriptParam(ElementId id) {
    synchronized (map) {
      map.put(id, this);
    }
  }

  private static void validate(JSObject node, ContextItem contextItem) {
    if (node == null) {
      throw new NoSuchElementException("Element not found or does not exist.");
    }
    JSObject doc = node instanceof Document ? node : (JSObject) ((Node) node).getOwnerDocument();
    if (!contextItem.containsFrame(doc)) {
      throw new StaleElementReferenceException("The page containing the element no longer exists.");
    }
    if (!(Boolean) doc.call("contains", node)) {
      throw new StaleElementReferenceException("The element no longer exists within the page.");
    }
  }

  private void validate(boolean mustBeVisible) {
    validate(node, context.item());
    if (mustBeVisible && !isDisplayed()) {
      throw new ElementNotVisibleException("Element is not visible.");
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void click() {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(false);
            node.call("scrollIntoView");
            if (context.keyboard.get().isShiftPressed()) {
              node.eval(
                  new StringBuilder()
                      .append("this.origOnclick = this.onclick;")
                      .append("this.onclick=function(event){")
                      .append("  this.target='_blank';")
                      .append("  if(event){")
                      .append("    if(event.stopPropagation){")
                      .append("      event.stopPropagation();")
                      .append("    }")
                      .append("  }")
                      .append("  if(this.origOnclick){")
                      .append("    this.origOnclick(event? event: null);")
                      .append("  }")
                      .append("  this.onclick = this.origOnclick;")
                      .append("};").toString());
            }
            return null;
          }
        });

    if (node instanceof HTMLOptionElement) {
      AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
          new Sync() {
            @Override
            public Object perform() {
              validate(false);
              try {
                new ElementServer((JSObject) ((HTMLOptionElement) node).getParentNode(), context).click();
              } catch (RemoteException e) {
                Util.handleException(e);
              }
              int index = ((HTMLOptionElement) node).getIndex();
              for (int i = 0; i <= index; i++) {
                context.robot.get().keysType(Keys.DOWN);
              }
              context.robot.get().keysType(Keys.SPACE);
              return null;
            }
          });
    } else {
      AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
          new Sync() {
            @Override
            public Object perform() {
              validate(true);
              final JSObject obj = (JSObject) node.call("getBoundingClientRect");
              final double top = Double.parseDouble(obj.getMember("top").toString());
              final double left = Double.parseDouble(obj.getMember("left").toString());
              final double bottom = Double.parseDouble(obj.getMember("bottom").toString());
              final double right = Double.parseDouble(obj.getMember("right").toString());
              double clickX = (left + right) / 2d;
              double clickY = (top + bottom) / 2d;
              ElementServer doc = ElementServer.create(context);
              if (!node.equals(doc.node.eval(
                  "(function(){return document.elementFromPoint(" + clickX + "," + clickY + ");})();"))) {
                final Stage stage = context.item().stage.get();
                final int minX = Math.max(0, (int) Math.floor(left));
                final int maxX = Math.min((int) Math.ceil(stage.getScene().getWidth()), (int) Math.ceil(right));
                final int minY = Math.max(0, (int) Math.floor(top));
                final int maxY = Math.min((int) Math.ceil(stage.getScene().getHeight()), (int) Math.ceil(bottom));
                final int incX = (int) Math.max(1, .05d * (double) (maxX - minX));
                final int incY = (int) Math.max(1, .05d * (double) (maxY - minY));
                for (int x = minX; x <= maxX; x += incX) {
                  boolean found = false;
                  for (int y = minY; y <= maxY; y += incY) {
                    if (node.equals(doc.node.eval(
                        "(function(){return document.elementFromPoint(" + x + "," + y + ");})();"))) {
                      clickX = x;
                      clickY = y;
                      found = true;
                      break;
                    }
                  }
                  if (found) {
                    break;
                  }
                }
              }
              final org.openqa.selenium.Point frameLocation = context.item().selectedFrameLocation();
              context.robot.get().mouseMove(clickX + frameLocation.getX(), clickY + frameLocation.getY());
              context.robot.get().mouseClick(MouseButton.LEFT);
              return null;
            }
          });
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void submit() {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(false);
            context.item().httpListener.get().resetStatusCode();
            if (node instanceof HTMLInputElement) {
              ((HTMLInputElement) node).getForm().submit();
            } else if (node instanceof HTMLFormElement) {
              ((HTMLFormElement) node).submit();
            }
            return null;
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void sendKeys(final CharSequence... keys) {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(true);
            node.call("scrollIntoView");
            node.call("focus");
            return null;
          }
        });
    final boolean fileChooser = node instanceof HTMLInputElement && "file".equalsIgnoreCase(getAttribute("type"));
    if (fileChooser) {
      click();
    }
    context.robot.get().keysType(keys);
    if (fileChooser) {
      context.robot.get().typeEnter();
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void clear() {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(false);
            context.item().httpListener.get().resetStatusCode();
            node.call("scrollIntoView");
            node.call("focus");
            node.eval("this.value='';");
            return null;
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getAttribute(final String attrName) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public String perform() {
            validate(false);
            Object obj = node.getMember(attrName);
            if (obj != null) {
              String str = obj.toString();
              if (!StringUtils.isEmpty(str) && !"undefined".equals(str)) {
                return str;
              }
            }

            obj = executeScript(new StringBuilder()
                .append("return this.getAttribute('").append(attrName).append("');").toString());
            if (obj != null) {
              String str = obj.toString();
              if (!StringUtils.isEmpty(str)) {
                return str;
              }
            }

            return null;
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getCssValue(final String name) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public String perform() {
            validate(false);
            return cleanUpCssVal((String) (node.eval(new StringBuilder()
                .append("var me = this;")
                .append("(function(){")
                .append("  return window.getComputedStyle(me).getPropertyValue('")
                .append(name)
                .append("');")
                .append("})();").toString())));
          }
        });
  }

  private static String cleanUpCssVal(String rgbStr) {
    if (rgbStr != null) {
      Matcher matcher = rgb.matcher(rgbStr);
      if (matcher.matches()) {
        return new StringBuilder().append("rgba(").append(matcher.group(1)).append(", ")
            .append(matcher.group(2)).append(", ").append(matcher.group(3)).append(", 1)").toString();
      }
    }
    return rgbStr == null ? "" : rgbStr;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Point remoteGetLocation() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Point perform() {
            validate(true);
            JSObject obj = (JSObject) node.call("getBoundingClientRect");
            int y = (int) Math.rint(Double.parseDouble(obj.getMember("top").toString()));
            int x = (int) Math.rint(Double.parseDouble(obj.getMember("left").toString()));
            return new Point(x + 1, y + 1);
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public org.openqa.selenium.Point getLocation() {
    return remoteGetLocation().toSelenium();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Dimension remoteGetSize() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Dimension perform() {
            validate(true);
            JSObject obj = (JSObject) node.call("getBoundingClientRect");
            int y = (int) Math.rint(Double.parseDouble(obj.getMember("top").toString()));
            int y2 = (int) Math.rint(Double.parseDouble(obj.getMember("bottom").toString()));
            int x = (int) Math.rint(Double.parseDouble(obj.getMember("left").toString()));
            int x2 = (int) Math.rint(Double.parseDouble(obj.getMember("right").toString()));
            return new Dimension(x2 - x, y2 - y);
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public org.openqa.selenium.Dimension getSize() {
    return remoteGetSize().toSelenium();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Rectangle remoteGetRect() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Rectangle perform() {
            validate(true);
            JSObject obj = (JSObject) node.call("getBoundingClientRect");
            int y = (int) Math.rint(Double.parseDouble(obj.getMember("top").toString()));
            int y2 = (int) Math.rint(Double.parseDouble(obj.getMember("bottom").toString()));
            int x = (int) Math.rint(Double.parseDouble(obj.getMember("left").toString()));
            int x2 = (int) Math.rint(Double.parseDouble(obj.getMember("right").toString()));
            return new Rectangle(x + 1, y + 1, y2 - y, x2 - x);
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public org.openqa.selenium.Rectangle getRect() {
    return remoteGetRect().toSelenium();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getTagName() {
    return getAttribute("tagName").toLowerCase();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getText() {
    return AppThread.exec(context.statusCode,
        new Sync() {
          @Override
          public String perform() {
            validate(false);
            if ((Boolean) node.eval(IS_VISIBLE)) {
              Object text = node.getMember("innerText");
              return text instanceof String ? ((String) text).trim() : "";
            }
            return "";
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDisplayed() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Boolean perform() {
            validate(false);
            return (Boolean) node.eval(IS_VISIBLE);
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isEnabled() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Boolean perform() {
            validate(false);
            String val = node.getMember("disabled").toString();
            return val == null || "undefined".equals(val) || val.isEmpty() || "false".equals(val);
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isSelected() {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Boolean perform() {
            validate(false);
            String selected = node.getMember("selected").toString();
            String checked = node.getMember("checked").toString();
            return (selected != null && !"undefined".equals(selected) && !"false".equals(selected) && !selected.isEmpty())
                || (checked != null && !"undefined".equals(checked) && !"false".equals(checked) && !checked.isEmpty());
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElement(By by) {
    return (ElementServer) by.findElement(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElements(By by) {
    return by.findElements(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByXPath(final String expr) {
    List list = findElementsByXPath(expr);
    return list.isEmpty() ? null : (ElementServer) list.get(0);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByXPath(final String expr) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync>() {
          @Override
          public List perform() {
            validate(false);
            return asList(executeScript(new StringBuilder()
                .append("var iter = ")
                .append("  document.evaluate(arguments[0], arguments[1], null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);")
                .append("var items = [];")
                .append("var cur = null;")
                .append("while(cur = iter.iterateNext()){")
                .append("  items.push(cur);")
                .append("}")
                .append("return items;").toString(), expr, node));
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByTagName(String tagName) {
    List list = byTagName(tagName);
    return list == null || list.isEmpty() ? null : list.get(0);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByTagName(String tagName) {
    return byTagName(tagName);
  }

  private List byTagName(final String tagName) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync>() {
          @Override
          public List perform() {
            validate(false);
            if (node != null) {
              return asList(parseScriptResult(
                  node.call("getElementsByTagName", new Object[] { tagName })));
            }
            return new ArrayList();
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByCssSelector(final String expr) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public ElementServer perform() {
            validate(false);
            JSObject result = (JSObject) node.call("querySelector", new Object[] { expr });
            if (result == null) {
              return null;
            }
            try {
              return new ElementServer(result, context);
            } catch (RemoteException e) {
              Util.handleException(e);
              return null;
            }
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByCssSelector(final String expr) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync>() {
          @Override
          public List perform() {
            validate(false);
            List elements = new ArrayList();
            JSObject result = (JSObject) node.call("querySelectorAll", new Object[] { expr });
            for (int i = 0;; i++) {
              Object cur = result.getSlot(i);
              if (cur instanceof Node) {
                try {
                  elements.add(new ElementServer((JSObject) cur, context));
                } catch (RemoteException e) {
                  Util.handleException(e);
                }
              } else {
                break;
              }
            }
            return elements;
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByName(String name) {
    return findElementByCssSelector(new StringBuilder().append("*[name='").append(name).append("']").toString());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByName(String name) {
    return findElementsByCssSelector(new StringBuilder().append("*[name='").append(name).append("']").toString());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByLinkText(final String text) {
    List list = byLinkText(text, false, false);
    return list.isEmpty() ? null : list.get(0);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByPartialLinkText(String text) {
    List list = byLinkText(text, false, true);
    return list.isEmpty() ? null : list.get(0);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByLinkText(String text) {
    return byLinkText(text, true, false);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByPartialLinkText(String text) {
    return byLinkText(text, true, true);
  }

  private List byLinkText(final String text,
      final boolean multiple, final boolean partial) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync>() {
          @Override
          public List perform() {
            validate(false);
            List elements = new ArrayList();
            List nodes = (List) findElementsByTagName("a");
            for (ElementServer cur : nodes) {
              String curText = cur.getText();
              if (curText == null) {
                continue;
              }
              if ((partial && curText.contains(text))
                  || (!partial && curText.equals(text))) {
                elements.add(cur);
                if (!multiple) {
                  break;
                }
              }
            }
            return elements;
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementByClassName(String cssClass) {
    List list = byCssClass(cssClass);
    return list.isEmpty() ? null : list.get(0);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsByClassName(String cssClass) {
    return byCssClass(cssClass);
  }

  private List byCssClass(String cssClass) {
    return asList(executeScript(
        new StringBuilder().append("return this.getElementsByClassName('").append(cssClass).append("');").toString()));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ElementServer findElementById(final String id) {
    return findElementByCssSelector(new StringBuilder("*[id='").append(id).append("']").toString());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List findElementsById(String id) {
    return findElementsByCssSelector(new StringBuilder().append("*[id='").append(id).append("']").toString());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Object executeAsyncScript(final String script, final Object... args) {
    final JavascriptNames jsNames = new JavascriptNames();
    script(true, script, args, jsNames);
    int sleep = 1;
    final int sleepBackoff = 2;
    final int sleepMax = 500;
    while (true) {
      sleep = sleep < sleepMax ? sleep * sleepBackoff : sleep;
      try {
        Thread.sleep(sleep);
      } catch (InterruptedException e) {}
      Object result = AppThread.exec(
          context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
          new Sync() {
            @Override
            public Object perform() {
              validate(false);
              try {
                return node.eval(new StringBuilder()
                    .append("(function(){return this.")
                    .append(jsNames.callbackVal)
                    .append(";})();").toString());
              } finally {
                node.eval(new StringBuilder()
                    .append("delete ")
                    .append(jsNames.callbackVal)
                    .append(";").toString());
              }
            }
          });
      if (!(result instanceof String) || !"undefined".equals(result.toString())) {
        Object parsed = parseScriptResult(result);
        if (parsed instanceof List) {
          if (((List) parsed).size() == 0) {
            return null;
          }
          if (((List) parsed).size() == 1) {
            return ((List) parsed).get(0);
          }
        }
        return parsed;
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Object executeScript(final String script, final Object... args) {
    return script(false, script, args, new JavascriptNames());
  }

  private static List asList(Object objToCast) {
    try {
      return (List) objToCast;
    } catch (ClassCastException e) {
      return new ArrayList();
    }
  }

  private static class JavascriptNames {
    private final String callbackVal = Util.randomPropertyName();
    private final String callback = Util.randomPropertyName();
    private final String exec = Util.randomPropertyName();
  }

  private Object script(boolean callback, String script, Object[] args, final JavascriptNames jsNames) {
    for (int i = 0; args != null && i < args.length; i++) {
      if (args[i] instanceof ElementId) {
        synchronized (map) {
          args[i] = ((ElementServer) map.remove(args[i])).node;
        }
      }
    }
    return parseScriptResult(AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(false);
            List argList = new ArrayList();
            if (args != null) {
              argList.addAll(Arrays.asList(args));
            }
            try {
              if (callback) {
                argList.add(null);
                node.eval(new StringBuilder().append("(function(){")
                    .append("this.").append(jsNames.callback).append(" = function(){")
                    .append(jsNames.callbackVal).append(" = arguments && arguments.length > 0? arguments[0] : null;")
                    .append("}")
                    .append("}).apply(this);")
                    .append("this.").append(jsNames.exec).append(" = function(){")
                    .append("arguments[arguments.length-1] = this.").append(jsNames.callback).append(";")
                    .append("return (function(){").append(script).append("}).apply(this, arguments);")
                    .append("};").toString());
              } else {
                node.eval(new StringBuilder().append("this.").append(jsNames.exec).append(" = function(){")
                    .append("return (function(){").append(script).append("}).apply(this, arguments);")
                    .append("};").toString());
              }
              return node.call(jsNames.exec, argList.toArray(new Object[0]));
            } catch (Throwable t) {
              return t;
            } finally {
              node.eval(new StringBuilder().append("delete ").append("this.").append(jsNames.exec).append(";").toString());
              if (callback) {
                node.eval(new StringBuilder().append("delete ").append("this.").append(jsNames.callback).append(";").toString());
              }
            }
          }
        }));
  }

  private Object parseScriptResult(final Object obj) {
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Object perform() {
            validate(false);
            AppThread.handleExecutionException(obj);
            if (obj == null || (obj instanceof String && "undefined".equals(obj.toString()))) {
              return null;
            }
            if (obj instanceof Node) {
              try {
                return new ElementServer((JSObject) obj, context);
              } catch (RemoteException e) {
                Util.handleException(e);
                return null;
              }
            }
            if (obj instanceof JSObject) {
              List list = new ArrayList();
              boolean isList = false;
              for (int i = 0;; i++) {
                Object cur = ((JSObject) obj).getSlot(i);
                if (cur instanceof String && "undefined".equals(cur.toString())) {
                  break;
                }
                isList = true;
                list.add(parseScriptResult(cur));
              }
              if (isList) {
                return list;
              }
              if ("function".equals(executeScript("return typeof arguments[0];", obj))) {
                return obj.toString();
              }
              if (Boolean.TRUE.equals(executeScript("return Array.isArray(arguments[0]);", obj))) {
                return new ArrayList();
              }
              List mapAsList = (List) executeScript(new StringBuilder()
                  .append("var list = [];")
                  .append("for(var propertyName in arguments[0]){")
                  .append("list.push(propertyName);")
                  .append("var val = arguments[0][propertyName];")
                  .append("list.push(val === undefined? null : val);")
                  .append("}")
                  .append("return list.length > 0? list : undefined;").toString(),
                  obj);
              //TODO ES6 will support Symbol keys
              Map map = new LinkedHashMap();
              for (int i = 0; mapAsList != null && i < mapAsList.size(); i += 2) {
                map.put(mapAsList.get(i).toString(), mapAsList.get(i + 1));
              }
              return map;
            }
            if (obj instanceof Boolean || obj instanceof Long || obj instanceof Double) {
              return obj;
            }
            if (obj instanceof Integer) {
              return new Long((Integer) obj);
            }
            if (obj instanceof Float) {
              return new Double((Float) obj);
            }
            return obj.toString();
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Point locate() {
    AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Point perform() {
            validate(false);
            node.call("scrollIntoView");
            return null;
          }
        });
    return AppThread.exec(context.statusCode, context.timeouts.get().getScriptTimeoutMS(),
        new Sync() {
          @Override
          public Point perform() {
            validate(true);
            JSObject obj = (JSObject) node.call("getBoundingClientRect");
            double y = Double.parseDouble(obj.getMember("top").toString());
            double x = Double.parseDouble(obj.getMember("left").toString());
            y = y < 0d ? 0d : y;
            x = x < 0d ? 0d : x;
            final org.openqa.selenium.Point frameLocation = context.item().selectedFrameLocation();
            return new Point((int) Math.rint(x) + 1 + frameLocation.getX(),
                (int) Math.rint(y) + 1 + frameLocation.getY());
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public  X getScreenshotAs(OutputType arg0) throws WebDriverException {
    LogsServer.instance().warn("Screenshot not supported on jBrowserDriver WebElements");
    return null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public byte[] getScreenshot() throws WebDriverException {
    LogsServer.instance().warn("Screenshot not supported on jBrowserDriver WebElements");
    return null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int remoteHashCode() {
    return AppThread.exec(
        context.statusCode,
        new Sync() {
          @Override
          public Integer perform() {
            validate(false);
            return node.hashCode();
          }
        });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean remoteEquals(ElementId id) {
    return AppThread.exec(
        context.statusCode,
        new Sync() {
          @Override
          public Boolean perform() {
            validate(false);
            ElementServer other;
            synchronized (map) {
              other = map.remove(id);
            }
            other.validate(false);
            return node.equals(other.node);
          }
        });
  }

}