/*
* 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.api;
import static jj.script.api.ServerEventScriptResult.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.mozilla.javascript.Callable;
import com.google.inject.Injector;
import jj.event.Listener;
import jj.event.Subscriber;
import jj.execution.TaskRunner;
import jj.script.CurrentScriptEnvironment;
import jj.script.ScriptEnvironment;
import jj.script.ScriptEnvironmentDied;
import jj.util.CodeGenHelper;
/**
* <p>
* Component to allow scripts to adhoc register for server events.
*
* <p>
* Basic flow is codegen a new class if needed, and put an instance of it
* into a map keyed ScriptEnvironment instance -> lots of stuff. see below!
*
* <p>
* When the environment dies, it gets unregistered automatically. it is also
* possible to unsubscribe directly
*
* @author jason
*
*/
@Singleton
@Subscriber
public class ServerEventScriptBridge {
// ugh! okay map of ScriptEnvironment -> (map of event name -> (map of Callable -> generated invoker instance))
// only the top level map needs to accomodate concurrency because a given script environment is guaranteed
// to only execute from a single thread.
private final ConcurrentMap<ScriptEnvironment<?>, Map<String, Map<Callable, ServerEventCallableInvoker>>> invokers =
new ConcurrentHashMap<>(16, 0.75F, 4);
private final ConcurrentMap<String, Class<? extends ServerEventCallableInvoker>> invokerClasses = new ConcurrentHashMap<>(16, 0.75F, 2);
private final CurrentScriptEnvironment env;
private final Injector injector;
private final ClassPool classPool = CodeGenHelper.classPool();
private final CtClass superClass;
private final CtConstructor superConstructor;
@Inject
ServerEventScriptBridge(CurrentScriptEnvironment env, Injector injector) throws Exception {
this.env = env;
this.injector = injector;
superClass = classPool.get(ServerEventCallableInvoker.class.getName());
CtClass taskRunner = classPool.get(TaskRunner.class.getName());
superConstructor = superClass.getDeclaredConstructor(new CtClass[] {taskRunner});
}
private String generateInvokerClassName(String eventClassName) {
return getClass().getPackage().getName() + ".GeneratedInvokerFor$$" + eventClassName.replace('.', '_');
}
private void generateConstructor(final CtClass invokerClass) throws Exception {
CtConstructor ctor =
CtNewConstructor.make(
superConstructor.getParameterTypes(),
superConstructor.getExceptionTypes(),
invokerClass
);
invokerClass.addConstructor(ctor);
CodeGenHelper.addAnnotationToMethod(ctor, Inject.class);
}
private void generateInvocationMethod(final CtClass invokerClass, final String eventClassName) throws Exception {
CtMethod method = CtNewMethod.make(
"void invocationBridge(" + eventClassName + " event) { " +
"super.invoke(event);" +
"}",
invokerClass
);
CodeGenHelper.addAnnotationToMethod(method, Listener.class);
invokerClass.addMethod(method);
}
@SuppressWarnings("unchecked")
private Class<? extends ServerEventCallableInvoker> makeOrFindInvokerClass(String eventClassName) {
final String className = generateInvokerClassName(eventClassName);
// this pattern is required to make this class testable and resilient in the face of
// multiple simultaneous requests for the same event. granted, it's not the most
// likely scenario, but it would sure be a pain if it happened, and that also implies
// that contention should be low anyway
return invokerClasses.computeIfAbsent(className, name -> {
try {
return (Class<? extends ServerEventCallableInvoker>)Class.forName(className);
} catch (ClassNotFoundException cnfe) {}
try {
CtClass invokerClass = classPool.makeClass(className, superClass);
generateConstructor(invokerClass);
CodeGenHelper.addAnnotationToClass(invokerClass, Subscriber.class);
generateInvocationMethod(invokerClass, eventClassName);
CodeGenHelper.storeGeneratedClass(invokerClass);
return classPool.toClass(
invokerClass,
getClass().getClassLoader(),
getClass().getProtectionDomain()
);
// don't bother detaching, it's going to get looked up in the event system.
} catch (Exception cce) {
throw new AssertionError(cce);
}
});
}
@Listener
void on(ScriptEnvironmentDied sed) {
Map<String, Map<Callable, ServerEventCallableInvoker>> map = invokers.remove(sed.scriptEnvironment());
// make sure the various invokers don't get invoked before cleanup
if (map != null) {
map.values().forEach(callablesMap -> callablesMap.values().forEach(invoker -> invoker.kill()));
}
}
private Map<String, Map<Callable, ServerEventCallableInvoker>> scriptEnvironmentMap() {
return invokers.computeIfAbsent(env.current(), se -> new HashMap<>());
}
public ServerEventScriptResult subscribe(final String eventClassName, final Callable callable) {
try {
Class.forName(eventClassName);
} catch (ClassNotFoundException cnfe) {
return NotAnEventClass;
}
Map<String, Map<Callable, ServerEventCallableInvoker>> callablesMap = scriptEnvironmentMap();
Map<Callable, ServerEventCallableInvoker> invokerMap = callablesMap.computeIfAbsent(eventClassName, name -> new HashMap<>());
if (invokerMap.containsKey(callable)) { // no double adding!
return AlreadyBound;
}
Class<? extends ServerEventCallableInvoker> invokerClass = makeOrFindInvokerClass(eventClassName);
ServerEventCallableInvoker invoker = injector.getInstance(invokerClass);
invoker.invocationInstances(env.current(), callable);
invokerMap.put(callable, invoker);
return Success;
}
public ServerEventScriptResult unsubscribe(final String eventName, final Callable callable) {
Map<Callable, ServerEventCallableInvoker> callableMap = scriptEnvironmentMap().get(eventName);
if (callableMap != null) {
ServerEventCallableInvoker invoker = callableMap.remove(callable);
if (invoker != null) {
invoker.kill();
return Success;
}
}
return NotBound;
}
}