/*
GNU LESSER GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: lobochief@users.sourceforge.net
*/
/*
* Created on Nov 12, 2005
*/
package org.lobobrowser.html.js;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Timer;
import org.lobobrowser.html.HtmlRendererContext;
import org.lobobrowser.html.domimpl.CanvasPath2D;
import org.lobobrowser.html.domimpl.CommentImpl;
import org.lobobrowser.html.domimpl.HTMLDivElementImpl;
import org.lobobrowser.html.domimpl.HTMLDocumentImpl;
import org.lobobrowser.html.domimpl.HTMLElementImpl;
import org.lobobrowser.html.domimpl.HTMLIFrameElementImpl;
import org.lobobrowser.html.domimpl.HTMLImageElementImpl;
import org.lobobrowser.html.domimpl.HTMLOptionElementImpl;
import org.lobobrowser.html.domimpl.HTMLScriptElementImpl;
import org.lobobrowser.html.domimpl.HTMLSelectElementImpl;
import org.lobobrowser.html.domimpl.NodeImpl;
import org.lobobrowser.html.domimpl.TextImpl;
import org.lobobrowser.js.AbstractScriptableDelegate;
import org.lobobrowser.js.HideFromJS;
import org.lobobrowser.js.JavaClassWrapper;
import org.lobobrowser.js.JavaClassWrapperFactory;
import org.lobobrowser.js.JavaInstantiator;
import org.lobobrowser.js.JavaObjectWrapper;
import org.lobobrowser.js.JavaScript;
import org.lobobrowser.ua.UserAgentContext;
import org.lobobrowser.ua.UserAgentContext.Request;
import org.lobobrowser.ua.UserAgentContext.RequestKind;
import org.lobobrowser.util.ID;
import org.mozilla.javascript.ClassShutter;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.css.CSS2Properties;
import org.w3c.dom.events.EventException;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLCollection;
import org.w3c.dom.html.HTMLElement;
import org.w3c.dom.views.AbstractView;
import org.w3c.dom.views.DocumentView;
public class Window extends AbstractScriptableDelegate implements AbstractView, EventTarget {
private static final Storage STORAGE = new Storage();
private static final Logger logger = Logger.getLogger(Window.class.getName());
private static final Map<HtmlRendererContext, WeakReference<Window>> CONTEXT_WINDOWS = new WeakHashMap<>();
// private static final JavaClassWrapper IMAGE_WRAPPER =
// JavaClassWrapperFactory.getInstance().getClassWrapper(Image.class);
private static final JavaClassWrapper XMLHTTPREQUEST_WRAPPER = JavaClassWrapperFactory.getInstance()
.getClassWrapper(XMLHttpRequest.class);
private static final JavaClassWrapper PATH2D_WRAPPER = JavaClassWrapperFactory.getInstance()
.getClassWrapper(CanvasPath2D.class);
private static final JavaClassWrapper EVENT_WRAPPER = JavaClassWrapperFactory.getInstance()
.getClassWrapper(Event.class);
// Timer ids should begin counting from 1 or more.
// jQuery's ajax polling handler relies on a non-zero value (uses it as a boolean condition)
// Chromium 37 starts counting from 1 while Firefox 32 starts counting from 2 (from developer consoles and plugins installed)
private static int timerIdCounter = 1;
private final HtmlRendererContext rcontext;
private final UserAgentContext uaContext;
private Navigator navigator;
private Screen screen;
private Location location;
private Map<Integer, TaskWrapper> taskMap;
private volatile Document document;
// private volatile HTMLDocumentImpl document;
public Window(final HtmlRendererContext rcontext, final UserAgentContext uaContext) {
// TODO: Probably need to create a new Window instance
// for every document. Sharing of Window state between
// different documents is not correct.
this.rcontext = rcontext;
this.uaContext = uaContext;
}
private static int generateTimerID() {
synchronized (logger) {
return timerIdCounter++;
}
}
public HtmlRendererContext getHtmlRendererContext() {
return this.rcontext;
}
public UserAgentContext getUserAgentContext() {
return this.uaContext;
}
private void clearState() {
synchronized (this) {
// windowClosing = true;
if (document instanceof HTMLDocumentImpl) {
((HTMLDocumentImpl) document).stopEverything();
}
jsScheduler.stopAndWindUp(true);
jsScheduler = new JSScheduler(this);
eventTargetManager.reset();
this.onWindowLoadHandler = null;
this.forgetAllTasks();
// Commenting out call to getWindowScope() since that creates a new scope which is wasteful
// if we are going to destroy it anyway.
// final Scriptable s = this.getWindowScope();
final Scriptable s = this.windowScope;
if (s != null) {
final Object[] ids = s.getIds();
for (final Object id : ids) {
if (id instanceof String) {
s.delete((String) id);
} else if (id instanceof Integer) {
s.delete(((Integer) id).intValue());
}
}
}
// This will ensure that a fresh scope will be created by getWindowScope() on the next call
this.windowScope = null;
}
}
@HideFromJS
public void setDocument(final Document document) {
synchronized (this) {
final Document prevDocument = this.document;
if (prevDocument != document) {
final Function onunload = this.onunload;
if (onunload != null) {
final HTMLDocumentImpl oldDoc = (HTMLDocumentImpl) prevDocument;
Executor.executeFunction(this.getWindowScope(), onunload, oldDoc.getDocumentURL(), this.uaContext, windowContextFactory);
this.onunload = null;
}
// TODO: Should clearing of the state be done when window "unloads"?
if (prevDocument != null) {
// Only clearing when the previous document was not null
// because state might have been set on the window before
// the very first document is added.
this.clearState();
}
// this.forgetAllTasks();
this.initWindowScope(document);
jobsOver.set(false);
jsScheduler.start();
this.document = document;
// eventTargetManager.setNode(document);
}
}
}
public DocumentView getDocument() {
return (DocumentView) this.document;
}
public Document getDocumentNode() {
return this.document;
}
private abstract static class JSTask implements Comparable<JSTask> {
protected final int priority;
protected final long creationTime;
protected final String description;
private final AccessControlContext context;
// TODO: Add a context parameter that will be combined with current context, to help with creation of timer tasks
// public JSTask(final int priority, final Runnable runnable) {
public JSTask(final int priority, final String description) {
this.priority = priority;
this.description = description;
this.context = AccessController.getContext();
this.creationTime = System.nanoTime();
}
// TODO: Add a way to stop a task. It should return false if the task can't be stopped in which case a thread kill will be performed by the task scheduler.
// TODO: Sorting by priority
public int compareTo(final JSTask o) {
final long diffCreation = (o.creationTime - creationTime);
if (diffCreation < 0) {
return 1;
} else if (diffCreation == 0) {
return 0;
} else {
return -1;
}
}
public abstract void run();
}
public final static class JSRunnableTask extends JSTask {
private final Runnable runnable;
public JSRunnableTask(final int priority, final Runnable runnable) {
this(priority, "", runnable);
}
public JSRunnableTask(final int priority, final String description, final Runnable runnable) {
super(priority, description);
this.runnable = runnable;
}
@Override
public String toString() {
// return "JSRunnableTask [priority=" + priority + ", runnable=" + runnable + ", creationTime=" + creationTime + "]";
return "JSRunnableTask [priority=" + priority + ", description=" + description + ", creationTime=" + creationTime + "]";
}
@Override
public void run() {
runnable.run();
}
}
public final static class JSSupplierTask<T> extends JSTask {
private final Supplier<T> supplier;
private final Consumer<T> consumer;
public JSSupplierTask(final int priority, final Supplier<T> supplier, final Consumer<T> consumer) {
super(priority, "supplier description TODO");
this.supplier = supplier;
this.consumer = consumer;
}
@Override
public void run() {
final T result = supplier.get();
consumer.accept(result);
}
}
private static final int JS_SCHED_POLL_INTERVAL_MILLIS = 100;
private static final int JS_SCHED_JOIN_INTERVAL_MILLIS = JS_SCHED_POLL_INTERVAL_MILLIS * 2;
private static final class JSScheduler extends Thread {
private static final class ScheduledTask implements Comparable<ScheduledTask> {
final int id;
final JSTask task;
public ScheduledTask(final int id, final JSTask task) {
this.id = id;
this.task = task;
}
public int compareTo(final ScheduledTask other) {
return task.compareTo(other.task);
}
@Override
public boolean equals(final Object o) {
if (o instanceof Integer) {
final Integer oId = (Integer) o;
return oId == id;
}
return false;
}
@Override
public String toString() {
return "Scheduled Task (" + id + ", " + task + ")";
}
}
private final PriorityBlockingQueue<ScheduledTask> jsQueue = new PriorityBlockingQueue<>();
private final AtomicBoolean running = new AtomicBoolean(false);
private volatile boolean windowClosing = false;
// TODO: This is not water tight for one reason, Windows are reused for different documents.
// If they are always freshly created, the taskIdCounter will be more reliable.
private volatile AtomicInteger taskIdCounter = new AtomicInteger(0);
private String name;
public JSScheduler(final Window window) {
super("JS Scheduler");
this.name = "JS Sched " + (window.document == null ? "" : "" + window.document.getBaseURI());
}
@Override
public void run() {
setName(name);
while (!windowClosing) {
try {
ScheduledTask scheduledTask;
// TODO: uncomment if synchronization is necessary with the add methods
// synchronized (this) {
scheduledTask = jsQueue.poll(JS_SCHED_POLL_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
if (scheduledTask != null) {
final PrivilegedAction<Object> action = new PrivilegedAction<Object>() {
public Object run() {
// System.out.println("In " + window.document.getBaseURI() + "\n Running task: " + scheduledTask);
// System.out.println("In " + name + "\n Running task: " + scheduledTask);
running.set(true);
scheduledTask.task.run();
// System.out.println("Done task: " + scheduledTask);
// System.out.println(" Remaining tasks: " + jsQueue.size());
return null;
}
};
AccessController.doPrivileged(action, scheduledTask.task.context);
}
} catch (final InterruptedException e) {
final int queueSize = jsQueue.size();
if (queueSize > 0) {
System.err.println("JS Scheduler was interrupted. Tasks remaining: " + jsQueue.size());
}
} catch (final Exception e) {
e.printStackTrace();
} catch (final WindowClosingError wce) {
// Javascript context detected a request for closing and bailed out.
assert(windowClosing);
} finally {
running.set(false);
}
}
// System.out.println("Exiting loop\n\n");
}
public void stopAndWindUp(final boolean blocking) {
// System.out.println("Going to stop JS scheduler");
windowClosing = true;
/* TODO: If the thread refuses to join(), perhaps the thread could be interrupted and stopped in
* the catch block of join() below. This could be done immediately, or scheduled for a stopping
* in a separate collector Thread
* */
// this.interrupt();
if (blocking) {
try {
this.join(JS_SCHED_JOIN_INTERVAL_MILLIS);
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
/*
this.stop();
*/
}
public boolean isWindowClosing() {
return windowClosing;
}
public void addJSTask(final JSTask task) {
// synchronized (this) {
jsQueue.add(new ScheduledTask(0, task));
// }
}
public int addUniqueJSTask(final int oldId, final JSTask task) {
// synchronized (this) {
if (oldId != -1) {
if (jsQueue.contains(oldId)) {
return oldId;
}
/*
for (ScheduledTask t : jsQueue) {
if (t.id == oldId) {
// Task found
return oldId;
}
}*/
}
final int newId = taskIdCounter.addAndGet(1);
jsQueue.add(new ScheduledTask(newId, task));
return newId;
// }
}
public boolean hasPendingTasks() {
return (!jsQueue.isEmpty()) || running.get();
}
}
private URL getCurrURL() {
try {
return new URL(rcontext.getCurrentURL());
} catch (MalformedURLException e) {
return null;
}
}
private volatile JSScheduler jsScheduler = new JSScheduler(this);
@HideFromJS
public void addJSTask(final JSTask task) {
/*
final URL urlContext = new URL(rcontext.getCurrentURL());
if (document != null) {
final URL urlDoc = document.getDocumentURL();
if (!urlDoc.equals(urlContext)) {
throw new RuntimeException(String.format("doc url(%s) is different from context url (%s)", urlDoc, urlContext));
}
}*/
final URL urlContext = getCurrURL();
if (urlContext != null) {
if (uaContext.isRequestPermitted(new Request(urlContext, RequestKind.JavaScript))) {
// System.out.println("Adding task: " + task);
synchronized (this) {
jsScheduler.addJSTask(task);
}
}
} else {
// TODO: This happens when the URL is not accepted by okhttp
System.out.println("Not adding task because url context is null");
}
}
// TODO: Also look at GH #149
// TODO: Try to refactor this so that all tasks are checked here rather than in caller
// TODO: Some tasks are added unchecked for various reasons that need to be reviewed:
// 1. Timer task. The logic is that a script that was permitted to create the timer already has the permission to execute it.
// But it would be better if their permission is checked again to account for subsequent changes through RequestManager,
// or if RequestManager assures that page is reloaded for *any* permission change.
// 2. Event listeners. Logic is similar to Timer task
// 3. Script elements. They are doing the checks themselves, but it would better to move the check here.
// 4. XHR handler. Logic similar to timer task.
@HideFromJS
public void addJSTaskUnchecked(final JSTask task) {
// System.out.println("Adding task: " + task);
synchronized (this) {
jsScheduler.addJSTask(task);
}
}
@HideFromJS
public int addJSUniqueTask(final int oldId, final JSTask task) {
System.out.println("Adding unique task: " + task);
synchronized (this) {
return jsScheduler.addUniqueJSTask(oldId, task);
}
}
private void putAndStartTask(final Integer timeoutID, final Timer timer, final Object retained) {
TaskWrapper oldTaskWrapper = null;
synchronized (this) {
Map<Integer, TaskWrapper> taskMap = this.taskMap;
if (taskMap == null) {
taskMap = new HashMap<>(4);
this.taskMap = taskMap;
} else {
oldTaskWrapper = taskMap.get(timeoutID);
}
taskMap.put(timeoutID, new TaskWrapper(timer, retained));
}
// Do this outside synchronized block, just in case.
if (oldTaskWrapper != null) {
oldTaskWrapper.timer.stop();
}
timer.start();
}
private void forgetTask(final Integer timeoutID, final boolean cancel) {
TaskWrapper oldTimer = null;
synchronized (this) {
final Map<Integer, TaskWrapper> taskMap = this.taskMap;
if (taskMap != null) {
oldTimer = taskMap.remove(timeoutID);
}
}
if ((oldTimer != null) && cancel) {
oldTimer.timer.stop();
}
}
private void forgetAllTasks() {
TaskWrapper[] oldTaskWrappers = null;
synchronized (this) {
final Map<Integer, TaskWrapper> taskMap = this.taskMap;
if (taskMap != null) {
oldTaskWrappers = taskMap.values().toArray(new TaskWrapper[0]);
this.taskMap = null;
}
}
if (oldTaskWrappers != null) {
for (final TaskWrapper taskWrapper : oldTaskWrappers) {
taskWrapper.timer.stop();
}
}
}
// private Timer getTask(Long timeoutID) {
// synchronized(this) {
// Map taskMap = this.taskMap;
// if(taskMap != null) {
// return (Timer) taskMap.get(timeoutID);
// }
// }
// return null;
// }
/**
* @param aFunction
* Javascript function to invoke on each loop.
* @param aTimeInMs
* Time in millisecund between each loop. TODO: Can this be converted
* to long type?
* @return Return the timer ID to use as reference
* @see <a
* href="http://developer.mozilla.org/en/docs/DOM:window.setInterval">Window.setInterval
* interface definition</a>
* @todo Make proper and refactore with
* {@link Window#setTimeout(Function, double)}.
*/
public int setInterval(final Function aFunction, final double aTimeInMs) {
if ((aTimeInMs > Integer.MAX_VALUE) || (aTimeInMs < 0)) {
throw new IllegalArgumentException("Timeout value " + aTimeInMs + " is not supported.");
}
final int timeID = generateTimerID();
System.out.println("Created interval timer: " + timeID);
final Integer timeIDInt = new Integer(timeID);
final ActionListener task = new FunctionTimerTask(this, timeIDInt, aFunction, false);
int t = (int) aTimeInMs;
if (t < 1) {
t = 1;
}
final Timer timer = new Timer(t, task);
timer.setRepeats(true); // The only difference with setTimeout
this.putAndStartTask(timeIDInt, timer, aFunction);
return timeID;
}
/**
* @param aExpression
* Javascript expression to invoke on each loop.
* @param aTimeInMs
* Time in millisecund between each loop.
* @return Return the timer ID to use as reference
* @see <a
* href="http://developer.mozilla.org/en/docs/DOM:window.setInterval">Window.setInterval
* interface definition</a>
* @todo Make proper and refactore with
* {@link Window#setTimeout(String, double)}.
*/
public int setInterval(final String aExpression, final double aTimeInMs) {
if ((aTimeInMs > Integer.MAX_VALUE) || (aTimeInMs < 0)) {
throw new IllegalArgumentException("Timeout value " + aTimeInMs + " is not supported.");
}
final int timeID = generateTimerID();
final Integer timeIDInt = new Integer(timeID);
final ActionListener task = new ExpressionTimerTask(this, timeIDInt, aExpression, false);
int t = (int) aTimeInMs;
if (t < 1) {
t = 1;
}
final Timer timer = new Timer(t, task);
timer.setRepeats(false); // The only difference with setTimeout
this.putAndStartTask(timeIDInt, timer, null);
return timeID;
}
/**
* @param aTimerID
* Timer ID to stop.
* @see <a
* href="http://developer.mozilla.org/en/docs/DOM:window.clearInterval">Window.clearInterval
* interface Definition</a>
*/
public void clearInterval(final int aTimerID) {
final Integer key = new Integer(aTimerID);
this.forgetTask(key, true);
}
public void clearInterval(final Object unused) {
// Happens when jQuery calls this with a null parameter;
// TODO: Check if there are other cases
if (unused instanceof Integer) {
final Integer id = (Integer) unused;
clearInterval((int) id);
return;
}
System.out.println("Clear interval : ignoring " + unused);
// TODO: Should this be throwing an exception?
// throw new UnsupportedOperationException();
}
public void alert(final String message) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.alert(message);
}
}
public void back() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.back();
}
}
public void blur() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.blur();
}
}
public void clearTimeout(final Object someObj) {
if (someObj instanceof Integer) {
final Integer id = (Integer) someObj;
clearTimeout(id.intValue());
} else {
System.out.println("Window.clearTimeout() : Ignoring: " + someObj);
}
}
private void clearTimeout(final int timeoutID) {
System.out.println("Clearing timeout: " + timeoutID);
final Integer key = new Integer(timeoutID);
this.forgetTask(key, true);
}
public void close() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.close();
}
}
public boolean confirm(final String message) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return rc.confirm(message);
} else {
return false;
}
}
// Making public for link element
@HideFromJS
public void evalInScope(final String javascript) {
addJSTask(new JSRunnableTask(0, new Runnable() {
public void run() {
try {
final String scriptURI = "window.eval";
final Context ctx = Executor.createContext(getCurrURL(), Window.this.uaContext, windowContextFactory);
ctx.evaluateString(getWindowScope(), javascript, scriptURI, 1, null);
} finally {
Context.exit();
}
}
}));
}
/*
private Object evalInScope(final String javascript) {
final Context ctx = Executor.createContext(document.getDocumentURL(), this.uaContext);
try {
final String scriptURI = "window.eval";
return ctx.evaluateString(getWindowScope(), javascript, scriptURI, 1, null);
} finally {
Context.exit();
}
}
/* Removing because this eval method interferes with the default eval() method.
* The context of the JS eval() call is not preserved by this method.
public Object eval(final String javascript) {
final HTMLDocumentImpl document = (HTMLDocumentImpl) this.document;
if (document == null) {
throw new IllegalStateException("Cannot evaluate if document is not set.");
}
final Context ctx = Executor.createContext(document.getDocumentURL(), this.uaContext);
try {
final Scriptable scope = this.getWindowScope();
if (scope == null) {
throw new IllegalStateException("Scriptable (scope) instance was expected to be keyed as UserData to document using "
+ Executor.SCOPE_KEY);
}
final String scriptURI = "window.eval";
if (logger.isLoggable(Level.INFO)) {
logger.info("eval(): javascript follows...\r\n" + javascript);
}
return ctx.evaluateString(scope, javascript, scriptURI, 1, null);
} finally {
Context.exit();
}
}
*/
public void focus() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.focus();
}
}
private class MyContextFactory extends ContextFactory {
final private ClassShutter myClassShutter = new ClassShutter() {
public boolean visibleToScripts(final String fullClassName) {
// System.out.println("class shutter Checking: " + fullClassName);
if (fullClassName.startsWith("java")) {
final boolean isException = (fullClassName.startsWith("java.lang") && fullClassName.endsWith("Exception"));
if (fullClassName.equals("java.lang.Object") || isException) {
return true;
}
System.out.println("Warning: Something tried to access java classes from javascript.");
Thread.dumpStack();
return false;
}
// TODO: Change the default to false
return true;
}
};
// Override {@link #makeContext()}
@Override
protected Context makeContext()
{
final Context cx = super.makeContext();
cx.setClassShutter(myClassShutter);
// cx.setOptimizationLevel(9);
cx.setOptimizationLevel(-1);
cx.setLanguageVersion(Context.VERSION_1_8);
// Make Rhino runtime to call observeInstructionCount each 100_000 bytecode instructions
cx.setInstructionObserverThreshold(100_000);
// cx.setMaximumInterpreterStackDepth(100);
// cx.seal(null);
return cx;
}
@Override
protected void observeInstructionCount(final Context cx, final int instructionCount) {
final JSScheduler jsSchedulerLocal = jsScheduler;
if (jsSchedulerLocal != null) {
if (jsSchedulerLocal.isWindowClosing()) {
throw new WindowClosingError();
}
}
}
@Override
protected boolean hasFeature(Context cx, int featureIndex) {
if (featureIndex == Context.FEATURE_V8_EXTENSIONS) {
return true;
}
return super.hasFeature(cx, featureIndex);
}
}
static final class WindowClosingError extends Error {
private static final long serialVersionUID = 5375592396498284425L;
}
private final MyContextFactory windowContextFactory = new MyContextFactory();
@HideFromJS
public ContextFactory getContextFactory() {
return windowContextFactory;
}
private void initWindowScope(final Document doc) {
// Special Javascript class: XMLHttpRequest
final Scriptable ws = this.getWindowScope();
final JavaInstantiator xi = new JavaInstantiator() {
public Object newInstance(final Object[] args) {
final Document d = doc;
if (d == null) {
throw new IllegalStateException("Cannot perform operation when document is unset.");
}
HTMLDocumentImpl hd;
try {
hd = (HTMLDocumentImpl) d;
} catch (final ClassCastException err) {
throw new IllegalStateException("Cannot perform operation with documents of type " + d.getClass().getName() + ".");
}
return new XMLHttpRequest(uaContext, hd.getDocumentURL(), ws, Window.this);
}
};
defineInstantiator(ws, "XMLHttpRequest", XMLHTTPREQUEST_WRAPPER, xi);
final JavaInstantiator pi = new JavaInstantiator() {
public Object newInstance(final Object[] args) {
return new CanvasPath2D();
}
};
defineInstantiator(ws, "Path2D", PATH2D_WRAPPER, pi);
final JavaInstantiator ei = new JavaInstantiator() {
public Object newInstance(final Object[] args) {
if (args.length > 0) {
return new Event(args[0].toString(), doc);
}
throw ScriptRuntime.constructError("TypeError", "An event name must be provided");
}
};
defineInstantiator(ws, "Event", EVENT_WRAPPER, ei);
// We can use a single shared instance since it is dummy for now
ScriptableObject.putProperty(ws, "localStorage", STORAGE);
ScriptableObject.putProperty(ws, "sessionStorage", STORAGE);
// ScriptableObject.defineClass(ws, org.mozilla.javascript.ast.Comment.class);
defineElementClass(ws, doc, "Comment", "comment", CommentImpl.class);
// HTML element classes
defineElementClass(ws, doc, "Image", "img", HTMLImageElementImpl.class);
defineElementClass(ws, doc, "Script", "script", HTMLScriptElementImpl.class);
defineElementClass(ws, doc, "IFrame", "iframe", HTMLIFrameElementImpl.class);
defineElementClass(ws, doc, "Option", "option", HTMLOptionElementImpl.class);
defineElementClass(ws, doc, "Select", "select", HTMLSelectElementImpl.class);
// TODO: Add all similar elements
defineElementClass(ws, doc, "HTMLDivElement", "div", HTMLDivElementImpl.class);
defineInstantiator(ws, "Text", JavaClassWrapperFactory.getInstance().getClassWrapper(TextImpl.class), new JavaInstantiator() {
public Object newInstance(final Object[] args) {
final String data = (args != null && args.length > 0 && args[0] != null) ? args[0].toString() : "";
return document.createTextNode(data);
}
});
}
private static void defineInstantiator(
final Scriptable ws,
final String name,
final JavaClassWrapper wrapper,
final JavaInstantiator ji) {
final Function constructor = JavaObjectWrapper.getConstructor(name, wrapper, ws, ji);
ScriptableObject.defineProperty(ws, name, constructor, ScriptableObject.READONLY);
}
private Scriptable windowScope;
@HideFromJS
public Scriptable getWindowScope() {
synchronized (this) {
Scriptable ws = this.windowScope;
if (ws != null) {
return ws;
}
// Context.enter() OK in this particular case.
// final Context ctx = Context.enter();
final Context ctx = windowContextFactory.enterContext();
try {
// Window scope needs to be top-most scope.
ws = (Scriptable) JavaScript.getInstance().getJavascriptObject(this, null);
ws = ctx.initSafeStandardObjects((ScriptableObject) ws);
final Object consoleJSObj = JavaScript.getInstance().getJavascriptObject(new Console(), ws);
ScriptableObject.putProperty(ws, "console", consoleJSObj);
this.windowScope = ws;
return ws;
} finally {
Context.exit();
}
}
}
static public class Console {
public static void log(final Object obj) {
System.out.println("> " + obj);
}
}
private final static void defineElementClass(final Scriptable scope, final Document document, final String jsClassName,
final String elementName,
final Class<?> javaClass) {
final JavaInstantiator ji = new JavaInstantiator() {
public Object newInstance(final Object[] args) {
final Document d = document;
if (d == null) {
throw new IllegalStateException("Document not set in current context.");
}
return d.createElement(elementName);
}
};
final JavaClassWrapper classWrapper = JavaClassWrapperFactory.getInstance().getClassWrapper(javaClass);
final Function constructorFunction = JavaObjectWrapper.getConstructor(jsClassName, classWrapper, scope, ji);
ScriptableObject.defineProperty(scope, jsClassName, constructorFunction, ScriptableObject.READONLY);
}
@HideFromJS
public static Window getWindow(final HtmlRendererContext rcontext) {
if (rcontext == null) {
return null;
}
synchronized (CONTEXT_WINDOWS) {
final WeakReference<Window> wref = CONTEXT_WINDOWS.get(rcontext);
if (wref != null) {
final Window window = wref.get();
if (window != null) {
return window;
}
}
final Window window = new Window(rcontext, rcontext.getUserAgentContext());
CONTEXT_WINDOWS.put(rcontext, new WeakReference<>(window));
return window;
}
}
public Window open(final String relativeUrl, final String windowName, final String windowFeatures, final boolean replace) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
java.net.URL url;
final Document doc = this.document;
try {
if (doc instanceof HTMLDocumentImpl) {
url = ((HTMLDocumentImpl) doc).getFullURL(relativeUrl);
} else {
url = new java.net.URL(relativeUrl);
}
} catch (final java.net.MalformedURLException mfu) {
throw new IllegalArgumentException("Malformed URI: " + relativeUrl);
}
if (replace) {
this.document = null;
rc.navigate(url, null);
return this;
} else {
final HtmlRendererContext newContext = rc.open(url, windowName, windowFeatures, replace);
return getWindow(newContext);
}
} else {
return null;
}
}
public Window open(final String url) {
return this.open(url, "window:" + String.valueOf(ID.generateLong()));
}
public Window open(final String url, final String windowName) {
return this.open(url, windowName, "", false);
}
public Window open(final String url, final String windowName, final String windowFeatures) {
return this.open(url, windowName, windowFeatures, false);
}
public String prompt(final String message) {
return this.prompt(message, "");
}
public String prompt(final String message, final int inputDefault) {
return this.prompt(message, String.valueOf(inputDefault));
}
public String prompt(final String message, final String inputDefault) {
final HtmlRendererContext rcontext = this.rcontext;
if (rcontext != null) {
return rcontext.prompt(message, inputDefault);
} else {
return null;
}
}
public void scrollTo(final int x, final int y) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.scroll(x, y);
}
}
public void scrollBy(final int x, final int y) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.scrollBy(x, y);
}
}
public void resizeTo(final int width, final int height) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.resizeTo(width, height);
}
}
public void resizeBy(final int byWidth, final int byHeight) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.resizeBy(byWidth, byHeight);
}
}
@NotGetterSetter
public int setTimeout(final String expr, final double millis) {
if ((millis > Integer.MAX_VALUE) || (millis < 0)) {
throw new IllegalArgumentException("Timeout value " + millis + " is not supported.");
}
final int timeID = generateTimerID();
final Integer timeIDInt = new Integer(timeID);
final ActionListener task = new ExpressionTimerTask(this, timeIDInt, expr, true);
int t = (int) millis;
if (t < 1) {
t = 1;
}
final Timer timer = new Timer(t, task);
timer.setRepeats(false);
this.putAndStartTask(timeIDInt, timer, null);
return timeID;
}
@NotGetterSetter
public int setTimeout(final Function function, final double millis) {
if ((millis > Integer.MAX_VALUE) || (millis < 0)) {
throw new IllegalArgumentException("Timeout value " + millis + " is not supported.");
}
final int timeID = generateTimerID();
System.out.println("Creating timer with id: " + timeID + " in " + document.getBaseURI());
final Integer timeIDInt = new Integer(timeID);
final ActionListener task = new FunctionTimerTask(this, timeIDInt, function, true);
int t = (int) millis;
if (t < 1) {
t = 1;
}
final Timer timer = new Timer(t, task);
timer.setRepeats(false);
this.putAndStartTask(timeIDInt, timer, function);
return timeID;
}
@NotGetterSetter
public int setTimeout(final Function function) {
return setTimeout(function, 0);
}
@NotGetterSetter
public int setTimeout(final String expr) {
return setTimeout(expr, 0);
}
public boolean isClosed() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return rc.isClosed();
} else {
return false;
}
}
public String getDefaultStatus() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return rc.getDefaultStatus();
} else {
return null;
}
}
public HTMLCollection getFrames() {
final Document doc = this.document;
if (doc instanceof HTMLDocumentImpl) {
return ((HTMLDocumentImpl) doc).getFrames();
}
return null;
}
private int length;
private boolean lengthSet = false;
/**
* Gets the number of frames.
*/
public int getLength() {
if (this.lengthSet) {
return this.length;
} else {
final HTMLCollection frames = this.getFrames();
return frames == null ? 0 : frames.getLength();
}
}
public void setLength(final int length) {
this.lengthSet = true;
this.length = length;
}
public String getName() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return rc.getName();
} else {
return null;
}
}
public void setName(final String newName) {
// TODO
System.out.println("TODO: window.setName");
}
public Window getParent() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
final HtmlRendererContext rcontextParent = rc.getParent();
if (rcontextParent == null) {
return this;
} else {
return Window.getWindow(rcontextParent);
}
} else {
return null;
}
}
public Window getOpener() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return Window.getWindow(rc.getOpener());
} else {
return null;
}
}
public void setOpener(final Window opener) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
if (opener == null) {
rc.setOpener(null);
} else {
rc.setOpener(opener.rcontext);
}
}
}
public Window getSelf() {
return this;
}
public String getStatus() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return rc.getStatus();
} else {
return null;
}
}
public void setStatus(final String message) {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
rc.setStatus(message);
}
}
public Window getTop() {
final HtmlRendererContext rc = this.rcontext;
if (rc != null) {
return Window.getWindow(rc.getTop());
} else {
return null;
}
}
public Window getWindow() {
return this;
}
public Navigator getNavigator() {
synchronized (this) {
Navigator nav = this.navigator;
if (nav == null) {
nav = new Navigator(this.uaContext);
this.navigator = nav;
}
return nav;
}
}
public Screen getScreen() {
synchronized (this) {
Screen nav = this.screen;
if (nav == null) {
nav = new Screen();
this.screen = nav;
}
return nav;
}
}
public Location getLocation() {
synchronized (this) {
Location loc = this.location;
if (loc == null) {
loc = new Location(this);
this.location = loc;
}
return loc;
}
}
public void setLocation(final String location) {
this.getLocation().setHref(location);
}
private History history;
public History getHistory() {
synchronized (this) {
History hist = this.history;
if (hist == null) {
hist = new History(this);
this.history = hist;
}
return hist;
}
}
public CSS2Properties getComputedStyle(final HTMLElement element, final String pseudoElement) {
if (element instanceof HTMLElementImpl) {
return ((HTMLElementImpl) element).getComputedStyle(pseudoElement);
} else {
throw new java.lang.IllegalArgumentException("Element implementation unknown: " + element);
}
}
public Function getOnload() {
final Document doc = this.document;
if (doc instanceof HTMLDocumentImpl) {
return ((HTMLDocumentImpl) doc).getOnloadHandler();
} else {
return null;
}
}
public void setOnload(final Function onload) {
// Note that body.onload overrides
// window.onload.
/*
final Document doc = this.document;
if (doc instanceof HTMLDocumentImpl) {
((HTMLDocumentImpl) doc).setWindowOnloadHandler(onload);
}*/
onWindowLoadHandler = onload;
}
private Function onunload;
public Function getOnunload() {
return onunload;
}
public void setOnunload(final Function onunload) {
this.onunload = onunload;
}
public org.w3c.dom.Node namedItem(final String name) {
// Bug 1928758: Element IDs are named objects in context.
final Document doc = this.document;
if (doc == null) {
return null;
}
final org.w3c.dom.Node node = doc.getElementById(name);
if (node != null) {
return node;
}
return null;
}
private static abstract class WeakWindowTask implements ActionListener {
private final WeakReference<Window> windowRef;
public WeakWindowTask(final Window window) {
this.windowRef = new WeakReference<>(window);
}
protected Window getWindow() {
final WeakReference<Window> ref = this.windowRef;
return ref == null ? null : ref.get();
}
}
private static class FunctionTimerTask extends WeakWindowTask {
// Implemented as a static WeakWindowTask to allow the Window
// to get garbage collected, especially in infinite loop
// scenarios.
private final Integer timeIDInt;
private final WeakReference<Function> functionRef;
private final boolean removeTask;
public FunctionTimerTask(final Window window, final Integer timeIDInt, final Function function, final boolean removeTask) {
super(window);
this.timeIDInt = timeIDInt;
this.functionRef = new WeakReference<>(function);
this.removeTask = removeTask;
}
public void actionPerformed(final ActionEvent e) {
System.out.println("Timer ID fired: " + timeIDInt + ", oneshot: " + removeTask);
// This executes in the GUI thread and that's good.
try {
final Window window = this.getWindow();
if (window == null) {
if (logger.isLoggable(Level.INFO)) {
logger.info("actionPerformed(): Window is no longer available.");
}
return;
}
if (this.removeTask) {
window.forgetTask(this.timeIDInt, false);
}
// final HTMLDocumentImpl doc = (HTMLDocumentImpl) window.getDocument();
if (window.getDocument() == null) {
throw new IllegalStateException("Cannot perform operation when document is unset.");
}
final Function function = this.functionRef.get();
if (function == null) {
throw new IllegalStateException("Cannot perform operation. Function is no longer available.");
}
window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt + ", oneshot: " + removeTask, () -> {
Executor.executeFunction(window.getWindowScope(), function, window.getCurrURL(), window.getUserAgentContext(),
window.windowContextFactory);
}));
// Executor.executeFunction(window.getWindowScope(), function, doc.getDocumentURL(), window.getUserAgentContext(), window.windowFactory);
} catch (final Exception err) {
logger.log(Level.WARNING, "actionPerformed()", err);
}
}
}
private static class ExpressionTimerTask extends WeakWindowTask {
// Implemented as a static WeakWindowTask to allow the Window
// to get garbage collected, especially in infinite loop
// scenarios.
private final Integer timeIDInt;
private final String expression;
private final boolean removeTask;
public ExpressionTimerTask(final Window window, final Integer timeIDInt, final String expression, final boolean removeTask) {
super(window);
this.timeIDInt = timeIDInt;
this.expression = expression;
this.removeTask = removeTask;
}
public void actionPerformed(final ActionEvent e) {
// This executes in the GUI thread and that's good.
try {
final Window window = this.getWindow();
if (window == null) {
if (logger.isLoggable(Level.INFO)) {
logger.info("actionPerformed(): Window is no longer available.");
}
return;
}
if (this.removeTask) {
window.forgetTask(this.timeIDInt, false);
}
// final HTMLDocumentImpl doc = (HTMLDocumentImpl) window.getDocument();
if (window.getDocument() == null) {
throw new IllegalStateException("Cannot perform operation when document is unset.");
}
window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt, () -> {
window.evalInScope(this.expression);
}));
// window.evalInScope(this.expression);
} catch (final Exception err) {
logger.log(Level.WARNING, "actionPerformed()", err);
}
}
}
private static class TaskWrapper {
public final Timer timer;
// TODO: The retained object seems to be required to keep timer callback functions from being garbage collected.
// The FunctionTimerTask only keeps a weak reference. Need to review this design.
@SuppressWarnings("unused")
private final Object retained;
public TaskWrapper(final Timer timer, final Object retained) {
super();
this.timer = timer;
this.retained = retained;
}
}
public void addEventListener(final String type, final Function listener) {
addEventListener(type, listener, false);
}
private final EventTargetManager eventTargetManager = new EventTargetManager(this);
public EventTargetManager getEventTargetManager() {
return eventTargetManager;
}
public void addEventListener(final String type, final Function listener, final boolean useCapture) {
if (useCapture) {
throw new UnsupportedOperationException();
}
/*
// TODO: Should this delegate completely to document
if ("load".equals(type)) {
document.addLoadHandler(listener);
} else {
document.addEventListener(type, listener);
}*/
System.out.println("window Added listener for: " + type);
eventTargetManager.addEventListener((NodeImpl) document, type, listener);
}
public void removeEventListener(final String type, final Function listener, final boolean useCapture) {
// TODO: Should this delegate completely to document
if ("load".equals(type)) {
((HTMLDocumentImpl) document).removeLoadHandler(listener);
}
eventTargetManager.removeEventListener((NodeImpl) document, type, listener, useCapture);
}
public boolean dispatchEvent(final Event evt) throws EventException {
// TODO
System.out.println("TODO: window dispatch event");
eventTargetManager.dispatchEvent((NodeImpl) document, evt);
return false;
}
// TODO: Hide from JS
public void domContentLoaded(final Event domContentLoadedEvent) {
eventTargetManager.dispatchEvent((NodeImpl) document, domContentLoadedEvent);
}
private Function onWindowLoadHandler;
// private Function windowLoadListeners;
// TODO: Move job scheduling logic into Window class
private AtomicBoolean jobsOver = new AtomicBoolean(false);
@HideFromJS
public void jobsFinished() {
final Event windowLoadEvent = new Event("load", document);
eventTargetManager.dispatchEvent((NodeImpl) document, windowLoadEvent);
final Function handler = this.onWindowLoadHandler;
if (handler != null) {
addJSTask(new JSRunnableTask(0, new Runnable() {
public void run() {
Executor.executeFunction((NodeImpl) document, handler, windowLoadEvent, windowContextFactory);
}
}));
// Executor.executeFunction(document, handler, windowLoadEvent);
}
jobsOver.set(true);
}
@PropertyName("Element")
public Class<Element> getElement() {
return Element.class;
}
@PropertyName("Node")
public Class<Node> getNode() {
return Node.class;
}
public void addEventListener(final String type, final EventListener listener) {
addEventListener(type, listener, false);
}
public void addEventListener(final String type, final EventListener listener, final boolean useCapture) {
if (useCapture) {
throw new UnsupportedOperationException();
}
// TODO Auto-generated method stub
// throw new UnsupportedOperationException();
eventTargetManager.addEventListener((NodeImpl) document, type, listener, useCapture);
}
public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
@Override
public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
private void shutdown() {
// TODO: Add the sync below, when/if the scheduleLock is added
// synchronized (scheduleLock) {
forgetAllTasks();
if (jsScheduler != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
jsScheduler.stopAndWindUp(false);
jsScheduler = null;
return null;
});
}
// }
}
// TODO: More thorough research and design needs to be done here. GH-127
@Override
protected void finalize() throws Throwable {
shutdown();
super.finalize();
}
@HideFromJS
public boolean hasPendingTasks() {
return (!jobsOver.get()) || jsScheduler.hasPendingTasks();
}
}