package xapi.test.components.bdd; import com.github.javaparser.ASTHelper; import com.github.javaparser.JavaParser; import com.github.javaparser.ParseException; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.MemberValuePair; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.UiContainerExpr; import com.github.javaparser.ast.type.ClassOrInterfaceType; import cucumber.api.java.Before; import cucumber.api.java.en.And; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import org.junit.ComparisonFailure; import xapi.collect.X_Collect; import xapi.collect.api.StringTo; import xapi.components.api.IsWebComponent; import xapi.components.api.WebComponentFactory; import xapi.dev.X_Gwtc; import xapi.dev.components.WebComponentFactoryGenerator; import xapi.dev.gwtc.api.GwtcService; import xapi.dev.source.MethodBuffer; import xapi.dev.source.SourceBuilder.JavaType; import xapi.fu.In1; import xapi.gwtc.api.GwtManifest; import xapi.gwtc.api.GwtManifest.CleanupMode; import xapi.gwtc.api.GwtcXmlBuilder; import xapi.inject.X_Inject; import xapi.io.X_IO; import xapi.javac.dev.api.CompilerService; import xapi.log.X_Log; import xapi.log.api.LogLevel; import xapi.source.X_Source; import xapi.test.components.client.GeneratedComponentEntryPoint; import xapi.util.X_Namespace; import xapi.util.X_Properties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static xapi.collect.X_Collect.toArray; import static xapi.fu.X_Fu.notEmpty; import static xapi.util.X_String.join; import com.google.gwt.core.client.GWT; import com.google.gwt.core.ext.TreeLogger.Type; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; /** * @author James X. Nelson (james@wetheinter.net) * Created on 5/14/16. */ public class GwtcSteps { public static class CompiledComponent { private final GwtcService gwtc; private final CompilationUnit parsed; private final ClassOrInterfaceDeclaration cls; private final GwtManifest manifest; public CompiledComponent( ClassOrInterfaceDeclaration cls, CompilationUnit parsed, GwtcService gwtc, GwtManifest manifest ) { this.cls = cls; this.parsed = parsed; this.gwtc = gwtc; this.manifest = manifest; } public GwtcService getGwtc() { return gwtc; } public CompilationUnit getParsed() { return parsed; } protected String getWebComponentFactorySimpleName() { return WebComponentFactoryGenerator.toFactoryName(cls.getName()); } public File getWebComponentFactoryFile() { String simpleName = getWebComponentFactorySimpleName(); String resourcePath = cls.getPackageAsPath() + simpleName + ".java"; return new File(getGwtc().inGeneratedDirectory(getManifest(), resourcePath)); } public GwtManifest getManifest() { return manifest; } } private static final String QUOTED = "\"([^\"]+)\""; static { X_Log.logLevel(LogLevel.INFO); X_Properties.setProperty(X_Namespace.PROPERTY_LOG_LEVEL, "ALL"); X_Properties.setProperty(X_Namespace.PROPERTY_MULTITHREADED, "10"); } private StringTo<CompiledComponent> compiledComponents; private StringTo<String> sources; @Before public void before() { compiledComponents = X_Collect.newStringMap(CompiledComponent.class); sources = X_Collect.newStringMap(String.class); } @Given("^compile the code:$") public void compileTheCode(List<String> lines) { final Optional<String> first = lines.stream().filter(notEmpty()).findFirst(); if (!first.isPresent()) { throw new AssertionError("No text supplied to compile method. You sent " + lines); } CompilerService compiler = X_Inject.singleton(CompilerService.class); } @Given("^compile the component:$") public void compileTheComponent(List<String> lines) throws ParseException { final Optional<String> first = lines.stream().filter(notEmpty()).findFirst(); if (!first.isPresent()) { throw new AssertionError("No text supplied to compile method. You sent " + lines); } GwtcService service = X_Gwtc.getServiceFor(GeneratedComponentEntryPoint.class); service.addClasspath(GeneratedComponentEntryPoint.class); service.addClasspath(IsWebComponent.class); service.createFile("META-INF", "xapi.properties", ()->""); final GwtManifest manifest = new GwtManifest(service.getModuleName()); initializeManifest(service, manifest); // Now, lets add our component to the Gwt compilation classpath String code = join("\n", toArray(lines)); JavaType type = JavaType.UNKNOWN; String pkgToUse; String clsToUse; try { final CompilationUnit parsed = JavaParser.parse(X_IO.toStreamUtf8(code), "UTF-8"); type = parsed.getPrimaryType().getJavaType(); final int ident = System.identityHashCode(manifest); final String fileName = "Gen" + ident; pkgToUse = parsed.getPackage() == null ? null : parsed.getPackage().getPackageName(); if (pkgToUse == null) { pkgToUse = service.modifyPackage("xapi.test.pkg" + ident); parsed.setPackage(new PackageDeclaration(new NameExpr(pkgToUse))); } final GwtcXmlBuilder builder = manifest.getOrCreateBuilder(pkgToUse, fileName); builder.addSource(""); service.addGwtInherit(builder.getInheritName()); parsed.getImports().add(new ImportDeclaration(new NameExpr("xapi.components.api"), false, true)); parsed.getImports().add(new ImportDeclaration(new NameExpr("xapi.ui.api"), false, true)); clsToUse = parsed.getPrimaryType().getName(); // lets save the code, then generate an inclusion for it. service.createFile(pkgToUse.replace('.', '/'), clsToUse + ".java", parsed::toSource); final MethodBuffer method = service.addMethodToEntryPoint("public void runMethod" + ident); service.getOnModuleLoad() .println(method.getName() + "();"); parsed.getTypes().forEach(t -> { if (t instanceof ClassOrInterfaceDeclaration) { final ClassOrInterfaceDeclaration cls = (ClassOrInterfaceDeclaration) t; In1<ClassOrInterfaceDeclaration> printComponent = coi -> { String print = coi.getName(); String qualifiedName = null; boolean hadName = false; if (print.indexOf('.') == -1) { // check the imports for the correct package for (ImportDeclaration importDecl : parsed.getImports()) { if (importDecl.getName().getName().endsWith(print)) { qualifiedName = importDecl.getName().getName(); hadName = true; break; } } if (!hadName) { qualifiedName = X_Source.qualifiedName(parsed.getPackage().getPackageName(), coi.getName()); } } else { qualifiedName = coi.getName(); } print = method.addImport(qualifiedName); String gwt = method.addImport(GWT.class); final String factory = method.addImport(WebComponentFactory.class); method.print(factory + "<" + print + "> factory = "); method.println(gwt + ".create(" + print + ".class);"); method.println("factory.newComponent();"); final CompiledComponent component = new CompiledComponent(cls, parsed, service, manifest); String tagName = getTagName(cls); compiledComponents.put(qualifiedName, component); compiledComponents.put(tagName, component); }; if (cls.isInterface()) { for (ClassOrInterfaceType iface : cls.getExtends()) { if (iface.getName().endsWith(IsWebComponent.class.getSimpleName())) { printComponent.in(cls); } } } else { for (ClassOrInterfaceType iface : cls.getImplements()) { if (iface.getName().endsWith(IsWebComponent.class.getSimpleName())) { printComponent.in(cls); } } } } else if (t instanceof AnnotationDeclaration) { X_Log.warn( getClass(), "Saw a generated annotation, but we aren't running javac yet; this annotation won't be used in the Gwt compile" ); } else if (t instanceof EnumDeclaration) { X_Log.warn( getClass(), "Saw a generated enum, but we aren't running javac yet; this enum won't be used in the Gwt compile" ); } else { throw new AssertionError("Unsupported type " + t.getClass() + " : " + t); } }); } catch (ParseException e) { // The source of the component is just final UiContainerExpr uiContainer = JavaParser.parseUiContainer(code); } if (type != JavaType.UNKNOWN) { } else { // if it wasn't a valid java file, then it is just a plain UiContainer, // which we should componentize (wrap in a valid java class) } final int result = service.compile(manifest); assertEquals("Compile failed w/ code " + result, 0, result); } private void initializeManifest(GwtcService service, GwtManifest manifest) { manifest.addSystemProp("gwt.usearchives=false"); manifest.setLogLevel(Type.TRACE); manifest.setWorkDir(service.getTempDir().getAbsolutePath()); manifest.setGenDir(manifest.getGenDir()); // make the default explicit, so the argument is sent to command line manifest.setStrict(true); // break on any error manifest.setCleanupMode(CleanupMode.DELETE_ON_SUCCESSFUL_EXIT); } private String getTagName(ClassOrInterfaceDeclaration cls) { for (AnnotationExpr anno : cls.getAnnotations()) { if (anno.getName().getSimpleName().equals("WebComponent")) { if (anno instanceof NormalAnnotationExpr) { for (MemberValuePair pair : ((NormalAnnotationExpr) anno).getPairs()) { if (pair.getName().equals("tagName")) { String val = ASTHelper.extractAnnoValue(pair); if (val != null) { return val; } else { X_Log.error( getClass(), "Type of annotation value not supported: " + pair.getValue().getClass() + " : " + pair.toSource() ); throw new AssertionError("Must use string literals for tagName values of @WebComponent attributes"); } } } } } } throw new AssertionError("Unable to find the generated tag name from component " + cls.toSource()); } @And("^save generated source of component " + QUOTED + " as " + QUOTED + "$") public void saveGeneratedSourceOfComponentAs(String componentName, String targetName) throws Throwable { final CompiledComponent component = compiledComponents.get(componentName); if (component == null) { throw new NullPointerException(); } File location = component.getWebComponentFactoryFile(); String fileContents; try (FileInputStream in = new FileInputStream(location)) { fileContents = X_IO.toStringUtf8(in); } sources.put(targetName, fileContents); } @Then("^confirm source \"([^\"]*)\" matches:$") public void confirmSourceMatches(String componentName, List<String> lines) throws Throwable { class Matches { int expectedLine; String expected; int actualLine; String actual; public Matches(int expectedLine, String expected) { this.expectedLine = expectedLine; this.expected = expected; } } List<Matches> matches = new ArrayList<>(); lines.stream() .filter(line->!line.trim().isEmpty()) .forEach(line -> { for (String subLine : line.split("\n|\\\\n")) { if (!subLine.trim().isEmpty()) { matches.add(new Matches(matches.size(), subLine)); } } }); final String source = sources.get(componentName); assertNotNull(source); int lineNum = 0; for (String line : source.split("\n|\\\\n")) { if (!line.trim().isEmpty()) { if (lineNum >= matches.size()) { // failure just in line size... break; } final Matches match = matches.get(lineNum); match.actualLine = lineNum++; match.actual = line; } } final Iterator<Matches> itr = matches.iterator(); while (itr.hasNext()) { Matches match = itr.next(); if (match.actual == null || !match.expected.replaceAll("\\s+", "") .equals( match.actual.replaceAll("\\s+", "") ) ) { StringBuilder restExpected = new StringBuilder(); StringBuilder restActual = new StringBuilder(); while (true) { restExpected.append(match.expectedLine).append('\t').append(match.expected).append("\n"); restActual.append(match.actualLine).append('\t').append(match.actual).append("\n"); if (!itr.hasNext()) { break; } match = itr.next(); } throw new ComparisonFailure( "Expected source failed on line " + match.expectedLine + ": \n" + match.expected + "\n" + "Action source match @ line " + match.actualLine + "\n" + match.actual + "\n" + "Rest of expected source:\n" + restExpected + "\n" + "Rest of actual source:\n" + restActual + "\n" +" Full actual source: \n" + source , match.expected, match.actual); } } } }