/* * Copyright 2012 Jason Miller * * 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 jj.script; import java.util.Arrays; import java.util.Map; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import javax.inject.Singleton; import org.mozilla.javascript.BaseFunction; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Undefined; import jj.event.Listener; import jj.event.Subscriber; import jj.execution.DelayedExecutor.CancelKey; import jj.execution.TaskRunner; import jj.script.module.RootScriptEnvironment; import jj.util.Sequence; /** * <p> * Provides standard timer functions to script environments, i.e. * <ul> * <li>setTimeout * <li>setInterval * <li>clearTimeout * <li>clearInterval * </ul> * * @author jason * */ @Singleton @Subscriber class Timers { private static final Object[] EMPTY_SLICE = new Object[0]; private final TaskRunner taskRunner; private final CurrentScriptEnvironment env; // there is a bit of a dance around the cancel keys. they must be stored according to the root environment, because // it's conceivable that a module will pass a cancel key via exports or a callback or a function call to some other // environment - but the timer itself should execute in the context of the original environment private final ConcurrentHashMap<RootScriptEnvironment<?>, Map<String, CancelKey>> runningTimers = new ConcurrentHashMap<>(); private final Sequence cancelIds = new Sequence(); @Inject Timers(TaskRunner taskRunner, CurrentScriptEnvironment env) { this.taskRunner = taskRunner; this.env = env; } @Listener void on(ScriptEnvironmentDied event) { //noinspection SuspiciousMethodCalls Map<String, CancelKey> cancelKeys = runningTimers.remove(event.scriptEnvironment()); if (cancelKeys != null) { cancelKeys.values().forEach(CancelKey::cancel); } } private void killTimerCancelKey(final ScriptEnvironment<?> se, final String timerKey) { //noinspection SuspiciousMethodCalls Map<String, CancelKey> keys = runningTimers.get(se); if (keys != null) { CancelKey key = keys.remove(timerKey); if (key != null) { key.cancel(); } } } private String setTimer(final Callable function, final int delay, final boolean repeat, final Object...args) { final String key = "jj-timer-" + cancelIds.next(); final ScriptEnvironment<?> rootEnvironment = env.currentRootScriptEnvironment(); ScriptTask<ScriptEnvironment<?>> task = new ScriptTask<ScriptEnvironment<?>>(repeat ? "setInterval" : "setTimeout", env.current()) { @Override protected void begin() throws Exception { // if this is setTimeout, kill the cancelation structure if (!repeat) { killTimerCancelKey(rootEnvironment, key); } pendingKey = scriptEnvironment.execute(function, args); } @Override protected void complete() throws Exception { // we need to repeat once the task is complete, as an artifact of the resumable structure // to do otherwise would require a way to clone tasks, which should actually be doable? // but for now, repeat on complete if (repeat) { repeat(); } } @Override protected long delay() { return delay; } }; taskRunner.execute(task); runningTimers.computeIfAbsent(env.currentRootScriptEnvironment(), a -> new HashMap<>()).put(key, task.cancelKey()); return key; } private final class TimerFunction extends BaseFunction { private static final long serialVersionUID = 1L; private final boolean repeat; TimerFunction(final boolean repeat) { this.repeat = repeat; } @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { Callable c = null; int time = 0; Object[] slice = EMPTY_SLICE; if (args.length > 0 && args[0] instanceof Callable) { c = (Callable)args[0]; } if (args.length > 1) { Integer timeAtt = Util.toJavaInt(args[1]); if (timeAtt != null) { time = timeAtt; } } if (args.length > 2) { slice = Arrays.copyOfRange(args, 2, args.length); } if (c != null) { return setTimer(c, time, repeat, slice); } return Undefined.instance; } } final BaseFunction setInterval = new TimerFunction(true); final BaseFunction setTimeout = new TimerFunction(false); final BaseFunction clearInterval = new BaseFunction() { private static final long serialVersionUID = 1L; @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args.length > 0 && args[0] instanceof CharSequence) { killTimerCancelKey(env.currentRootScriptEnvironment(), String.valueOf(args[0])); } return Undefined.instance; } }; final BaseFunction clearTimeout = clearInterval; }