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;
+ }
+}