package xapi.inject.impl; import xapi.collect.api.InitMap; import xapi.collect.impl.AbstractInitMap; import xapi.collect.impl.InitMapDefault; import xapi.fu.In2; import xapi.inject.api.Injector; import xapi.inject.api.PlatformChecker; import xapi.log.X_Log; import xapi.log.api.LogLevel; import xapi.log.api.LogService; import xapi.log.impl.JreLog; import xapi.util.X_Runtime; import xapi.util.api.ConvertsValue; import xapi.util.impl.ImmutableProvider; import static xapi.util.X_Namespace.*; import javax.inject.Provider; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.Map; public class JreInjector implements Injector { static final String DEFAULT_INJECTOR = "xapi.jre.inject.RuntimeInjector"; private static final class RuntimeProxy extends SingletonProvider<In2<String, PlatformChecker>> implements In2<String, PlatformChecker> { @SuppressWarnings("unchecked") @Override protected In2<String, PlatformChecker> initialValue() { final String injector = System.getProperty( PROPERTY_INJECTOR, DEFAULT_INJECTOR ); try { final Class<?> cls = Class.forName(injector); return (In2<String, PlatformChecker>) cls.newInstance(); } catch (final ClassNotFoundException e) { X_Log.warn(getClass(), "Unable to find injector ", injector, "on the classpath"); } catch (final Exception e) { e.printStackTrace(); final Thread t = Thread.currentThread(); t.getUncaughtExceptionHandler().uncaughtException(t, e); } return this; } @Override public void in(final String value, PlatformChecker checker) { //no-op implementation. } } private static final RuntimeProxy runtimeInjector = new RuntimeProxy(); private boolean initOnce = true; private final PlatformChecker checker; public JreInjector() { checker = createPlatformChecker(); if (checker.needsInject()) { scanClasspath(); } } private static final SingletonProvider<String> instanceUrlFragment = new SingletonProvider<String>() { @Override protected String initialValue() { final String value = System.getProperty( PROPERTY_INSTANCES, DEFAULT_INSTANCES_LOCATION ); return value.endsWith("/") ? value : value + "/"; } ; }; private static final SingletonProvider<String> singletonUrlFragment = new SingletonProvider<String>() { @Override protected String initialValue() { final String value = System.getProperty( PROPERTY_SINGLETONS, DEFAULT_SINGLETONS_LOCATION ); return value.endsWith("/") ? value : value + "/"; } ; }; private final AbstractInitMap<Class<?>, Provider<?>> instanceProviders = InitMapDefault.createInitMap( AbstractInitMap.CLASS_NAME, new ConvertsValue<Class<?>, Provider<?>>() { @Override public Provider<?> convert(final Class<?> clazz) { //First, lookup META-INF/instances for a replacement. final Class<?> cls; try { cls = lookup(clazz, instanceUrlFragment.get(), JreInjector.this, instanceProviders); if (cls == clazz && instanceProviders.containsKey(cls)) { return instanceProviders.get(cls); } return ()->{ try { return cls.newInstance(); } catch (final Exception e) { e.printStackTrace(); throw new RuntimeException("Could not instantiate new instance of " + cls.getName() + " : " + clazz, e); } }; } catch (final Exception e) { if (instanceProviders.containsKey(clazz)) { return instanceProviders.get(clazz); } throw new RuntimeException("Could not create instance provider for " + clazz.getName() + " : " + clazz, e); } } } ); /** * Rather than use java.util.ServiceLoader, which only works in Java >= 6, * and which can cause issues with android+proguard, * we use our own simplified version of ServiceLoader, * which can allow us to change the target directory from META-INF/singletons, * and caches singletons internally. * <p> * Note that this method will use whatever ClassLoader loaded the key class. */ private final InitMap<Class<?>, Provider<Object>> singletonProviders = InitMapDefault.createInitMap(AbstractInitMap.CLASS_NAME, new ConvertsValue<Class<?>, Provider<Object>>() { @Override public Provider<Object> convert(final Class<?> clazz) { //TODO: optionally run through java.util.ServiceLoader, //in case client code already uses ServiceLoader directly (unlikely edge case) Class<?> cls; try { //First, lookup META-INF/singletons for a replacement. cls = lookup(clazz, singletonUrlFragment.get(), JreInjector.this, singletonProviders); if (cls == clazz && singletonProviders.containsKey(cls)) { return singletonProviders.get(cls); } return new ImmutableProvider<>(cls.newInstance()); } catch (final Throwable e) { if (singletonProviders.containsKey(clazz)) { return singletonProviders.get(clazz); } //Try to log the exception, but do not recurse into X_Inject methods if (clazz == LogService.class) { final LogService serv = new JreLog(); final ImmutableProvider<Object> provider = new ImmutableProvider<Object>(serv); singletonProviders.setValue(clazz.getName(), provider); return provider; } e.printStackTrace(); final String message = "Could not instantiate singleton for " + clazz.getName() + " for " + clazz; tryLog(message, e); throw new RuntimeException(message, e); } } }); protected PlatformChecker createPlatformChecker() { return new PlatformChecker(); } private void tryLog(final String message, final Throwable e) { try { final LogService log = (LogService) singletonProviders.get(LogService.class).get(); log.log(LogLevel.ERROR, message); } catch (final Exception ex) { System.err.println(message); ex.printStackTrace(); } } private static Class<?> lookup( final Class<?> cls, String relativeUrl, final JreInjector injector, final InitMap<Class<?>, ?> map ) throws IOException, ClassNotFoundException { final String name = cls.getName(); final ClassLoader loader = cls.getClassLoader(); if (!relativeUrl.endsWith("/")) { relativeUrl += "/"; } Enumeration<URL> resources = loader.getResources(relativeUrl + name); if (resources == null || !resources.hasMoreElements()) { if (injector.initOnce) { injector.initOnce = false; injector.init(cls, map); resources = loader.getResources(relativeUrl + name); if (relativeUrl.contains(instanceUrlFragment.get())) { if (injector.instanceProviders.containsKey(cls)) { return cls; } } else { if (injector.singletonProviders.containsKey(cls)) { return cls; } } } if (resources == null || !resources.hasMoreElements()) { return cls; } } URL resource; Map<Class<?>, Integer> candidates = new LinkedHashMap<>(); while (resources.hasMoreElements()) { resource = resources.nextElement(); final InputStream stream = resource.openStream(); final byte[] into = new byte[stream.available()]; stream.read(into); try { String result = new String(into).split("\n")[0]; String[] bits = result.split("="); try { final Class<?> clazz = Class.forName(bits[0], true, cls.getClassLoader()); Integer newVal = bits.length == 1 ? null : Integer.parseInt(bits[1].trim()); final Integer was = candidates.get(clazz); if (was == null) { candidates.put(clazz, newVal); } else if (newVal != null) { candidates.put(clazz, Math.max(was, newVal)); } } catch (ClassNotFoundException e) { // TODO: warn... } } finally { stream.close(); } } Class<?> best = injector.checker.findBest(candidates); if (best == null) { // TODO: warn return cls; } return best; } protected static boolean isAcceptable(Class<?> clazz) { return true; } @Override @SuppressWarnings("unchecked") public <T, C extends Class<? extends T>> T create(final C cls) { try { return (T) instanceProviders.get(cls).get(); } catch (final Exception e) { if (initOnce) { X_Log.warn("Instance provider failed; attempting runtime injection", e); initOnce = false; init(cls, instanceProviders); return create(cls); } //Try to log the exception, but do not recurse into X_Inject methods final String message = "Could not instantiate instance for " + cls.getName(); tryLog(message, e); throw new RuntimeException(message, e); } } @Override @SuppressWarnings("unchecked") public <T, C extends Class<? extends T>> T provide(final C cls) { try { return (T) singletonProviders.get(cls).get(); } catch (final Exception e) { if (initOnce) { X_Log.warn("Singleton provider failed; attempting runtime injection", e); initOnce = false; init(cls, singletonProviders); return provide(cls); } //Try to log the exception, but do not recurse into X_Inject methods final String message = "Could not instantiate singleton for " + cls.getName(); tryLog(message, e); throw new RuntimeException(message, e); } } protected void init(final Class<?> on, final InitMap<Class<?>, ?> map) { X_Log.warn(getClass(), "X_Inject encountered a class without injection metadata:", on); if (!"false".equals(System.getProperty("xinject.no.runtime.injection"))) { X_Log.info(getClass(), "Attempting runtime injection."); try { scanClasspath(); X_Log.info(getClass(), "Runtime injection success."); } catch (final Exception e) { X_Log.warn(getClass(), "Runtime injection failure.", e); } } } private void scanClasspath() { runtimeInjector.get().in( System.getProperty(PROPERTY_RUNTIME_META, "target/classes"), checker ); } @Override public <T> void setInstanceFactory(final Class<T> cls, final Provider<T> provider) { if (X_Runtime.isDebug()) { X_Log.debug("Setting instance factory for ", cls); } instanceProviders.put(cls, provider); } @Override @SuppressWarnings({"unchecked", "rawtypes"}) // Our target is already erased to Object public <T> void setSingletonFactory(final Class<T> cls, final Provider<T> provider) { if (X_Runtime.isDebug()) { X_Log.debug("Setting singleton factory for ", cls); } singletonProviders.put(cls, (Provider) provider); } public void initialize(final Object o) { } }