package xapi.jre.inject;
import xapi.annotation.inject.InstanceDefault;
import xapi.annotation.inject.InstanceOverride;
import xapi.annotation.inject.SingletonDefault;
import xapi.annotation.inject.SingletonOverride;
import xapi.bytecode.ClassFile;
import xapi.bytecode.annotation.Annotation;
import xapi.bytecode.annotation.ArrayMemberValue;
import xapi.bytecode.annotation.ClassMemberValue;
import xapi.bytecode.annotation.IntegerMemberValue;
import xapi.bytecode.annotation.MemberValue;
import xapi.collect.api.Fifo;
import xapi.collect.impl.SimpleFifo;
import xapi.dev.scanner.api.ClasspathScanner;
import xapi.dev.scanner.impl.ClasspathResourceMap;
import xapi.dev.scanner.impl.ClasspathScannerDefault;
import xapi.except.NotConfiguredCorrectly;
import xapi.fu.In2;
import xapi.inject.X_Inject;
import xapi.inject.api.PlatformChecker;
import xapi.inject.impl.JavaInjector;
import xapi.log.X_Log;
import xapi.platform.Platform;
import xapi.time.X_Time;
import xapi.time.api.Moment;
import xapi.time.impl.ImmutableMoment;
import xapi.util.X_Runtime;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RuntimeInjector implements In2<String, PlatformChecker> {
@Override
public void in(final String targetDir, PlatformChecker selector) {
final String prefix = "META-INF"+File.separator;
writeMetaInfo(targetDir, selector, prefix+"singletons", prefix+"instances");
}
@SuppressWarnings("unchecked")
public void writeMetaInfo(String targetDir, PlatformChecker checker, final String singletonDir, final String instanceDir){
if (!targetDir.endsWith(File.separator)) {
targetDir += File.separator;
}
final File target = new File(targetDir).getAbsoluteFile();
if (!target.isDirectory()) {
if (!target.mkdirs()) {
throw new RuntimeException("Unable to get or make jre injection generator output directory: "+
target.getAbsolutePath());
}
}
ClasspathScanner scanner;
try {
scanner = X_Inject.instance(ClasspathScanner.class);
} catch (final Exception e) {
scanner = new ClasspathScannerDefault();
}
final Moment start = now();
final ClasspathResourceMap resources = scanner
.scanAnnotations(
Platform.class,
SingletonDefault.class, SingletonOverride.class,
InstanceDefault.class, InstanceOverride.class
)
.matchResource("META[-]INF/(instances|singletons)")
.matchClassFile(".*")
.scan(targetClassloader())
;
// Only collect platform types if we are not running in a known platform.
final String runtime[] = checker.getRuntime();
final Fifo<ClassFile> defaultSingletons = new SimpleFifo<ClassFile>();
final Fifo<ClassFile> defaultInstances = new SimpleFifo<ClassFile>();
final Fifo<ClassFile> singletonImpls = new SimpleFifo<ClassFile>();
final Fifo<ClassFile> instanceImpls = new SimpleFifo<ClassFile>();
ClassFile bestMatch = null;
final HashMap<String,ClassFile> platformMap = new HashMap<String, ClassFile>();
String shortName = null;
final Set<String> scopes = new LinkedHashSet<String>();
final ArrayList<ClassFile> platforms = new ArrayList<ClassFile>();
final Moment prepped = now();
for (final ClassFile file : resources.findClassAnnotatedWith(Platform.class)) {
platforms.add(file);
for (final String platform : runtime) {
scopes.add(platform);
shortName = platform.substring(platform.lastIndexOf('.')+1);
platformMap.put(file.getName(), file);
if (file.getName().equals(platform)) {
bestMatch = file;
}
if (bestMatch == null && file.getName().endsWith(shortName)) {
bestMatch = file;
}
}
}
final Moment scanned = now();
if (bestMatch == null) {
throw platformMisconfigured(shortName);
}
MemberValue fallbacks;
final LinkedHashSet<ClassFile> remainder = new LinkedHashSet<ClassFile>();
remainder.add(bestMatch);
while (remainder.size()>0) {
final ClassFile next = remainder.iterator().next();
final Annotation anno = next.getAnnotation("xapi.platform.Platform");
if (anno == null) {
throw platformMisconfigured(next.getName());
}
fallbacks = anno.getMemberValue("fallback");
List<String> names = new ArrayList<>();
names.add(anno.getTypeName());
if (fallbacks != null) {
final ArrayMemberValue arr = (ArrayMemberValue)fallbacks;
for (final MemberValue v : arr.getValue()) {
final String name = ((ClassMemberValue)v).getValue();
final ClassFile fallback = platformMap.get(name);
if (fallback != null) {
names.add(name);
if (scopes.add(fallback.getName())) {
remainder.add(fallback);
}
}
}
}
checker.addPlatform(targetClassloader(), anno.getTypeName(), names);
remainder.remove(next);
}
final Moment checked = now();
for (final ClassFile cls : resources.findClassAnnotatedWith(
SingletonDefault.class, SingletonOverride.class,
InstanceDefault.class, InstanceOverride.class
)) {
Annotation anno = cls.getAnnotation(SingletonDefault.class.getName());
if (anno != null && allowedPlatform(cls, scopes, platforms)) {
if (needsSingletonBinding(cls)) {
defaultSingletons.give(cls);
}
}
anno = cls.getAnnotation(SingletonOverride.class.getName());
if (anno != null && allowedPlatform(cls, scopes, platforms)) {
if (needsSingletonBinding(cls)) {
singletonImpls.give(cls);
}
}
anno = cls.getAnnotation(InstanceDefault.class.getName());
if (anno != null && allowedPlatform(cls, scopes, platforms)) {
if (needsInstanceBinding(cls)) {
defaultInstances.give(cls);
}
}
anno = cls.getAnnotation(InstanceOverride.class.getName());
if (anno != null && allowedPlatform(cls, scopes, platforms)) {
if (needsInstanceBinding(cls)) {
instanceImpls.give(cls);
}
}
}
final Moment mapped = now();
final Map<String, ClassFile> injectionTargets = new HashMap<String,ClassFile>();
//determine injection by priority. Defaults first
for (final ClassFile cls : defaultSingletons.forEach()){
final Annotation impl = cls.getAnnotation(SingletonDefault.class.getName());
final ClassMemberValue value = (ClassMemberValue)impl.getMemberValue("implFor");
injectionTargets.put(value.getValue(), cls);
}
//now scan overrides
for (final ClassFile cls : singletonImpls.forEach()){
final Annotation impl = cls.getAnnotation(SingletonOverride.class.getName());
final ClassMemberValue value = (ClassMemberValue)impl.getMemberValue("implFor");
final String clsName = value.getValue();
final ClassFile existing = injectionTargets.get(value.getValue());
if (existing == null){
injectionTargets.put(clsName, cls);
continue;
}
final ClassFile previous = injectionTargets.get(clsName);
// previous value was a default
if (previous == null){
injectionTargets.put(clsName, cls);
continue;
}
final Annotation oldOverride = previous.getAnnotation(SingletonOverride.class.getName());
if (oldOverride == null) {
injectionTargets.put(clsName, cls);
continue;
}
final IntegerMemberValue oldPriority = (IntegerMemberValue)oldOverride.getMemberValue("priority");
final IntegerMemberValue newPriority = (IntegerMemberValue)impl.getMemberValue("priority");
if (newPriority == null) {
continue;
}
if (oldPriority == null || newPriority.getValue() > oldPriority.getValue()){
injectionTargets.put(clsName, cls);
}
}
final Moment startInject = now();
for (final String iface : injectionTargets.keySet()) {
JavaInjector.registerSingletonProvider(iface, injectionTargets.get(iface).getName());
}
try {
writeMeta(injectionTargets, new File(target, singletonDir));
} catch( final Exception e) {e.printStackTrace();}
injectionTargets.clear();
//determine injection by priority
for (final ClassFile cls : defaultInstances.forEach()){
final Annotation impl = cls.getAnnotation(InstanceDefault.class.getName());
final ClassMemberValue value = (ClassMemberValue)impl.getMemberValue("implFor");
injectionTargets.put(value.getValue(), cls);
}
for (final ClassFile cls : instanceImpls.forEach()){
final Annotation impl = cls.getAnnotation(InstanceOverride.class.getName());
final ClassMemberValue value = (ClassMemberValue)impl.getMemberValue("implFor");
final String clsName = value.getValue();
final ClassFile existing = injectionTargets.get(value.getValue());
if (existing == null){
injectionTargets.put(clsName, cls);
continue;
}
final ClassFile previous = injectionTargets.get(clsName);
// previous value was a default
if (previous == null){
injectionTargets.put(clsName, cls);
continue;
}
final Annotation oldOverride = previous.getAnnotation(InstanceOverride.class.getName());
if (oldOverride == null) {
injectionTargets.put(clsName, cls);
continue;
}
final IntegerMemberValue oldPriority = (IntegerMemberValue)oldOverride.getMemberValue("priority");
final IntegerMemberValue newPriority = (IntegerMemberValue)impl.getMemberValue("priority");
if (newPriority.getValue() > oldPriority.getValue()){
injectionTargets.put(clsName, cls);
}
}
for (final String iface : injectionTargets.keySet()) {
JavaInjector.registerInstanceProvider(iface, injectionTargets.get(iface).getName());
}
try{
writeMeta(injectionTargets, new File(target, instanceDir));
} catch (final Throwable e) {
X_Log.warn("Trouble encountered writing instance meta to ",new File(target, instanceDir),e);
}
final Moment finished = now();
if (X_Runtime.isDebug()) {
X_Log.info("Multithreaded? ", X_Runtime.isMultithreaded());
X_Log.info("Prepped: ", X_Time.difference(start, prepped));
X_Log.info("Scanned: ", X_Time.difference(prepped, scanned));
X_Log.info("Checked: ", X_Time.difference(scanned, checked));
X_Log.info("Mapped: ", X_Time.difference(checked, mapped));
X_Log.info("Analyzed: ", X_Time.difference(mapped, startInject));
X_Log.info("Injected: ", X_Time.difference(startInject, finished));
X_Log.info("Total: ", X_Time.difference(start, finished));
}
}
protected boolean needsSingletonBinding(ClassFile cls) {
String name = "META-INF/singletons/" + cls.getQualifiedName();
return targetClassloader().getResource(name) == null;
}
protected boolean needsInstanceBinding(ClassFile cls) {
String name = "META-INF/instances/" + cls.getQualifiedName();
return targetClassloader().getResource(name) == null;
}
/**
* @return
*/
protected ClassLoader targetClassloader() {
return Thread.currentThread().getContextClassLoader();
}
private final double init = System.nanoTime();
private Moment now() {
return new ImmutableMoment(System.currentTimeMillis()+ Math.abs(System.nanoTime()-init)/100000000.0);
}
private boolean allowedPlatform(final ClassFile cls, final Set<String> scopes, final Iterable<ClassFile> platforms) {
// first, if the given class file has a platform we are using, return true
for (final String allowed : scopes) {
if (cls.getAnnotation(allowed) != null) {
return true;
}
}
// next, check if the type has any known platforms
for (final ClassFile platform : platforms) {
if (cls.getAnnotation(platform.getName())!=null) {
System.out.println("Skipping "+cls.getName()+" for having a platform " +
"the does not match the current runtime.");
return false;
}
}
// this is a universal class (no platform specified)
return true;
}
private NotConfiguredCorrectly platformMisconfigured(final String platform) {
return new NotConfiguredCorrectly("Could not find platform annotation for " +
"current runtime "+platform+"; please ensure this class is on the " +
"classpath, and that it is annotated with @Platform");
}
protected void writeMeta(final Map<String, ClassFile> injectables,
final File target) {
X_Log.info(getClass(), "Writing meta to ",target.getAbsoluteFile());
if (!target.exists()) {
if (!target.mkdirs()) {
throw new RuntimeException("Unable to create meta info directory for "+target.getAbsolutePath());
}
}
mainLoop:
for (final String iface : injectables.keySet()){
final File metaInf = new File(target, iface);
final String impl = injectables.get(iface).getName();
X_Log.debug("Injecting ",iface," -> ",impl);
try{
if (metaInf.exists()){
// when the file exists, we must append to the top of it,
// but only if it doesn't already contain our target.
final BufferedReader reader = new BufferedReader(new FileReader(metaInf));
final ArrayList<String> lines = new ArrayList<String>();
try {
String line;
while((line = reader.readLine()) != null){
if (line.equals(impl)) {
continue mainLoop;
}
}
}finally {
reader.close();
}
final BufferedWriter writer = new BufferedWriter(new FileWriter(metaInf));
try {
writer.append(impl);
writer.append("\n");
for (final String line : lines){
writer.append(line);
writer.append("\n");
}
writer.flush();
}finally {
writer.close();
}
}else{
if (!metaInf.createNewFile()) {
throw new RuntimeException("Unable to create meta info for "+metaInf.getAbsolutePath()+". " +
"Please ensure java has write permissions for "+metaInf.getParent());
}
final FileWriter writer = new FileWriter(metaInf);
try {
writer.append(impl);
writer.flush();
}finally {
writer.close();
}
}
}catch(final Exception e){
throw new RuntimeException("Unable to create meta info for "+metaInf.getAbsolutePath(), e);
}
}
}
}