/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.jjs.impl.codesplitter; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.CompilerContext; import com.google.gwt.dev.PrecompileTaskOptions; import com.google.gwt.dev.PrecompileTaskOptionsImpl; import com.google.gwt.dev.cfg.BindingProperty; import com.google.gwt.dev.cfg.ConditionNone; import com.google.gwt.dev.cfg.ConfigurationProperty; import com.google.gwt.dev.jjs.JsOutputOption; import com.google.gwt.dev.jjs.ast.JClassType; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JType; import com.google.gwt.dev.jjs.impl.FullCompileTestBase; import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap; import com.google.gwt.dev.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsExprStmt; import com.google.gwt.dev.js.ast.JsFunction; import com.google.gwt.dev.js.ast.JsName; import com.google.gwt.dev.js.ast.JsNode; import com.google.gwt.dev.js.ast.JsVisitor; import com.google.gwt.dev.util.Pair; import com.google.gwt.thirdparty.guava.common.collect.Sets; import java.util.List; import java.util.Set; /** * Unit test for {@link com.google.gwt.dev.jjs.impl.codesplitter.CodeSplitter}. */ public class CodeSplitterTest extends FullCompileTestBase { /** * A {@link MultipleDependencyGraphRecorder} that does nothing. */ private static final MultipleDependencyGraphRecorder NULL_RECORDER = new MultipleDependencyGraphRecorder() { @Override public void close() { } @Override public void endDependencyGraph() { } @Override public void methodIsLiveBecause(JMethod liveMethod, List<JMethod> dependencyChain) { } @Override public void open() { } @Override public void startDependencyGraph(String name, String extnds) { } }; // These will be the functions that are shared between fragments. This unit test will // be based for finding these function in the proper fragments. private final String functionA = "public static void functionA() {}"; private final String functionB = "public static void functionB() {}"; private final String functionC = "public static void functionC() {}"; private final String functionD = "public static void functionD() {}"; private final String initialA = "public static void initialA() {}"; private final String initialB = "public static void initialB() {}"; private final String functionAllocateFooInt = "public static void functionAllocateFooInt() { " + "new Foo(42); }"; private final String functionAllocateFooString = "public static void functionAllocateFooString() { new Foo(\"Hello\"); }"; public int leftOverMergeSize = 0; public int expectedFragmentCount = 0; private ConfigurationProperty initialSequenceProp = new ConfigurationProperty(CodeSplitters.PROP_INITIAL_SEQUENCE, true); private boolean closureOutputFormat = false; private JavaToJavaScriptMap currentJjsMap; @Override public void setUp() throws Exception { super.setUp(); // Compilation Configuration Properties. BindingProperty stackMode = new BindingProperty("compiler.stackMode"); stackMode.addDefinedValue(new ConditionNone(), "STRIP"); setProperties(new BindingProperty[]{stackMode}, new String[]{"STRIP"}, new ConfigurationProperty[]{initialSequenceProp}); currentJjsMap = null; } public void testSimple() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append("static {"); // code.append(" functionC();"); code.append("}"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(" public static void onModuleLoad() {\n"); code.append("functionC();"); // Fragment #1 code.append(createRunAsync("functionA();")); // Fragment #1 (merged) code.append(createRunAsync("functionA(); functionB();")); // Fragment #2 code.append(createRunAsync("functionC();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 4; compileSnippetToJS(code.toString()); // init + 2 fragments + leftover. assertFragmentCount(4); assertInFragment("functionA", 1); // Verify that functionA isn't duplicated else where. assertNotInFragment("functionA", 0); assertNotInFragment("functionA", 2); assertNotInFragment("functionA", 3); assertInFragment("functionB", 1); // functionC must be in the initial fragment. assertInFragment("functionC", 0); } public void testClosureConstructorPrototypeMovement() throws UnableToCompleteException { closureOutputFormat = true; StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append("static class Foo {"); code.append(" int x; String y;\n"); code.append(" public Foo(int x) { this.x = x; }\n"); code.append(" public Foo(String y) { this.y = y; }\n"); code.append("};"); code.append(functionAllocateFooInt); code.append(functionAllocateFooString); code.append(" public static void onModuleLoad() {\n"); code.append("functionAllocateFooInt();"); // Fragment #1 code.append(createRunAsync("functionAllocateFooString();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 3; compileSnippetToJS(code.toString()); // init + 1 fragments + leftover. assertFragmentCount(3); // ------- Verify Fragment 0 ------- // Int related function and int ctor, plus prototype ctor in fragment 0 assertInFragment("functionAllocateFooInt", 0); // assert synthetic prototype ctor is in fragment 0 assertClosureTypeCtorInFragment("EntryPoint$Foo", 0); // and int ctor in fragment 0 assertClosureMethodInFragment("EntryPoint$Foo", 0, JPrimitiveType.INT); // we also don't want string related ctor stuff in fragment 0 assertNotInFragment("functionAllocateFooString", 0); JClassType stringType = jProgram.getTypeJavaLangString(); assertClosureFunctionNotInFragment("EntryPoint$Foo", 0, stringType); assertPrototypeAssignmentStatementInFragment("EntryPoint$Foo", 0, JPrimitiveType.INT); assertPrototypeAssignmentStatementNotInFragment("EntryPoint$Foo", 0, stringType); // ------- Verify fragment 1 ------- // check if string ctor and related code is in fragment 1 assertInFragment("functionAllocateFooString", 1); assertClosureMethodInFragment("EntryPoint$Foo", 1, stringType); assertPrototypeAssignmentStatementInFragment("EntryPoint$Foo", 1, stringType); assertPrototypeAssignmentStatementNotInFragment("EntryPoint$Foo", 1, JPrimitiveType.INT); // Verify that the Class prototype ctor isn't duplicated here assertClosureTypeCtorNotInFragment("EntryPoint$Foo", 1); // but no int related code is here assertNotInFragment("functionAllocateFooInt", 1); assertClosureFunctionNotInFragment("EntryPoint$Foo", 1, JPrimitiveType.INT); // Verify fragment 2 (leftovers) assertNotInFragment("functionAllocateFooInt", 2); assertNotInFragment("functionAllocateFooString", 2); assertClosureFunctionNotInFragment("EntryPoint$Foo", 2, JPrimitiveType.INT); assertClosureFunctionNotInFragment("EntryPoint$Foo", 2, stringType); assertClosureTypeCtorNotInFragment("EntryPoint$Foo", 2); assertPrototypeAssignmentStatementNotInFragment("EntryPoint$Foo", 2, JPrimitiveType.INT); assertPrototypeAssignmentStatementNotInFragment("EntryPoint$Foo", 2, stringType); } public void testPredefinedAsyncGrouping() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(functionD); code.append(createNamedRunAsyncCallback("RunAsyncCallBack1", "functionA(); functionD();")); code.append(createNamedRunAsyncCallback("RunAsyncCallBack2", "functionB(); functionD();")); code.append(createNamedRunAsyncCallback("RunAsyncCallBack3", "functionC();")); code.append(" public static void onModuleLoad() {\n"); // Fragment #1 code.append("createCallBack1();"); // Fragment #2 code.append("createCallBack2();"); code.append("createCallBack3();"); code.append(" }\n"); code.append(" private static void createCallBack1() {\n"); code.append(" GWT.runAsync( new RunAsyncCallBack1() );"); code.append(" }\n"); code.append(" private static void createCallBack2() {\n"); code.append(" GWT.runAsync(RunAsyncCallBack2.class, new RunAsyncCallBack2() );"); code.append(" }\n"); code.append(" private static void createCallBack3() {\n"); code.append(" GWT.runAsync(RunAsyncCallBack2.class, new RunAsyncCallBack3() );"); code.append(" }\n"); code.append("}\n"); // Use 1 to 1 merging. expectedFragmentCount = -1; compileSnippetToJS(code.toString()); // 1 initial + 2 fragments + leftover. assertFragmentCount(4); // TODO(rluble): using fragment numbers here is kind of tricky as the ordering is not // guaranteed. // functionB must be in the fragment #2. As first are all the user specified merges. assertInFragment("functionA", 2); // Verify that functionA isn't duplicated else where. assertNotInFragment("functionA", 0); assertNotInFragment("functionA", 1); assertNotInFragment("functionA", 3); // functionB must be in the fragment #1. assertInFragment("functionB", 1); // functionC must be in the fragment #1. assertInFragment("functionC", 1); // functionC must be in the leftover. assertInFragment("functionD", 3); } public void testSimpleWithInitialSequence() throws UnableToCompleteException { // Set up configuration property. initialSequenceProp.addValue("@test.EntryPoint::createInitialCallBack1()"); initialSequenceProp.addValue("@test.EntryPoint::createInitialCallBack2()"); StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(initialA); code.append(initialB); code.append(createNamedRunAsyncCallback("InitialRunAsyncCallBack1", "initialA();")); code.append(createNamedRunAsyncCallback("InitialRunAsyncCallBack2", "initialB();")); code.append(" public static void onModuleLoad() {\n"); code.append("functionC();"); // Fragment #3 code.append(createRunAsync("functionA();")); // Fragment #3 (merged) code.append(createRunAsync("functionA(); functionB();")); // Fragment #4 code.append(createRunAsync("functionC();")); // initial fragments #1, #2 code.append("createInitialCallBack1();"); code.append("createInitialCallBack2();"); code.append(" }\n"); code.append(" private static void createInitialCallBack1() {\n"); code.append(" GWT.runAsync( new InitialRunAsyncCallBack1() );"); code.append(" }\n"); code.append(" private static void createInitialCallBack2() {\n"); code.append(" GWT.runAsync( new InitialRunAsyncCallBack2() );"); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 6; compileSnippetToJS(code.toString()); // 3 initial + 2 fragments + leftover. assertFragmentCount(6); assertInFragment("functionA", 3); // Verify that functionA isn't duplicated else where. assertNotInFragment("functionA", 0); assertNotInFragment("functionA", 1); assertNotInFragment("functionA", 2); assertNotInFragment("functionA", 4); assertNotInFragment("functionA", 5); assertInFragment("functionB", 3); // functionC must be in the initial fragment. assertInFragment("functionC", 0); // initialA must be in the initial fragment #1. assertInFragment("initialA", 1); // functionC must be in the initial fragment #2. assertInFragment("initialB", 2); } public void testOnSuccessCallCast() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(" " + functionA); code.append(" " + functionB); code.append(" " + functionC); code.append(" public static void onModuleLoad() {\n"); code.append(" functionC();"); code.append(" " + createRunAsync("(RunAsyncCallback)", "functionA();")); code.append(" " + createRunAsync("(RunAsyncCallback)", "functionB();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 4; compileSnippetToJS(code.toString()); // init + 2 fragments + leftover. assertFragmentCount(4); assertInFragment("functionA", 1); assertInFragment("functionB", 2); // Verify that functionA and B aren't in the leftover. assertNotInFragment("functionA", 3); assertNotInFragment("functionB", 3); } public void testMergeLeftOvers() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(" public static void onModuleLoad() {\n"); // Fragment #1 code.append(createRunAsync("functionA();")); // Fragment #2 code.append(createRunAsync("functionB();")); // Fragment #3 code.append(createRunAsync("functionC();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 2; leftOverMergeSize = 100 * 1024 /* 100k minumum */; this.compileSnippetToJS(code.toString()); // init + leftover. assertFragmentCount(2); assertInFragment("functionA", 1); assertInFragment("functionB", 1); assertInFragment("functionC", 1); } /** * Test that the conversion from -XfragmentCount expectCount into number of exclusive fragments * is correct. */ public void testExpectedFragmentCountFromFragmentMerge() { // Exclusive fragments are totalFragments - (1 + initials) - leftovers. assertEquals(40, CodeSplitters.getNumberOfExclusiveFragmentFromExpectedFragmentCount(2, 44)); // This is a non negative number always assertEquals(0, CodeSplitters.getNumberOfExclusiveFragmentFromExpectedFragmentCount(2, 1)); } public void testDontMergeLeftOvers() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(" public static void onModuleLoad() {\n"); // Fragment #1 code.append(createRunAsync("functionA();")); // Fragment #2 code.append(createRunAsync("functionB();")); // Fragment #3 code.append(createRunAsync("functionC();")); code.append(" }\n"); code.append("}\n"); // we want don't want them to be merged leftOverMergeSize = 10; expectedFragmentCount = 5; this.compileSnippetToJS(code.toString()); // init + 3 exlclusive fragments + leftover. assertFragmentCount(5); assertNotInFragment("functionA", 4); assertNotInFragment("functionB", 4); assertNotInFragment("functionC", 4); } public void testNoMergeMoreThanTwo() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(" public static void onModuleLoad() {\n"); // Fragment #1 code.append(createRunAsync("functionA();")); // Fragment #2 code.append(createRunAsync("functionA();")); // Fragment #3 code.append(createRunAsync("functionA();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 2; compileSnippetToJS(code.toString()); // There is no common code shared between any pair. // init + 3 fragments + leftover. assertFragmentCount(5); } public void testDoubleMerge() throws UnableToCompleteException { StringBuilder code = new StringBuilder(); code.append("package test;\n"); code.append("import com.google.gwt.core.client.GWT;\n"); code.append("import com.google.gwt.core.client.RunAsyncCallback;\n"); code.append("public class EntryPoint {\n"); code.append(functionA); code.append(functionB); code.append(functionC); code.append(" public static void onModuleLoad() {\n"); // Fragment #1 code.append(createRunAsync("functionA();")); // Fragment #1 code.append(createRunAsync("functionA(); functionC();")); // Fragment #2 code.append(createRunAsync("functionB(); functionC();")); // Fragment #2 code.append(createRunAsync("functionB(); functionC();")); code.append(" }\n"); code.append("}\n"); expectedFragmentCount = 4; compileSnippetToJS(code.toString()); // init + 2 fragments + leftover. assertFragmentCount(4); assertInFragment("functionA", 1); assertInFragment("functionB", 2); assertInFragment("functionC", 3); } private void assertFragmentCount(int num) { assertEquals(num, jsProgram.getFragmentCount()); } private void assertInFragment(String functionName, int expectedFragmentNumber) { Set<Integer> fragments = Sets.newHashSet(); for (int fragmentNumber = 0; fragmentNumber < jsProgram.getFragmentCount(); fragmentNumber++) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNumber); if (findFunctionIn(functionName, fragment)) { fragments.add(fragmentNumber); } } assertTrue("function " + functionName + " should be in fragments " + expectedFragmentNumber + " but is in " + fragments, fragments.equals(Sets.newHashSet(expectedFragmentNumber))); } private void assertClosureTypeCtorInFragment(String functionName, int expectedFragmentNumber) { Set<Integer> fragments = Sets.newHashSet(); for (int fragmentNumber = 0; fragmentNumber < jsProgram.getFragmentCount(); fragmentNumber++) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNumber); if (findClosureTypeFunctionIn(functionName, fragment)) { fragments.add(fragmentNumber); } } assertTrue("function " + functionName + " should be in fragments " + expectedFragmentNumber + " but is in " + fragments, fragments.equals(Sets.newHashSet(expectedFragmentNumber))); } private void assertClosureMethodInFragment(String functionName, int expectedFragmentNumber, JType... args) { Set<Integer> fragments = Sets.newHashSet(); for (int fragmentNumber = 0; fragmentNumber < jsProgram.getFragmentCount(); fragmentNumber++) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNumber); if (findClosureFunctionIn(functionName, fragment, args)) { fragments.add(fragmentNumber); } } assertTrue("function " + functionName + " should be in fragments " + expectedFragmentNumber + " but is in " + fragments, fragments.equals(Sets.newHashSet(expectedFragmentNumber))); } private void assertPrototypeAssignmentStatementInFragment(String functionName, int expectedFragmentNumber, JType... args) { Set<Integer> fragments = Sets.newHashSet(); for (int fragmentNumber = 0; fragmentNumber < jsProgram.getFragmentCount(); fragmentNumber++) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNumber); if (findPrototypeChainStatementIn(functionName, fragment, args)) { fragments.add(fragmentNumber); } } assertTrue("Prototype chain assignment for " + functionName + " should be in " + "fragments " + expectedFragmentNumber + " but is in " + fragments, fragments.equals(Sets.newHashSet(expectedFragmentNumber))); } private void assertNotInFragment(String functionName, int fragmentNum) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNum); assertFalse("function " + functionName + " should not be in fragment " + fragmentNum, findFunctionIn(functionName, fragment)); } private void assertClosureTypeCtorNotInFragment(String functionName, int fragmentNum) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNum); assertFalse("function " + functionName + " should not be in fragment " + fragmentNum, findClosureTypeFunctionIn(functionName, fragment)); } private void assertClosureFunctionNotInFragment(String functionName, int fragmentNum, JType... args) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNum); assertFalse("function " + functionName + " should not be in fragment " + fragmentNum, findClosureFunctionIn(functionName, fragment, args)); } private void assertPrototypeAssignmentStatementNotInFragment(String functionName, int fragmentNum, JType... args) { JsBlock fragment = jsProgram.getFragmentBlock(fragmentNum); assertFalse("function " + functionName + " should not be in fragment " + fragmentNum, findPrototypeChainStatementIn(functionName, fragment, args)); } /** * @return true if the function exists in that fragment. */ private static boolean findFunctionIn(final String functionName, JsBlock fragment) { final boolean[] found = {false}; JsVisitor visitor = new JsVisitor() { @Override public boolean visit(JsFunction x, JsContext ctx) { JsName jsName = x.getName(); if (jsName != null && jsName.getShortIdent().equals(functionName)) { found[0] = true; } return false; } }; visitor.accept(fragment); return found[0]; } /** * @return true if the function exists in that fragment and it mapes to a classType */ private boolean findClosureTypeFunctionIn(final String functionName, JsBlock fragment) { final boolean[] found = {false}; JsVisitor visitor = new JsVisitor() { @Override public boolean visit(JsFunction x, JsContext ctx) { JsName jsName = x.getName(); if (jsName != null && jsName.getShortIdent().equals(functionName) && currentJjsMap.nameToType(jsName) != null) { found[0] = true; } return false; } }; visitor.accept(fragment); return found[0]; } /** * @return true if the function exists in that fragment and it maps to a function * with the expected args. */ private boolean findClosureFunctionIn(final String functionName, JsBlock fragment, final JType... args) { final boolean[] found = {false}; JsVisitor visitor = new JsVisitor() { @Override public boolean visit(JsFunction x, JsContext ctx) { JsName jsName = x.getName(); if (jsName != null && jsName.getShortIdent().equals(functionName)) { JMethod meth = currentJjsMap.nameToMethod(jsName); if (meth != null) { found[0] = checkArguments(meth, args); } } return false; } }; visitor.accept(fragment); return found[0]; } /** * @return true if a statement of the form FunctionName.prototype = ClassFunction.prototype */ private boolean findPrototypeChainStatementIn(final String functionName, JsBlock fragment, final JType... args) { class PrototypeChainFinderVisitor extends JsVisitor { boolean found = false; @Override public boolean visit(JsExprStmt x, JsContext ctx) { JMethod method = currentJjsMap.methodForStatement(x); JsName jsName = currentJjsMap.nameForMethod(method); if (method != null && jsName != null && jsName.getShortIdent().equals(functionName)) { found = checkArguments(method, args); } return false; } }; PrototypeChainFinderVisitor visitor = new PrototypeChainFinderVisitor(); visitor.accept(fragment); return visitor.found; } private boolean checkArguments(JMethod method, JType[] args) { for (int i = 0; i < args.length; i++) { if (method.getParams().get(i).getType().getUnderlyingType() != args[i]) { return false; } } return true; } @Override protected void optimizeJava() { } @Override protected CompilerContext provideCompilerContext() { PrecompileTaskOptions options = new PrecompileTaskOptionsImpl(); options.setOutput(JsOutputOption.PRETTY); options.setRunAsyncEnabled(true); if (closureOutputFormat) { options.setClosureCompilerFormatEnabled(true); } return new CompilerContext.Builder().options(options).build(); } @Override protected Pair<JavaToJavaScriptMap, Set<JsNode>> compileSnippetToJS(final String code) throws UnableToCompleteException { currentJjsMap = super.compileSnippetToJS(code).getLeft(); CodeSplitter.exec(logger, jProgram, jsProgram, currentJjsMap, expectedFragmentCount, leftOverMergeSize, NULL_RECORDER); return null; } private static String createRunAsync(String cast, String body) { StringBuilder code = new StringBuilder(); code.append("GWT.runAsync(" + cast + "new " + "RunAsyncCallback() {\n"); code.append(" public void onFailure(Throwable reason) {}\n"); code.append(" public void onSuccess() {\n"); code.append(" " + body); code.append(" }\n"); code.append("});\n"); return code.toString(); } private static String createNamedRunAsyncCallback(String className, String body) { StringBuilder code = new StringBuilder(); code.append("private static class " + className + " implements RunAsyncCallback {\n"); code.append(" public void onFailure(Throwable reason) {}\n"); code.append(" public void onSuccess() {\n"); code.append(" " + body); code.append(" }\n"); code.append("}\n"); return code.toString(); } private static String createRunAsync(String body) { return createRunAsync("", body); } }