package knorxx.framework.generator.single; import com.google.common.base.Optional; import com.google.common.collect.Maps; import com.sun.source.tree.CompilationUnitTree; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.lang.model.type.TypeMirror; import knorxx.framework.generator.JavaFileWithSource; import knorxx.framework.generator.reloading.ReloadingClassLoader; import knorxx.framework.generator.single.api.ApiChangeException; import knorxx.framework.generator.single.api.ApiMethodNameChangeException; import knorxx.framework.generator.single.api.ApiMethodParamterChangeException; import knorxx.framework.generator.single.api.ApiReflectionException; import knorxx.framework.generator.util.JavaIdentifierUtils; import org.stjs.generator.GenerationContext; import org.stjs.generator.Generator; import org.stjs.generator.GeneratorConfiguration; import org.stjs.generator.GeneratorConfigurationBuilder; import org.stjs.generator.javascript.JavaScriptBuilder; import org.stjs.generator.javascript.rhino.RhinoJavaScriptBuilder; import org.stjs.generator.name.DefaultJavaScriptNameProvider; import org.stjs.generator.name.JavaScriptNameProvider; import org.stjs.generator.plugin.GenerationPlugins; import org.stjs.generator.transformation.ByteCodeLoader; import org.stjs.generator.transformation.SourceCodeLoader; import org.stjs.javascript.Window; import org.stjs.javascript.annotation.Namespace; import org.stjs.javascript.annotation.STJSBridge; import org.stjs.javascript.annotation.SyntheticType; /** * * @author sj */ public class StjsSingleFileGenerator extends SingleFileGenerator { @Override public JavaScriptResult generate(JavaFileWithSource<?> javaFile, final ClassLoader classLoader, Set<String> allowedPackages) throws SingleFileGeneratorException { JavaFileWithExposedSourceFile javaFileWithExposedSourceFile = new JavaFileWithExposedSourceFile(javaFile); Class<?> clazz = javaFile.getJavaClass(); GeneratorConfigurationBuilder configurationBuilder = new GeneratorConfigurationBuilder(); configurationBuilder.allowedPackage(javaFileWithExposedSourceFile.getJavaClass().getPackage().getName()); for(String allowedPackage : allowedPackages) { configurationBuilder.allowedPackage(allowedPackage); } configurationBuilder.sourceEncoding("UTF-8"); configurationBuilder.generateSourceMap(true); configurationBuilder.sourceCodeLoader(new SourceCodeLoader() { @Override public String load(Class clazz, String javaSourceCode) { Annotation namespace = clazz.getAnnotation(Namespace.class); // TODO Add more things... (e.g. templates?) if (namespace != null && !javaSourceCode.contains("@Namespace")) { // TODO This is more like a hack... ;-( javaSourceCode = javaSourceCode.replaceFirst(Pattern.quote("public "), "@org.stjs.javascript.annotation.Namespace(\"" + ((Namespace) namespace).value() + "\")\npublic "); } return javaSourceCode; } }); configurationBuilder.byteCodeLoader(new ByteCodeLoader() { @Override public InputStream load(String javaClassName, URI uri) throws IOException { if (classLoader instanceof ReloadingClassLoader) { Optional<byte[]> byteCode = ((ReloadingClassLoader) classLoader).getByteCode(javaClassName); if(byteCode.isPresent()) { return new ByteArrayInputStream(byteCode.get()); } } return uri.toURL().openStream(); } }); JavaScriptNameProvider names = new DefaultJavaScriptNameProvider() { @Override public String getTypeName(GenerationContext<?> context, TypeMirror type) { String typeName = super.getTypeName(context, type); // NOTE Using toString() here seems to work currently but is super hacky and might break in the future... ;-( if(!type.toString().equals(typeName) && !type.toString().contains("$")) { try { String className = type.toString(); className = className.contains("<") ? className.substring(0, className.indexOf("<")) : className; Class javaClass = classLoader.loadClass(className); if(JavaIdentifierUtils.hasSuperclassOrImplementsInterface(javaClass, Enum.class.getName())) { return type.toString(); } } catch (ClassNotFoundException ex) { context.addError(context.getCurrentPath().getLeaf(), "Error while resolving the class '" + type.toString() + "' for namespace generation of an external enum type."); } } return typeName; } }; GenerationPlugins<Object> currentClassPlugins = new GenerationPlugins<>().forClass(clazz); GeneratorConfiguration configuration = configurationBuilder.build(); Map<GenerationContext.AnnotationCacheKey, Object> cacheAnnotations = Maps.newHashMap(); GenerationContext<Object> context = new GenerationContext<>(javaFileWithExposedSourceFile.getSourceFile(), configuration, names, null, classLoader, cacheAnnotations, (JavaScriptBuilder) new RhinoJavaScriptBuilder()); try { clazz = classLoader.loadClass(javaFile.getJavaClassName()); } catch (ClassNotFoundException ex) { throw new SingleFileGeneratorException(ex, javaFileWithExposedSourceFile.getSourceFile()); } CompilationUnitTree cu = parseAndResolve(clazz, javaFileWithExposedSourceFile.getSourceFile(), context, classLoader, configuration.getSourceEncoding(), configuration.getSourceCodeLoader(), configuration.getByteCodeLoader()); // check the code currentClassPlugins.getCheckVisitor().scan(cu, (GenerationContext) context); context.getChecks().check(); // generate the javascript code Object javascriptRoot = currentClassPlugins.getWriterVisitor().scan(cu, context); // check for any error arriving during writing context.getChecks().check(); // write JavaScript StringWriter javaScriptStringWriter = new StringWriter(); context.writeJavaScript(javascriptRoot, javaScriptStringWriter); javaScriptStringWriter.flush(); // write SourceMap StringWriter sourceMapStringWriter = new StringWriter(); try { context.writeSourceMap(sourceMapStringWriter); } catch(IOException ex) { throw new IllegalStateException("An error occured while writing the source map for '" + javaFile.getJavaClassName() + "'.", ex); } sourceMapStringWriter.flush(); return new JavaScriptResult(stripSourceMappingUrl(javaScriptStringWriter.toString()), sourceMapStringWriter.toString()); } private String stripSourceMappingUrl(String javaScriptSource) { int sourceMappingUrlIndex = javaScriptSource.lastIndexOf("//@ sourceMappingURL="); if(sourceMappingUrlIndex < 0) { throw new ApiChangeException("The source mapping URL is missing in the generated file!"); } return javaScriptSource.substring(0, sourceMappingUrlIndex).trim(); } @Override public boolean isGeneratable(Class<?> javaClass) { return super.isGeneratable(javaClass) && !javaClass.isAnnotation() && javaClass.getAnnotation(STJSBridge.class) == null && javaClass.getAnnotation(SyntheticType.class) == null && !javaClass.getPackage().getName().startsWith(Window.class.getPackage().getName()); } /** * This special subclass of JavaFileWithSource exposes the underlying file object of the Java source file. * Normally we NEVER want that because we prefere InputStream for the shake of easy testability. */ private static final class JavaFileWithExposedSourceFile<T> extends JavaFileWithSource<T> { public JavaFileWithExposedSourceFile(JavaFileWithSource<T> javaFileWithSource) { super(javaFileWithSource); } public File getSourceFile() { if(sourceFile == null) { throw new IllegalStateException("The Java class '" + getJavaClassName() + "' is not accessible via file!"); } return sourceFile; } } private CompilationUnitTree parseAndResolve(Class clazz, File inputFile, GenerationContext<?> context, ClassLoader builtProjectClassLoader, String sourceEncoding, SourceCodeLoader sourceCodeLoader, ByteCodeLoader byteCodeLoader) throws SingleFileGeneratorException { Generator generator = new Generator(); try { for (Method method : generator.getClass().getDeclaredMethods()) { Class<?>[] parameterTypes = method.getParameterTypes(); if (method.getName().equals("parseAndResolve")) { if(parameterTypes.length == 7 && parameterTypes[0].equals(Class.class) && parameterTypes[1].equals(File.class) && parameterTypes[2].equals(GenerationContext.class) && parameterTypes[3].equals(ClassLoader.class) && parameterTypes[4].equals(String.class) && parameterTypes[5].equals(SourceCodeLoader.class) && parameterTypes[6].equals(ByteCodeLoader.class)) { method.setAccessible(true); return (CompilationUnitTree) method.invoke(generator, clazz, inputFile, context, builtProjectClassLoader, sourceEncoding, sourceCodeLoader, byteCodeLoader); } throw new ApiMethodParamterChangeException("The method 'parseAndResolve' in the class '" + Generator.class.getName() + "has different parameters than [] now!"); } } } catch (SecurityException | IllegalAccessException | IllegalArgumentException ex) { throw new ApiReflectionException("Error while accessing the method 'parseAndResolve' of '" + Generator.class.getName() + "' via reflection!", ex); } catch(InvocationTargetException ex) { throw new SingleFileGeneratorException(ex.getCause(), inputFile); } throw new ApiMethodNameChangeException("Can't find a method with the name 'parseAndResolve' in the class '" + Generator.class.getName() + "'!"); } }