package org.cryptocoinpartners.module; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.apache.commons.configuration.AbstractConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.ArrayUtils; import org.cryptocoinpartners.esper.annotation.Listeners; import org.cryptocoinpartners.esper.annotation.Subscriber; import org.cryptocoinpartners.esper.annotation.When; import org.cryptocoinpartners.schema.Event; import org.cryptocoinpartners.service.Service; import org.cryptocoinpartners.util.ConfigUtil; import org.cryptocoinpartners.util.Injector; import org.cryptocoinpartners.util.ReflectionUtil; import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.espertech.esper.client.EPAdministrator; import com.espertech.esper.client.EPRuntime; import com.espertech.esper.client.EPServiceProvider; import com.espertech.esper.client.EPServiceProviderManager; import com.espertech.esper.client.EPStatement; import com.espertech.esper.client.EventBean; import com.espertech.esper.client.SafeIterator; import com.espertech.esper.client.StatementAwareUpdateListener; import com.espertech.esper.client.UpdateListener; import com.espertech.esper.client.deploy.DeploymentException; import com.espertech.esper.client.deploy.DeploymentOptions; import com.espertech.esper.client.deploy.DeploymentResult; import com.espertech.esper.client.deploy.EPDeploymentAdmin; import com.espertech.esper.client.deploy.ParseException; import com.espertech.esper.client.time.CurrentTimeEvent; import com.espertech.esper.client.time.CurrentTimeSpanEvent; import com.espertech.esper.client.time.TimerControlEvent; import com.espertech.esper.core.service.EPServiceProviderImpl; import com.google.inject.Binder; import com.google.inject.Inject; import com.google.inject.Module; /** * This is a wrapper around Esper with dependency injection functionality as well. * * @author Tim Olson */ @SuppressWarnings("UnusedDeclaration") public class Context { /** * Contexts are not created through injection, because they are injection contexts themselves. Use this static * method for construction. */ public static Context create() { return new Context(null); } /** * Use a TimeProvider when you do not want real wall-clock time to drive the Context; for example, during replay * of historical events. */ public static Context create(TimeProvider timeProvider) { return new Context(timeProvider); } public interface TimeProvider { /** * @return the Instant the Context should be initialized to as the starting time */ Instant getInitialTime(); /** * @param event The event to be published after the time is advanced. If null is returned, the time is * not advanced and the current time in the Esper engine is used. */ Instant nextTime(Event event); } public static interface AttachListener { public void afterAttach(Context context); } /** * This is the main way to register modules with the Context. Attaching a class to a Context has * many effects: * <ol> * <li>The class will be instantiated and the instance will be <pre>@Inject</pre>ed by Guice, binding any other * objects which have been attached to this Context previously</li> * <li>The instance will have any fields marked with @Config set using this Context's current Configuration.</li> * <li>If the class c has any superclasses or interfaces tagged with @Service, this instance is registered as * an implementation of that service interface for future injections. Other instances in this Context will have * their @Injected fields of service types set to an instance of this class c when the types match.</li> * <li>The created instance is <pre>subscribe()</pre>'d to the Context's esper, binding any @When annotations * on the instances's methods to esper statements</li> * <li>If the attached class implements AttachListener, the instance's afterAttach() method is called.</li> * <li>The new instance is returned after configuration</li> * </ol> */ public <T> T attach(Class<T> c) { return attach(c, null); } /** * Looks in the module.path packages for a class with the given simple name. The classname may have "Module" * appended at the end e.g. class MyNameModule can be referenced as String "MyName" * @param name The name of the Class to load from the module.path (See Configuration) * @return the instance that was created and attached */ public Object attach(String name) { Class<?> c = findModuleClass(name); if (c == null) throw new Error("Could not find module named " + name + " in module.path"); //noinspection RedundantCast return attach(c, (Configuration) null); } /** * @param config Override configuration for this particular attachment * @see #attach(Class) */ public Object attach(String name, Configuration config, Module... specificInjections) { Class<?> c = findModuleClass(name); if (c == null) throw new Error("Could not find module named " + name + " in module.path"); return attach(c, config, specificInjections); } public <T> T attach(Class<T> c, final Configuration moduleConfig) { return attach(c, moduleConfig, (Module[]) null); } public <T> T attach(Class<T> c, final Configuration moduleConfig, Module... specificInjections) { Injector i = ArrayUtils.isEmpty(specificInjections) ? injector : injector.createChildInjector(specificInjections); if (moduleConfig != null) i = i.withConfig(moduleConfig); T instance = i.getInstance(c); attach(c, instance); return instance; } public void attach(Class c, Object instance) { ConfigUtil.applyConfiguration(instance, config); // loadStatements(instance.getClass().getSimpleName()); subscribe(instance); registerBindings(c, instance); if (AttachListener.class.isAssignableFrom(c)) { AttachListener listener = (AttachListener) instance; listener.afterAttach(this); } } public List<Object> loadStatementByName(String name) throws ParseException, DeploymentException, IOException { EPStatement statement = epAdministrator.getStatement(name); List<Object> list = new ArrayList<>(); if (statement != null && statement.isStarted()) { SafeIterator<EventBean> it = statement.safeIterator(); try { while (it.hasNext()) { EventBean bean = it.next(); Object underlaying = bean.getUnderlying(); list.add(underlaying); } } finally { it.close(); } } return list; } /** * Attaches a specific instance to this Context. */ public void attachInstance(Object instance) { attach(instance.getClass(), instance); } public void setPublishTime(Event e) { Instant now; // if (timeProvider != null) { // now = timeProvider.nextTime(e); //if (now != null) // advanceTime(now); //else // now = new Instant(epRuntime.getCurrentTime()); //} else now = new Instant(epRuntime.getCurrentTime()); e.publishedAt(now); } private class publishRunnable implements Runnable { private final Event event; // protected Logger log; public publishRunnable(Event event) { this.event = event; } @Override public void run() { handlePublish(event); } } public void publish(Event e) { // contextService.submit(new publishRunnable(e)); handlePublish(e); } private void handlePublish(Event e) { Instant now; if (timeProvider != null) { now = timeProvider.nextTime(e); if (now != null) advanceTime(now); else now = new Instant(epRuntime.getCurrentTime()); } else now = new Instant(epRuntime.getCurrentTime()); e.publishedAt(now); log.trace("publishg event: " + e); epRuntime.sendEvent(e); // epRuntime.route(e); } public synchronized void publish(TimerControlEvent e) { epRuntime.sendEvent(e); // epRuntime.route(e); } public void route(Event e) { Instant now = null; if (timeProvider != null) { now = timeProvider.nextTime(e); if (now == null) now = new Instant(epRuntime.getCurrentTime()); } else now = new Instant(epRuntime.getCurrentTime()); e.publishedAt(now); log.trace("routing event: " + e); epRuntime.route(e); // epRuntime.sendEvent(e); } public void destroy() { privateDestroy(); } public void advanceTime(Instant now) { if (timeProvider == null) throw new IllegalArgumentException("Can only advanceTime() when the Context was constructed with a TimeProvider"); if (lastTime == null) { // jump to the start time instead of stepping to it // log.debug("time:" + now.getMillis()); epRuntime.sendEvent(new CurrentTimeEvent(now.getMillis())); } else if (now.isBefore(lastTime)) throw new IllegalArgumentException("advanceTime must always move time forward. " + now + " < " + lastTime); else if (now.isAfter(lastTime)) { // log.debug("time:" + now.getMillis()); // step time up to now epRuntime.sendEvent(new CurrentTimeSpanEvent(now.getMillis())); } lastTime = now; } public void subscribe(Object listener) { if (listener == this) return; for (Class<?> cls = listener.getClass(); cls != Object.class; cls = cls.getSuperclass()) subscribe(listener, cls); } private void processAnnotations(EPStatement statement) throws Exception { Annotation[] annotations = statement.getAnnotations(); for (Annotation annotation : annotations) { if (annotation instanceof Subscriber) { Subscriber subscriber = (Subscriber) annotation; Object obj = getSubscriber(subscriber.className()); statement.setSubscriber(obj); } else if (annotation instanceof Listeners) { Listeners listeners = (Listeners) annotation; for (String className : listeners.classNames()) { Class<?> cl = Class.forName(className); Object obj = cl.newInstance(); if (obj instanceof StatementAwareUpdateListener) { statement.addListener((StatementAwareUpdateListener) obj); } else { statement.addListener((UpdateListener) obj); } } } } } private Object getSubscriber(String fqdn) throws Exception { Class<?> cl = Class.forName(fqdn); return cl.newInstance(); } public Instant getTime() { return new Instant(epRuntime.getCurrentTime()); } public EPRuntime getRunTime() { return epRuntime; } private void subscribe(Object listener, Class<?> cls) { String classname = null; for (Method method : cls.getDeclaredMethods()) { if (classname == null || !classname.equals(method.getDeclaringClass().getSimpleName())) { // we have a new class so let's try to load the epl files classname = method.getDeclaringClass().getSimpleName(); loadStatements(classname); } When when = method.getAnnotation(When.class); if (when != null) { String statement = when.value(); log.debug("subscribing " + method + " with statement \"" + statement + "\""); subscribe(listener, method, statement); } } } public void subscribe(Object listener, Method method, String statement) { EPStatement epStatement = epAdministrator.createEPL(statement); subscribe(listener, method, epStatement); } public void loadStatements(String source) { loadStatements(source, null); } /** * @param source a string containing EPL statements * @param intoFieldBean if not null, any @IntoMethod annotations on Esper statements will bind the columns from * the select statement into the fields of the intoFieldBean instance. */ public void loadStatements(String source, Object intoFieldBean) { EPDeploymentAdmin deploymentAdmin = epAdministrator.getDeploymentAdmin(); com.espertech.esper.client.deploy.Module module; String filename = source + ".epl"; try { module = deploymentAdmin.read(filename); DeploymentResult deployResult; try { deployResult = deploymentAdmin.deploy(module, new DeploymentOptions()); List<EPStatement> statements = deployResult.getStatements(); for (EPStatement statement : statements) { try { processAnnotations(statement); } catch (Exception e) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e); e.printStackTrace(); } } log.debug("deployed module " + filename); } catch (DeploymentException e) { log.error("error deploying module " + source, e); } } catch (FileNotFoundException ignored) { // it is not neccessary for every module to have an EPL file } catch (IOException e) { log.debug("no module file found for " + filename + " on classpath. Please ensure " + source + ".epl is in the resources directory."); } catch (ParseException e) { log.error("Could not parse EPL " + filename, e); } } public Injector getInjector() { return injector; } public Configuration getConfig() { return config; } // // End of Public Interface // private void subscribe(Object listener, Method method, EPStatement statement) { statement.setSubscriber(new Listener(listener, method, statement.getText())); } private Class<?> findModuleClass(String name) { Class<?> found; for (String path : getModulePathList()) { String pdot = path + "."; if ((found = findClass(pdot + name)) != null) return found; if ((found = findClass(pdot + name + "Module")) != null) return found; } return null; } private Class<?> findClass(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { return null; } } private static void load(Context context, File file) throws IOException, ParseException, DeploymentException { context.loadStatements(FileUtils.readFileToString(file)); } private static void loadEsperFiles(Context context, String modulePackageName) throws Exception { String path = modulePackageName.replaceAll("\\.", "/"); File[] files = new File(path).listFiles(); if (files != null) { for (File file : files) { if (file.getName().toLowerCase().endsWith(".epl")) { log.debug("loading epl file " + file.getName()); load(context, file); } } } } private static List<String> getModulePathList() { String pathProperty = "module.path"; return ConfigUtil.getPathProperty(pathProperty); } // todo how to bring in module-specific config now? private static AbstractConfiguration buildConfig(String name, String modulePackageName, @Nullable AbstractConfiguration c) throws ConfigurationException { final ClassLoader classLoader = Context.class.getClassLoader(); final ArrayList<AbstractConfiguration> moduleConfigs = new ArrayList<>(); // first priority is the caller's configuration if (c != null) moduleConfigs.add(c); // then add the package-specific props file String slashPackage = modulePackageName.replaceAll("\\.", "/"); String propsFilePath = slashPackage + "/" + name + ".properties"; URL resource = classLoader.getResource(propsFilePath); if (resource != null) { PropertiesConfiguration packageConfig = new PropertiesConfiguration(resource); moduleConfigs.add(packageConfig); } // then the more generic config.properties propsFilePath = slashPackage + "/config.properties"; resource = classLoader.getResource(propsFilePath); if (resource != null) { PropertiesConfiguration packageConfig = new PropertiesConfiguration(resource); moduleConfigs.add(packageConfig); } return ConfigUtil.forModule(moduleConfigs); } private boolean registerBindings(Class<?> c) { return registerBindings(c, c); } // recursion method walks up the superclass and interface parent tree looking for parent classes to register private boolean registerBindings(Class service, Object implementationClassOrObject) { boolean injectorWasUpdated = conditionalRegister(service, implementationClassOrObject); Class<?> superclass = service.getSuperclass(); if (superclass != null && registerBindings(superclass, implementationClassOrObject)) injectorWasUpdated = true; for (Class<?> interfaceClass : service.getInterfaces()) if (registerBindings(interfaceClass, implementationClassOrObject)) injectorWasUpdated = true; return injectorWasUpdated; } private boolean conditionalRegister(final Class interfaceClass, final Object implementationClassOrObject) { if (interfaceClass.getAnnotation(Service.class) != null) { doRegister(interfaceClass, implementationClassOrObject); return true; } return false; } private void doRegister(final Class interfaceClass, final Object implementationClassOrObject) { injector = childInjector(null, new Module() { @Override @SuppressWarnings("unchecked") public void configure(Binder binder) { if (Class.class.isAssignableFrom(implementationClassOrObject.getClass())) binder.bind(interfaceClass).to((Class) implementationClassOrObject); else binder.bind(interfaceClass).toInstance(implementationClassOrObject); } }); } private <T> void register(final Class<? super T> interfaceClass, final T instance) { injector = childInjector(null, new Module() { @Override public void configure(Binder binder) { binder.bind(interfaceClass).toInstance(instance); } }); } @SuppressWarnings("unchecked") private Injector childInjector(final @Nullable Configuration configParams, Module... modules) { final int moduleLength = ArrayUtils.getLength(modules); if (moduleLength == 0 && configParams == null) return injector; Injector childInjector = injector.createChildInjector(modules); if (configParams != null) childInjector.setConfig(configParams); return childInjector; } public void setTimeProvider(TimeProvider timeProvider) { this.timeProvider = timeProvider; // EPServiceProviderSPI spi = (EPServiceProviderSPI) epService; //spi. //epService.getEPRuntime().g } @Inject private Context(TimeProvider timeProvider) { this.timeProvider = timeProvider; // final com.espertech.esper.client.Configuration esperConfig = new com.espertech.esper.client.Configuration(); epConfig.configure("cointrader-esper.cfg.xml"); epConfig.addEventType(Event.class); Set<Class<? extends Event>> eventTypes = ReflectionUtil.getSubtypesOf(Event.class); for (Class<? extends Event> eventType : eventTypes) epConfig.addEventType(eventType); epConfig.addImport(IntoMethod.class); if (timeProvider != null) { epConfig.getEngineDefaults().getThreading().setInternalTimerEnabled(false); } epService = EPServiceProviderManager.getDefaultProvider(epConfig); if (timeProvider != null) { lastTime = timeProvider.getInitialTime(); final EPServiceProviderImpl epService1 = (EPServiceProviderImpl) epService; epService1.initialize(lastTime.getMillis()); } epRuntime = epService.getEPRuntime(); epAdministrator = epService.getEPAdministrator(); config = ConfigUtil.combined(); //injector = Injector.root().createChildInjector(subscribingModule,new Module() injector = Injector.root().createChildInjector(new Module() { @Override public void configure(Binder binder) { // bind this Context binder.bind(Context.class).toInstance(Context.this); } }); injector.setConfig(config); } /** * this class conforms to the callback specs for an Esper subscriber * http://esper.codehaus.org/esper-4.11.0/doc/reference/en-US/html_single/index.html#api-admin-subscriber * then forwards that invocation to the original listener */ private class Listener { public void update(Object[] row) { boolean wasAccessible = method.isAccessible(); method.setAccessible(true); try { method.invoke(delegate, row); } catch (IllegalAccessException | InvocationTargetException e) { throw new EsperError("Could not invoke method " + method + " on statement trigger " + statement, e); } catch (Throwable t) { throw new Error("Error invoking " + delegate.getClass().getName() + "." + method.getName(), t); } finally { method.setAccessible(wasAccessible); } } private Listener(Object delegate, Method method, String statement) { this.delegate = delegate; this.method = method; this.statement = statement; } private final Object delegate; private final Method method; private final String statement; } protected static Logger log = LoggerFactory.getLogger(Context.class); protected static ExecutorService contextService = Executors.newFixedThreadPool(1); private Configuration config; private Injector injector; private TimeProvider timeProvider; private Instant lastTime = null; private EPServiceProvider epService; private EPRuntime epRuntime; private EPAdministrator epAdministrator; private final com.espertech.esper.client.Configuration epConfig = new com.espertech.esper.client.Configuration(); private void privateDestroy() { epService.destroy(); // null all the variables here to eliminate any crazy cycles config = null; injector = null; timeProvider = null; lastTime = null; epService = null; epRuntime = null; epAdministrator = null; ScheduledExecutorService svc = Executors.newSingleThreadScheduledExecutor(); Runnable garbageCollection = new Runnable() { @Override public void run() { Runtime.getRuntime().gc(); } }; svc.schedule(garbageCollection, 1, TimeUnit.MILLISECONDS); } }