/* * Copyright 2009-2016 the original author or authors. * * 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 org.codehaus.jdt.groovy.internal.compiler.ast; import java.io.File; import java.lang.annotation.Annotation; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovySystem; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.jdt.groovy.control.EclipseSourceUnit; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; /** * Utility class, containing methods related to Grails 2.0 test support. This code should really be part of Grails support, but * lives here for now. * * @author Kris De Volder */ public class Grails20TestSupport { private static Object getField(Object o, String name) throws Exception { Class<?> c = o.getClass(); Field f = lookupField(c, name); f.setAccessible(true); return f.get(o); } /** * So we can get the field even if dealing with a subclass and irrespective of whether the class was loaded by our own * classloader (if not, its hard for us to directly get a reference to the class object. */ private static Field lookupField(Class<?> c, String name) throws Exception { if (c != null) { try { return c.getDeclaredField(name); } catch (NoSuchFieldException e) { Class<?> parent = c.getSuperclass(); if (parent != null) { return lookupField(parent, name); } } } return null; } /** * Helper to cleanup after bad code that creates ThreadLocals but doesn't remove them. We protect against such code by grabbing * the current set of thread locals when the ThreadLocalCleaner is instantiated and then sometime later, when 'cleanup' is * called, we remove any ThreadLocals that weren't there before. */ public static class ThreadLocalCleaner { private Set<ThreadLocal<?>> initialSet; public ThreadLocalCleaner() { try { this.initialSet = currentThreadLocals(); } catch (Throwable e) { e.printStackTrace(); } } private Set<ThreadLocal<?>> currentThreadLocals() throws Exception { Set<ThreadLocal<?>> initialSet = new HashSet<ThreadLocal<?>>(); Object threadLocalMap = getField(Thread.currentThread(), "threadLocals"); if (threadLocalMap != null) { Object[] entries = (Object[]) getField(threadLocalMap, "table"); if (entries != null) { for (Object object : entries) { @SuppressWarnings("unchecked") WeakReference<ThreadLocal<?>> ref = (WeakReference<ThreadLocal<?>>) object; if (ref != null) { ThreadLocal<?> tl = ref.get(); if (tl != null) { initialSet.add(tl); } } } } } return initialSet; } /** * When called, will remove any threadlocals from the current thread that were not there when the ThreadLocalCleaner * instance was first created. It is assumed this will be called in the same thread that created the instance. Typically it * will be called in a finally block to ensure the cleanup happens. */ public void cleanup() { if (initialSet != null) { try { for (ThreadLocal<?> tl : currentThreadLocals()) { if (!initialSet.contains(tl)) { tl.remove(); } } } catch (Throwable e) { e.printStackTrace(); } } } } public static boolean DEBUG = false; private static void debug(String msg) { if (DEBUG) { System.out.println("Grails20TestSupport: " + msg); } } private static final String GRAILS_UTIL_BUILD_SETTINGS = "grails.util.BuildSettings"; private static final String GRAILS_UTIL_BUILD_SETTINGS_HOLDER = "grails.util.BuildSettingsHolder"; CompilerOptions options; GroovyClassLoader gcl; public Grails20TestSupport(CompilerOptions options, GroovyClassLoader gcl) { this.options = options; this.gcl = gcl; } /** * Grails 2.0 adds automatic imports to classes in test/unit folder. See * org.codehaus.groovy.grails.test.compiler.GrailsTestCompiler and the _TestApp.groovy script. */ public void addGrailsTestCompilerCustomizers(CompilationUnit groovyCompilationUnit) { String groovyVersion = GroovySystem.getVersion(); if (groovyVersion.startsWith("1.8") || groovyVersion.startsWith("2.")) { // The assumption is that only Grails 2.0 projects will be affected, because 1.3.7 projects require 1.7 compiler. ImportCustomizer importCustomizer = new ImportCustomizer() { @Override public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { if (isInGrailsUnitTestSourceFolder(source)) { super.call(source, context, classNode); } } }; importCustomizer.addStarImports("grails.test.mixin"); importCustomizer.addStarImports("org.junit"); importCustomizer.addStaticStars("org.junit.Assert"); groovyCompilationUnit.addPhaseOperation(importCustomizer, importCustomizer.getPhase().getPhaseNumber()); try { @SuppressWarnings("unchecked") Class<? extends Annotation> testForClass = (Class<? extends Annotation>) Class.forName("grails.test.mixin.TestFor", false, gcl); if (testForClass != null) { ASTTransformationCustomizer astTransformationCustomizer = new ASTTransformationCustomizer(testForClass) { @Override public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { if (isInGrailsUnitTestSourceFolder(source)) { super.call(source, context, classNode); } } }; groovyCompilationUnit.addPhaseOperation( astTransformationCustomizer, astTransformationCustomizer.getPhase().getPhaseNumber()); ensureGrailsBuildSettings(); } } catch (ClassNotFoundException ignored) { } catch (LinkageError ignored) { } catch (Exception e) { e.printStackTrace(); } } } /** * Attempts to create and initialise a BuildSettings instance for */ void ensureGrailsBuildSettings() { debug("entering ensureGrailsBuildSettings"); ThreadLocalCleaner cleaner = new ThreadLocalCleaner(); try { String projectName = options.groovyProjectName; debug("projectName = " + projectName); if (projectName != null) { Class<?> buildSettingsHolder = gcl.loadClass(GRAILS_UTIL_BUILD_SETTINGS_HOLDER); debug("buildSettingsHolder = " + buildSettingsHolder); Object buildSettings = getBuildSettings(buildSettingsHolder); debug("buildSettings = " + buildSettings); if (buildSettings == null) { debug("Creating buildSettings"); buildSettings = createBuildSettings(); debug("created buildSettings = " + buildSettingsHolder); setBuildSettings(buildSettingsHolder, buildSettings); Object checkit = getBuildSettings(buildSettingsHolder); debug("set and get buildsettings = " + checkit); } } } catch (Exception e) { debug("FAILED ensureGrailsBuildSettings"); e.printStackTrace(); // ignore ... classpath doesn't have what we expect. } finally { cleaner.cleanup(); } debug("exiting ensureGrailsBuildSettings"); } private Object createBuildSettings() throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { Class<?> buildSettingsClass = gcl.loadClass(GRAILS_UTIL_BUILD_SETTINGS); debug("BuildSettingsClass = " + buildSettingsClass); Constructor<?> constructor = buildSettingsClass.getConstructor(File.class, File.class); debug("Constructor = " + constructor); Object grailsHome = getGrailsHome(); debug("grailsHome = " + grailsHome); File projectHome = getProjectHome(); debug("projectHome = " + projectHome); return constructor.newInstance(grailsHome, projectHome); } private Object getGrailsHome() { return null; // not computed for now... for the current use case it doesn't seem needed so why bother. } private File getProjectHome() { String projectName = options.groovyProjectName; if (projectName != null) { IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); IPath location = project.getLocation(); if (location != null) { return location.toFile(); } } return null; } private static Object getBuildSettings(Class<?> buildSettingsHolder) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { Method m = buildSettingsHolder.getMethod("getSettings"); return m.invoke(null); } private static synchronized void setBuildSettings(Class<?> buildSettingsHolder, Object buildSettings) throws SecurityException, NoSuchMethodException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { Method m = buildSettingsHolder.getMethod("setSettings", buildSettingsHolder.getClassLoader().loadClass(GRAILS_UTIL_BUILD_SETTINGS)); m.invoke(null, buildSettings); Assert.isTrue(getBuildSettings(buildSettingsHolder) == buildSettings); } static boolean isInGrailsUnitTestSourceFolder(SourceUnit source) { if (source instanceof EclipseSourceUnit) { EclipseSourceUnit eclipseSource = (EclipseSourceUnit) source; IFile file = eclipseSource.getEclipseFile(); if (file != null) { IPath path = file.getProjectRelativePath(); return new Path("test/unit").isPrefixOf(path); } } return false; } }