package spimedb; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.core.Appender; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; import ch.qos.logback.core.util.FileSize; import com.google.common.io.Files; import jcog.Texts; import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; import org.apache.commons.io.monitor.FileAlterationMonitor; import org.apache.commons.io.monitor.FileAlterationObserver; import org.eclipse.collections.api.tuple.Pair; import org.eclipse.collections.impl.tuple.Tuples; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mockito.internal.util.reflection.BeanPropertySetter; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spimedb.util.Locker; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.function.Function; /** * "Main.java" - a file-system driven, dynamically reconfiguring, dependency-injection container * designed to make DI as fun as mainlining an endless supply of ___ */ public abstract class Main extends FileAlterationListenerAdaptor { public final static Logger logger = LoggerFactory.getLogger(Main.class); private static final ch.qos.logback.classic.Logger LOG; static { Thread.currentThread().setName("$"); //http://logback.qos.ch/manual/layouts.html LOG = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerContext loggerContext = LOG.getLoggerContext(); loggerContext.reset(); SpimeDB.LOG(Logger.ROOT_LOGGER_NAME, Level.INFO); SpimeDB.LOG(Reflections.log, Level.WARN); SpimeDB.LOG("logging", Level.WARN); } /** * live objects */ final Map<Pair<Class, String>, Object> obj = new ConcurrentHashMap(); final Map<String, Class> klassPath = new ConcurrentHashMap<>(); @Nullable private final FileAlterationObserver fsObserver; protected Pair<Class, String> key(File f) { String fileName = f.getName(); if (fileName.startsWith(".")) return null; //ignore hidden files String absolutePath = f.getAbsolutePath(); if (absolutePath == null) return null; return key(fileName, absolutePath); } @Nullable protected Pair<Class, String> key(String fileName, String absolutePath) { String[] parts = fileName.split("."); String id; Class klass; if (parts.length < 1) { /** if only one filename component: * if this string resolves in the klasspath, then that's what it is and also its name * else default to 'String' */ Class specificclass = klassPath.get(fileName); if (specificclass == null) { id = fileName; klass = String.class; } else { klass = specificclass; id = ""; //designated for the singleton } } else if (parts.length >= 2) { String classSting = parts[0]; klass = klass(classSting); id = parts[1]; } else { return null; } return key(klass, id); } public static Pair<Class, String> key(Class klass) { return key(klass, ""); } @NotNull public static Pair<Class, String> key(Class klass, String id) { return Tuples.pair(klass, id); } public Class klass(String klass) { return klassPath.get(klass); } @Override public void onFileCreate(File file) { updateFile(file); } @Override public void onFileChange(File file) { updateFile(file); } protected void updateFile(File file) { Pair<Class, String> k = key(file); if (k == null) return; logger.info("reload file://{}", file); merge(k, build(k, file)); } protected void updateDirectory(File d) { //impl in subclasses } @Override public void onFileDelete(File file) { Pair<Class, String> k = key(file); if (k == null) return; remove(k); } final Locker<Pair<Class, String>> locker = new Locker(); private Object merge(Pair<Class, String> k, Function build) { Lock l = locker.get(k); l.lock(); try { if (build == null) { Object v = obj.remove(k); logger.info("remove {}: {}", k, v); return v; } else { return obj.compute(k, (kk, existing) -> build.apply(existing)); } } finally { l.unlock(); } } /** * the function returned will accept the previous value (null if there was none) and return a result * * @param file * @param klass */ @NotNull private Function build(Pair<Class, String> id, File file) { String[] parts = file.getName().split("."); switch (parts.length) { case 0: case 1: //string if (id.getOne() == String.class) { return new FileToString(file); } else { return buildDefault(id, file); } case 2: //properties file return buildDefault(id, file); case 3: switch (parts[3]) { case "js": //javascript break; case "json": break; case "xml": break; case "java": //dynamically recompiled java break; } break; } return (x) -> { logger.info("unbuildable: {}", file); return x; }; } @NotNull private Function buildDefault(Pair<Class, String> id, File file) { if (id.getOne() == LogConfigurator.class) { return (Object x) -> { LogConfigurator newConfig = new LogConfigurator(file); if (x instanceof LogConfigurator) { LogConfigurator old = (LogConfigurator) x; LoggingLogger.info("stop {}", old.message); old.stop(); } LoggingLogger.info("start {}", newConfig.message); return newConfig; }; } else { return new PropertyBuilder(id, file); } } static final Class[] spimeDBConstructor = new Class[]{SpimeDB.class}; public <X extends Plugin> Main with(Class<X> c) { //TODO return this; } public class PropertyBuilder<X> implements Function<X, X> { public final File file; private final Pair<Class, String> id; public PropertyBuilder(Pair<Class, String> id, File file) { this.id = id; this.file = file; } @Override public X apply(X x) { Class cl = id.getOne(); if (x == null) { //HACK TODO use some adaptive constructor argument injector for (Constructor c : cl.getConstructors()) { Class[] types = c.getParameterTypes(); Object[] param = defaultConstructorArgs(types); try { logger.info("start {}", x); x = (X) c.newInstance(param); } catch (Exception e) { logger.error("error instantiating {} {} {} {}", file, cl, c, e.getMessage()); return null; } } } Properties p = new Properties(); try { p.load(new FileInputStream(file)); } catch (IOException e) { logger.error("properties configure {} {} {}", file, cl, e.getMessage()); } for (Map.Entry e : p.entrySet()) { Object k = e.getKey().toString(); Object v = e.getValue(); String field = k.toString(); Field f = field(cl, field); if (f != null) { //TODO better type decoding BeanPropertySetter s = new BeanPropertySetter(x, f); try { switch (f.getType().toString()) { case "float": v = Texts.f(v.toString()); break; case "int": v = Texts.i(v.toString()); break; } logger.info("{}.{}={}", x, field, v); s.set(v); } catch (IllegalArgumentException aa) { logger.info("invalid type {} for {}", v.getClass(), f); } } else { logger.info("unknown field {} {}", file, field); } } return x; } } /** HACK TODO make not required */ @NotNull abstract protected Object[] defaultConstructorArgs(Class[] types); private static Field field(Class cl, String field) { //TODO add a reflection cache try { return cl.getDeclaredField(field); } catch (NoSuchFieldException e) { return null; } } public <X> Object put(Class k, X v) { return put(key(k), v); } public <X> X get(Class<? extends X> k) { return (X) obj.get(key(k)); } public <X> Object put(Pair<Class, String> k, X v) { if (v == null) { return remove(k); } Object existing = obj.put(k, v); if (existing != v) { logger.info("{} = {}", k, v); if (existing != null) { //logger.info("{} = {}", k, v); } else { //logger.info("{} = {} <== {}", k, v, existing); onRemove(k, existing); } onAdd(k, existing); } return existing; } protected void onAdd(Pair<Class, String> k, Object v) { } protected void onRemove(Pair<Class, String> k, Object v) { } public <X> X get(Pair<Class<? extends X>, String> k) { if (k == null) return null; return (X) obj.get(k); } public Object remove(Pair<Class, String> k) { return merge(k, null); } public final static Logger LoggingLogger = LoggerFactory.getLogger(LOG.getClass()); /** HACK TODO make not a requirement */ abstract String workingDirectory(); class LogConfigurator { Appender appender; String message; public LogConfigurator(File f) { Appender appender = null; if (f != null) { try { String line = Files.readFirstLine(f, Charset.defaultCharset()).trim(); switch (line) { case "rolling": String logFile = workingDirectory(); message = ("rolling to file://{}" + logFile); RollingFileAppender r = new RollingFileAppender(); r.setFile(logFile); r.setAppend(true); SizeBasedTriggeringPolicy triggeringPolicy = new ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy(); triggeringPolicy.setMaxFileSize(FileSize.valueOf("5MB")); triggeringPolicy.start(); FixedWindowRollingPolicy policy = new FixedWindowRollingPolicy(); policy.setFileNamePattern("log.%i.gz"); policy.setParent(r); policy.setContext(LOG.getLoggerContext()); policy.start(); r.setEncoder(logEncoder()); r.setRollingPolicy(policy); r.setTriggeringPolicy(triggeringPolicy); appender = r; break; //TODO TCP/UDP etc } } catch (IOException e) { logger.error("{}", e); appender = null; } } if (appender == null) { //default ConsoleAppender a = new ConsoleAppender(); a.setEncoder(logEncoder()); appender = a; message = "ConsoleAppender"; } LOG.detachAndStopAllAppenders(); appender.setContext(LOG.getLoggerContext()); appender.start(); LOG.addAppender(appender); this.appender = appender; } private Encoder logEncoder() { PatternLayoutEncoder logEncoder = new PatternLayoutEncoder(); logEncoder.setContext(LOG.getLoggerContext()); logEncoder.setPattern("%highlight(%logger{0}) %green(%thread) %message%n"); logEncoder.setImmediateFlush(true); logEncoder.start(); return logEncoder; } public void stop() { if (appender != null) { appender.stop(); appender = null; } } } public Main(String path, Map<String, Class> initialKlassPath) throws Exception { klassPath.putAll(initialKlassPath); //setup default klasspath klassPath.putIfAbsent("log", LogConfigurator.class); put(LogConfigurator.class, new LogConfigurator(null)); //new Multimedia(db); Set<Class<? extends Plugin>> plugins = new Reflections("spimedb") .getSubTypesOf(Plugin.class); plugins.forEach(c -> { String id = c.getSimpleName().toLowerCase(); logger.warn("Plugin available: {}", id); klassPath.put(id, c); }); if (path != null) { fsObserver = new FileAlterationObserver(path); logger.info("watching file://{}", path); /* http://www.baeldung.com/java-watchservice-vs-apache-commons-io-monitor-library */ int updatePeriodMS = 200; FileAlterationMonitor monitor = new FileAlterationMonitor(updatePeriodMS); //monitor.setThreadFactory(Executors.defaultThreadFactory()); fsObserver.addListener(this); monitor.addObserver(fsObserver); monitor.start(); } else { fsObserver = null; } // @Override // public synchronized void clear(boolean rebuild) { // super.clear(rebuild); // System.exit(2); // } // }; } /** reload files */ protected Main restart() { if (fsObserver != null) reload(fsObserver); return this; } private void reload(FileAlterationObserver observer) { for (File f : observer.getDirectory().listFiles()) { if (f.getName().startsWith(".")) continue; //ignore hidden files //exe.submit(0.9f, () -> { if (f.isFile()) { updateFile(f); } else if (f.isDirectory()) { updateDirectory(f); } //}); } } // public void put(Class c, String id, Object value) { // put(key(c, id), value); // } private static class FileToString implements Function { private final File file; public FileToString(File file) { this.file = file; } @Override public Object apply(Object o) { try { return Files.toString(file, Charset.defaultCharset()); } catch (IOException e) { logger.error("reading string: {}", file); return null; } } } // final static String cachePath = "cache"; // // public static void main(String[] args) throws Exception { // // /*final static String eq4_5week = "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.atom"; // // public USGSEarthquakes() { // super("USGSEarthquakes", eq4_5week, 128);*/ // // // //new IRCBot(s.db, "RAWinput", "irc.freenode.net", "#netention"); // // // HttpCache httpCache = new HttpCache(cachePath); // // // // //Self s = new Self(); // // Web j = new Web() // .add("/wikipedia", new Wikipedia(httpCache)) //// .add("/api/tag", new PathHandler() { //// //// @Override //// public void handleRequest(HttpServerExchange exchange) throws Exception { //// //// ArrayList<Object> av = Lists.newArrayList(s.allValues()); //// byte[] b = Core.jsonAnnotated.writeValueAsBytes(av); //// Web.send(b, exchange, "application/json" ); //// } //// }) // .add("/", ClientResources.handleClientResources()) // .start("localhost", 8080); // // // // SchemaOrg.load(null); //// logger.info("Loading ClimateViewer (ontology)"); //// new ClimateViewer(s.db); //// logger.info("Loading Netention (ontology)"); //// NOntology.load(s.db); // // //InfiniPeer.local("i", cachePath, 32000); // new ClimateViewerSources() { // // @Override // public void onLayer(String id, String name, String kml, String icon, String currentSection) { // URLSensor r = new URLSensor(currentSection + "/" + id, name, kml, icon); // //p.add(r); // } // // @Override // public void onSection(String name, String id, String icon) { // // } // }; // // //// int webPort = 9090; //// //// SpacetimeWebServer s = SpacetimeWebServer( //// //ElasticSpacetime.temporary("cv", -1), //// ElasticSpacetime.local("cv", "cache", true), //// "localhost", //// webPort); // //// /* //// //EXAMPLES //// { ////// s.add("irc", ////// new IRCBot(s.db, "RAWinput", "irc.freenode.net", "#netention", "#nars" ////// /*"#archlinux", "#jquery"*/).serverChannel ////// ); //// //// s.add("eq", new USGSEarthquakes()); //// //// //// //new IRCBot(s.db, "RAWinput", "irc.freenode.net", "#netention"); //// //new FileTailWindow(s.db, "netlog", "/home/me/.xchat2/scrollback/FreeNode/#netention.txt").start(); //// //// s.add("demo", new SimulationDemo.SimpleSimulation("DroneSquad3")); //// /*s.addPrefixPath("/sim", new WebSocketCore( //// new SimpleSimulation("x") //// ).handler());*/ //// } //// */ // } // // }