Add an escape hatch to CompilingClassLoader to allow individual "client" classes to be loaded from bytecode on the classpath and escape the module's source path jail.

Patch by: bobv
Review by: scottb

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5588 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
index 2db2593..39124ac 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.shell;
 
 import com.google.gwt.core.client.GWTBridge;
+import com.google.gwt.core.client.GwtScriptOnly;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.TreeLogger.Type;
@@ -31,9 +32,11 @@
 import com.google.gwt.dev.javac.CompiledClass;
 import com.google.gwt.dev.javac.JsniMethod;
 import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.shell.rewrite.HasAnnotation;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.InstanceMethodOracle;
 import com.google.gwt.dev.util.JsniRef;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.util.tools.Utility;
 
 import org.apache.commons.collections.map.AbstractReferenceMap;
@@ -323,6 +326,31 @@
   }
 
   /**
+   * A ClassLoader that will delegate to a parent ClassLoader and fall back to
+   * loading bytecode as resources from an alternate parent ClassLoader.
+   */
+  private static class MultiParentClassLoader extends ClassLoader {
+    private final ClassLoader resources;
+
+    public MultiParentClassLoader(ClassLoader parent, ClassLoader resources) {
+      super(parent);
+      this.resources = resources;
+    }
+
+    @Override
+    protected synchronized Class<?> findClass(String name)
+        throws ClassNotFoundException {
+      String resourceName = name.replace('.', '/') + ".class";
+      URL url = resources.getResource(resourceName);
+      if (url == null) {
+        throw new ClassNotFoundException();
+      }
+      byte[] bytes = Util.readURLAsBytes(url);
+      return defineClass(name, bytes, 0, bytes.length);
+    }
+  }
+
+  /**
    * Implements {@link InstanceMethodOracle} on behalf of the
    * {@link HostedModeClassRewriter}. Implemented using {@link TypeOracle}.
    */
@@ -574,9 +602,14 @@
 
   private final TreeLogger logger;
 
+  private final Set<String> scriptOnlyClasses = new HashSet<String>();
+
+  private ClassLoader scriptOnlyClassLoader;
+
   private ShellJavaScriptHost shellJavaScriptHost;
 
   private final Set<String> singleJsoImplTypes = new HashSet<String>();
+
   /**
    * Used by {@link #findClass(String)} to prevent reentrant JSNI injection.
    */
@@ -719,6 +752,11 @@
           new NullPointerException());
     }
 
+    if (scriptOnlyClasses.contains(className)) {
+      // Allow the child ClassLoader to handle this
+      throw new ClassNotFoundException();
+    }
+
     // Check for a bridge class that spans hosted and user space.
     if (BRIDGE_CLASS_NAMES.containsKey(className)) {
       return BRIDGE_CLASS_NAMES.get(className);
@@ -730,6 +768,12 @@
       throw new ClassNotFoundException(className);
     }
 
+    if (HasAnnotation.hasAnnotation(classBytes, GwtScriptOnly.class)) {
+      scriptOnlyClasses.add(className);
+      maybeInitializeScriptOnlyClassLoader();
+      return Class.forName(className, true, scriptOnlyClassLoader);
+    }
+
     /*
      * Prevent reentrant problems where classes that need to be injected have
      * circular dependencies on one another via JSNI and inheritance. This check
@@ -793,6 +837,8 @@
   void clear() {
     // Release our references to the shell.
     shellJavaScriptHost = null;
+    scriptOnlyClasses.clear();
+    scriptOnlyClassLoader = null;
     updateJavaScriptHost();
     weakJsoCache.clear();
     weakJavaWrapperCache.clear();
@@ -1053,6 +1099,13 @@
     shellJavaScriptHost.createNativeMethods(logger, unit.getJsniMethods(), this);
   }
 
+  private void maybeInitializeScriptOnlyClassLoader() {
+    if (scriptOnlyClassLoader == null) {
+      scriptOnlyClassLoader = new MultiParentClassLoader(this,
+          Thread.currentThread().getContextClassLoader());
+    }
+  }
+
   private boolean typeHasCompilationUnit(String className) {
     return getUnitForClassName(className) != null;
   }
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/HasAnnotation.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/HasAnnotation.java
new file mode 100644
index 0000000..24991ca
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/HasAnnotation.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 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.shell.rewrite;
+
+import com.google.gwt.dev.asm.AnnotationVisitor;
+import com.google.gwt.dev.asm.ClassAdapter;
+import com.google.gwt.dev.asm.ClassReader;
+import com.google.gwt.dev.asm.ClassVisitor;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.EmptyVisitor;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * A simple ClassAdapter that determines if a specific annotation is declared on
+ * a type (ignoring any annotatons that may be present on supertypes or
+ * superinterfaces).
+ */
+public class HasAnnotation extends ClassAdapter {
+  /**
+   * A utility method to determine if the class defined in
+   * <code>classBytes</code> has a particular annotation.
+   * 
+   * @param classBytes the class's bytecode
+   * @param annotation the type of annotation to look for
+   * @return <code>true</code> if the class defined in <code>classBytes</code>
+   *         possesses the desired annotation
+   */
+  public static boolean hasAnnotation(byte[] classBytes,
+      Class<? extends Annotation> annotation) {
+    HasAnnotation v = new HasAnnotation(new EmptyVisitor(), annotation);
+    new ClassReader(classBytes).accept(v, ClassReader.SKIP_CODE
+        | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+
+    return v.isFound();
+  }
+
+  private boolean found;
+
+  private final String targetDesc;
+
+  public HasAnnotation(ClassVisitor v, Class<? extends Annotation> annotation) {
+    super(v);
+    targetDesc = Type.getDescriptor(annotation);
+  }
+
+  public boolean isFound() {
+    return found;
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+    if (targetDesc.equals(desc)) {
+      found = true;
+    }
+    return super.visitAnnotation(desc, visible);
+  }
+}
\ No newline at end of file
diff --git a/dev/core/super/com/google/gwt/core/client/GwtScriptOnly.java b/dev/core/super/com/google/gwt/core/client/GwtScriptOnly.java
new file mode 100644
index 0000000..9161f97
--- /dev/null
+++ b/dev/core/super/com/google/gwt/core/client/GwtScriptOnly.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 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.core.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to break out of a module's source path in hosted
+ * mode. Types annotated with this annotation will not be loaded by hosted
+ * mode's CompilingClassLoader. Instead, the bytecode for the type will be
+ * loaded from the system classloader.
+ * <p>
+ * This annotation is typically combined with the <code>super-source</code> tag
+ * to provide web-mode implementations of (binary-only) types that the developer
+ * wishes to use in hosted mode. This can be used, for instance, to provide a
+ * reference implementation to develop unit tests.
+ */
+@Documented
+@Target(ElementType.TYPE)
+public @interface GwtScriptOnly {
+}
diff --git a/user/build.xml b/user/build.xml
index bba85ff..2d38baf 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -40,6 +40,7 @@
   -->
   <path id="test.extraclasspath">
     <pathelement location="${gwt.build}/out/dev/core/bin-test" />
+    <pathelement location="test-super" />
     <pathelement location="test_i18n_${gwt.i18n.test.InnerClassChar}" />
   </path>
 
diff --git a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java
new file mode 100644
index 0000000..d8e9ed6
--- /dev/null
+++ b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 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.scriptonly;
+
+import com.google.gwt.core.client.GwtScriptOnly;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * This is the web-mode version of this class. It will attempt to call methods
+ * not normally available in hosted-mode classes.
+ */
+@GwtScriptOnly
+public class ScriptOnlyClass extends BaseClass {
+  public static boolean isInstance(Class<?> test, Object instance) {
+    return false;
+  }
+
+  /**
+   * Test cross-bounday method invocation.
+   */
+  public void callCallback(AsyncCallback<ScriptOnlyClass> callback) {
+    unimplemented();
+  }
+
+  /**
+   * Access code that's not available in the module's source path.
+   */
+  public boolean callCodeNotInSourcePath() {
+    return this.<Boolean> unimplemented();
+  }
+
+  /**
+   * Can only do this in hosted mode.
+   */
+  public String getClassLoaderName() {
+    return unimplemented();
+  }
+
+  /**
+   * Test class-literal access to classes that come from the CCL.
+   */
+  public Class<?> getWindowClass() {
+    return unimplemented();
+  }
+
+  @Override
+  public boolean isHostedMode() {
+    return false;
+  }
+
+  private <T> T unimplemented() {
+    throw new RuntimeException("unimplemented");
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
index a2d754d..146fa61 100644
--- a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.jjs;
 
+import com.google.gwt.dev.jjs.scriptonly.ScriptOnlyTest;
 import com.google.gwt.dev.jjs.test.AnnotationsTest;
 import com.google.gwt.dev.jjs.test.AutoboxTest;
 import com.google.gwt.dev.jjs.test.BlankInterfaceTest;
@@ -84,6 +85,7 @@
     suite.addTestSuite(ObjectIdentityTest.class);
     suite.addTestSuite(RunAsyncFailureTest.class);
     suite.addTestSuite(RunAsyncTest.class);
+    suite.addTestSuite(ScriptOnlyTest.class);
     suite.addTestSuite(SingleJsoImplTest.class);
     suite.addTestSuite(TypeHierarchyTest.class);
     suite.addTestSuite(UnstableGeneratorTest.class);
diff --git a/user/test/com/google/gwt/dev/jjs/ScriptOnlyTest.gwt.xml b/user/test/com/google/gwt/dev/jjs/ScriptOnlyTest.gwt.xml
new file mode 100644
index 0000000..3822d89
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/ScriptOnlyTest.gwt.xml
@@ -0,0 +1,20 @@
+<!--                                                                        -->
+<!-- Copyright 2009 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<!-- Contains tests for breaking out of the client source path  -->
+<module>
+  <inherits name="com.google.gwt.core.Core" />
+  <source path='scriptonly' />
+  <super-source path='super' />
+</module>
\ No newline at end of file
diff --git a/user/test/com/google/gwt/dev/jjs/scriptonly/BaseClass.java b/user/test/com/google/gwt/dev/jjs/scriptonly/BaseClass.java
new file mode 100644
index 0000000..a479a3e
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/scriptonly/BaseClass.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2009 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.scriptonly;
+
+/**
+ * Used to test instanceof.
+ */
+public class BaseClass {
+  public boolean isHostedMode() {
+    throw new RuntimeException("Not running the right code");
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java b/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java
new file mode 100644
index 0000000..066dad4
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyClass.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 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.scriptonly;
+
+import com.google.gwt.core.client.GwtScriptOnly;
+import com.google.gwt.dev.jjs.server.AccessedByScriptOnlyClass;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * This is the hosted-mode version of this class. It will attempt to call
+ * methods not normally available in hosted-mode classes.
+ */
+@GwtScriptOnly
+public class ScriptOnlyClass extends BaseClass {
+  /*
+   * NB: If you see errors from the GWT compiler while trying to compile this
+   * type, make sure that the user/test-super folder is on the classpath.
+   */
+  public static boolean isInstance(Class<?> test, Object instance) {
+    return test.isInstance(instance);
+  }
+
+  /**
+   * Test cross-boundary method invocation.
+   */
+  public void callCallback(AsyncCallback<ScriptOnlyClass> callback) {
+    assert callback != null;
+    assert callback.getClass().getClassLoader() == this.getClass().getClassLoader().getParent();
+    callback.onSuccess(this);
+  }
+
+  /**
+   * Access code that's not available in the module's source path.
+   */
+  public boolean callCodeNotInSourcePath() {
+    return AccessedByScriptOnlyClass.getBool();
+  }
+
+  /**
+   * Can only do this in hosted mode.
+   */
+  public String getClassLoaderName() {
+    return getClass().getClassLoader().getClass().getName();
+  }
+
+  /**
+   * Test class-literal access to classes that come from the CCL.
+   */
+  public Class<?> getWindowClass() {
+    return Window.class;
+  }
+
+  @Override
+  public boolean isHostedMode() {
+    return true;
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyTest.java b/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyTest.java
new file mode 100644
index 0000000..b9e8045
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/scriptonly/ScriptOnlyTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2009 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.scriptonly;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * Tests various aspects of using user-defined bridge classes in hosted mode.
+ */
+public class ScriptOnlyTest extends GWTTestCase {
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.ScriptOnlyTest";
+  }
+
+  public void testClassSemantics() {
+    Object o = new ScriptOnlyClass();
+    assertTrue(o instanceof ScriptOnlyClass);
+    assertTrue(o instanceof BaseClass);
+
+    assertEquals(!GWT.isScript(),
+        ScriptOnlyClass.isInstance(BaseClass.class, o));
+
+    // Test cast
+    assertEquals(!GWT.isScript(), ((BaseClass) o).isHostedMode());
+  }
+
+  public void testUserBridgeClass() {
+    final ScriptOnlyClass b = new ScriptOnlyClass();
+    if (GWT.isScript()) {
+      // Just make sure the super-source version is used in web mode
+      assertFalse(b.isHostedMode());
+      return;
+    }
+
+    // Make sure the right version of the class was loaded
+    assertTrue(b.isHostedMode());
+
+    // Is the sub-loader delegating to our CCL?
+    assertSame(Window.class, b.getWindowClass());
+
+    // Try something you can't do in web-mode (JRE code)
+    assertNotNull(b.getClassLoaderName());
+
+    // Try something you can't do in web-mode ("server" code)
+    assertTrue(b.callCodeNotInSourcePath());
+
+    b.callCallback(new AsyncCallback<ScriptOnlyClass>() {
+      public void onFailure(Throwable caught) {
+        fail(caught.getMessage());
+      }
+
+      public void onSuccess(ScriptOnlyClass result) {
+        assertSame(b, result);
+      }
+    });
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/server/AccessedByScriptOnlyClass.java b/user/test/com/google/gwt/dev/jjs/server/AccessedByScriptOnlyClass.java
new file mode 100644
index 0000000..fa9f094
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/server/AccessedByScriptOnlyClass.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2009 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.server;
+
+/**
+ * This demonstrates how a user-defined bridge class can access types that are
+ * not within the CompilingClassLoader's source path.
+ */
+public class AccessedByScriptOnlyClass {
+  public static boolean getBool() {
+    return true;
+  }
+}