/* * 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.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarFile; import groovy.lang.GroovyClassLoader; import org.apache.xbean.classloader.NonLockingJarFileClassLoader; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilationUnit.PrimaryClassNodeOperation; import org.codehaus.groovy.control.CompilationUnit.ProgressListener; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.ErrorCollector; import org.codehaus.groovy.control.Phases; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.customizers.CompilationCustomizer; import org.codehaus.groovy.eclipse.GroovyLogManager; import org.codehaus.groovy.eclipse.TraceCategory; import org.codehaus.jdt.groovy.control.EclipseSourceUnit; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.util.CompilerUtils; import org.eclipse.jdt.groovy.core.util.GroovyUtils; import org.eclipse.jdt.groovy.core.util.ScriptFolderSelector; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.batch.FileSystem; import org.eclipse.jdt.internal.compiler.batch.FileSystem.Classpath; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; import org.eclipse.jdt.internal.core.builder.BatchImageBuilder; import org.eclipse.jdt.internal.core.builder.BuildNotifier; /** * The mapping layer between the groovy parser and the JDT. This class communicates with the groovy parser and translates results * back for JDT to consume. * * @author Andy Clement */ public class GroovyParser { public static IGroovyDebugRequestor debugRequestor; public Object requestor; public ProblemReporter problemReporter; private JDTResolver resolver; private String projectName; private String gclClasspath; private CompilationUnit compilationUnit; private CompilerOptions compilerOptions; public CompilerOptions getCompilerOptions() { return compilerOptions; } /* * Each project is allowed a GroovyClassLoader that will be used to load transform definitions and supporting classes. A cache * is maintained from project names to the current classpath and associated loader. If the classpath matches the cached version * on a call to build a parser then it is reused. If it does not match then a new loader is created and stored (storing it * orphans the previously cached one). When either a full build or a clean or project close occurs, we also discard the loader * instances associated with the project. */ private static Map<String, PathLoaderPair> projectToLoaderCache = new ConcurrentHashMap<String, PathLoaderPair>(); private static Map<String, ScriptFolderSelector> scriptFolderSelectorCache = new ConcurrentHashMap<String, ScriptFolderSelector>(); static class PathLoaderPair { String classpath; GroovyClassLoader groovyClassLoader; PathLoaderPair(String classpath) { this.classpath = classpath; this.groovyClassLoader = new GroovyClassLoader(createConfigureLoader(classpath)); } } /** * Close the jar files that have been kept open by the URLClassLoader */ public static void close(GroovyClassLoader groovyClassLoader) { try { Class<?> clazz = java.net.URLClassLoader.class; Field field_urlClasspath = clazz.getDeclaredField("ucp"); field_urlClasspath.setAccessible(true); Object urlClasspath = field_urlClasspath.get(groovyClassLoader); Field field_loaders = urlClasspath.getClass().getDeclaredField("loaders"); field_loaders.setAccessible(true); Object[] jarLoaders = ((java.util.Collection<?>) field_loaders.get(urlClasspath)).toArray(); for (Object jarLoader : jarLoaders) { try { Field field_jarFile = jarLoader.getClass().getDeclaredField("jar"); field_jarFile.setAccessible(true); JarFile jarFile = (JarFile) field_jarFile.get(jarLoader); String jarFileName = jarFile.getName(); if (jarFileName.indexOf("cache") != -1 || jarFileName.indexOf("plugins") != -1) { jarFile.close(); } } catch (Throwable t) { // Probably not a JarLoader } } } catch (Throwable t) { // Not the kind of VM we thought it was... } } /** * Remove all cached classloaders for this project */ public static void tidyCache(String projectName) { // This will orphan the loader on the heap projectToLoaderCache.remove(projectName); scriptFolderSelectorCache.remove(projectName); } public static void closeClassLoader(String projectName) { PathLoaderPair pathLoaderPair = projectToLoaderCache.get(projectName); if (pathLoaderPair != null) { close(pathLoaderPair.groovyClassLoader); } } /** * Clears cached class loaders for all caches. It helps to fix problems with cached trait helper classes. */ static void tidyCache() { projectToLoaderCache.clear(); } private GroovyClassLoader gclForBatch = null; private GroovyClassLoader getLoaderFor(String path) { GroovyClassLoader gcl = null; if (projectName == null && path == null) { if (gclForBatch == null) { try { // Batch compilation if (requestor instanceof org.eclipse.jdt.internal.compiler.Compiler) { org.eclipse.jdt.internal.compiler.Compiler compiler = ((org.eclipse.jdt.internal.compiler.Compiler) requestor); LookupEnvironment lookupEnvironment = compiler.lookupEnvironment; if (lookupEnvironment != null) { INameEnvironment nameEnvironment = lookupEnvironment.nameEnvironment; if (nameEnvironment instanceof FileSystem) { FileSystem fileSystem = (FileSystem) nameEnvironment; if (fileSystem != null) { Field f = FileSystem.class.getDeclaredField("classpaths"); if (f != null) { f.setAccessible(true); gclForBatch = new GroovyClassLoader(); Classpath[] classpaths = (Classpath[]) f.get(fileSystem); if (classpaths != null) { for (int i = 0; i < classpaths.length; i++) { gclForBatch.addClasspath(classpaths[i].getPath()); } } } else { System.err.println("Cannot find classpaths field on FileSystem class"); } } } } } } catch (Exception e) { System.err.println("Unexpected problem computing classpath for ast transform loader:"); e.printStackTrace(System.err); } } return gclForBatch; } if (path != null) { if (projectName == null) { // throw new IllegalStateException("Cannot build without knowing project name"); } else { PathLoaderPair pathAndLoader = projectToLoaderCache.get(projectName); if (pathAndLoader == null) { if (GroovyLogManager.manager.hasLoggers()) { GroovyLogManager.manager.log(TraceCategory.AST_TRANSFORM, "Classpath for GroovyClassLoader (used to discover transforms): " + path); } pathAndLoader = new PathLoaderPair(path); projectToLoaderCache.put(projectName, pathAndLoader); } else { if (!path.equals(pathAndLoader.classpath)) { // classpath change detected // System.out.println("Classpath change detected for " + projectName); pathAndLoader = new PathLoaderPair(path); projectToLoaderCache.put(projectName, pathAndLoader); } } // System.out.println("Using loader with path " + pathAndLoader.classpath); gcl = pathAndLoader.groovyClassLoader; } } return gcl; } public GroovyParser(CompilerOptions options, ProblemReporter problemReporter, boolean allowTransforms, boolean isReconcile) { this(null, options, problemReporter, allowTransforms, isReconcile); } public GroovyParser(Object requestor, CompilerOptions options, ProblemReporter problemReporter, boolean allowTransforms, boolean isReconcile) { // FIXASC review callers who pass null for options // FIXASC set parent of the loader to system or context class loader? // record any paths we use for a project so that when the project is cleared, // the paths (which point to cached classloaders) can be cleared this.requestor = requestor; this.compilerOptions = options; this.problemReporter = problemReporter; this.projectName = options.groovyProjectName; this.gclClasspath = (options == null ? null : options.groovyClassLoaderPath); GroovyClassLoader gcl = getLoaderFor(this.gclClasspath); // --- // Status of transforms and reconciling: Oct-18-2011 // Prior to 2.6.0 all transforms were turned OFF for reconciling, and by turned off that meant no phase // processing for them was done at all. With 2.6.0 this phase processing is now active during reconciling // but it is currently limited to only allowing the Grab (global) transform to run. (Not sure why Grab // is a global transform... isn't is always annotation driven). Non global transforms are all off. // This means the transformLoader is setup for the compilation unit but the cu is also told the // allowTransforms setting so it can decide what should be allowed through. // --- // Basic grab support: the design here is that a special classloader is created that will be augmented // with URLs when grab processing is running. This classloader is used as a last resort when resolving // types and is *only* called if a grab has occurred somewhere during compilation. // Currently it is not cached but created each time - we'll have to decide if there is a need to cache GrapeAwareGroovyClassLoader grabbyLoader = new GrapeAwareGroovyClassLoader(gcl); this.compilationUnit = makeCompilationUnit(grabbyLoader, gcl, isReconcile, allowTransforms); this.compilationUnit.removeOutputPhaseOperation(); } public void reset() { GroovyClassLoader gcl = getLoaderFor(gclClasspath); this.compilationUnit = makeCompilationUnit( new GrapeAwareGroovyClassLoader(gcl), gcl, this.compilationUnit.isReconcile, this.compilationUnit.allowTransforms); } static class GrapeAwareGroovyClassLoader extends GroovyClassLoader { // Could be prodded to indicate a grab has occurred within this compilation unit public boolean grabbed = false; // set to true if any grabbing is done public GrapeAwareGroovyClassLoader(ClassLoader parent) { super(parent != null ? parent : Thread.currentThread().getContextClassLoader()); } @Override public void addURL(URL url) { // System.out.println("Grape aware classloader was augmented with " + url); this.grabbed = true; super.addURL(url); } } private static boolean NONLOCKING = false; static { try { boolean value = System.getProperty("greclipse.nonlocking", "false").equalsIgnoreCase("true"); NONLOCKING = value; if (value) { System.out.println("property set: greclipse.nonlocking: will try to avoid locking jars"); } } catch (Throwable t) { } } private static URLClassLoader createLoader(URL[] urls, ClassLoader parent) { if (NONLOCKING) { return new NonLockingJarFileClassLoader("AST Transform loader", urls, parent); } else { return new URLClassLoader(urls, parent); } } private static URLClassLoader createConfigureLoader(String path) { // GRECLIPSE-1090 ClassLoader pcl = GroovyParser.class.getClassLoader();// Thread.currentThread().getContextClassLoader(); if (path == null) { return createLoader(null, pcl); } List<URL> urls = new ArrayList<URL>(); if (path.indexOf(File.pathSeparator) != -1) { int pos = 0; while (pos != -1) { int nextSep = path.indexOf(File.pathSeparator, pos); if (nextSep == -1) { // last piece addNewURL(path.substring(pos), urls); pos = -1; } else { addNewURL(path.substring(pos, nextSep), urls); pos = nextSep + 1; } } } else { addNewURL(path, urls); } return createLoader(urls.toArray(new URL[urls.size()]), pcl); } private static void addNewURL(String path, List<URL> existingURLs) { try { File f = new File(path); URL newURL = f.toURI().toURL(); for (URL url : existingURLs) { if (url.equals(newURL)) { return; } } existingURLs.add(newURL); } catch (MalformedURLException e) { // It was a busted URL anyway } } /** * Call the groovy parser to drive the first few phases of */ public CompilationUnitDeclaration dietParse(ICompilationUnit sourceUnit, CompilationResult compilationResult) { char[] sourceCode = sourceUnit.getContents(); if (sourceCode == null) { sourceCode = CharOperation.NO_CHAR; // pretend empty from thereon } ErrorCollector errorCollector = new GroovyErrorCollectorForJDT(compilationUnit.getConfiguration()); String filepath = null; // This check is necessary because the filename is short (as in the last part, eg. Foo.groovy) for types coming in // from the hierarchy resolver. If there is the same type in two different packages then the compilation process // is going to go wrong because the filename is used as a key in some groovy data structures. This can lead to false // complaints about the same file defining duplicate types. char[] fileName = sourceUnit.getFileName(); if (sourceUnit instanceof org.eclipse.jdt.internal.compiler.batch.CompilationUnit) { filepath = new String(((org.eclipse.jdt.internal.compiler.batch.CompilationUnit) sourceUnit).fileName); } else { filepath = new String(fileName); } // Try to turn this into a 'real' absolute file system reference (this is because Grails 1.5 expects it). Path path = new Path(filepath); IFile eclipseFile = null; // GRECLIPSE-1269 ensure get plugin is not null to ensure the workspace is open (ie- not in batch mode) if (ResourcesPlugin.getPlugin() != null && path.segmentCount() >= 2) { // Needs 2 segments: a project and file name or // eclipse throws assertion failed here. eclipseFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(filepath)); final IPath location = eclipseFile.getLocation(); if (location != null) { filepath = location.toFile().getAbsolutePath(); } } SourceUnit groovySourceUnit = new EclipseSourceUnit(eclipseFile, filepath, new String(sourceCode), compilationUnit.getConfiguration(), compilationUnit.getClassLoader(), errorCollector, this.resolver); groovySourceUnit.isReconcile = compilationUnit.isReconcile; GroovyCompilationUnitDeclaration gcuDeclaration = new GroovyCompilationUnitDeclaration(problemReporter, compilationResult, sourceCode.length, compilationUnit, groovySourceUnit, compilerOptions); // FIXASC get this from the Antlr parser compilationResult.lineSeparatorPositions = GroovyUtils.getSourceLineSeparatorsIn(sourceCode); compilationUnit.addSource(groovySourceUnit); // Check if it is worth plugging in a callback listener for parse/generation if (requestor instanceof org.eclipse.jdt.internal.compiler.Compiler) { org.eclipse.jdt.internal.compiler.Compiler compiler = ((org.eclipse.jdt.internal.compiler.Compiler) requestor); if (compiler.requestor instanceof BatchImageBuilder) { BuildNotifier notifier = ((BatchImageBuilder) compiler.requestor).notifier; if (notifier != null) { compilationUnit.setProgressListener(new ProgressListenerImpl(notifier)); } } } gcuDeclaration.processToPhase(Phases.CONVERSION); // Groovy moduleNode is null when there is a fatal error // Otherwise, recover what we can if (gcuDeclaration.getModuleNode() != null) { gcuDeclaration.populateCompilationUnitDeclaration(); for (TypeDeclaration decl : gcuDeclaration.types) { GroovyTypeDeclaration gtDeclaration = (GroovyTypeDeclaration) decl; resolver.record(gtDeclaration); } } // Is this a script? // If allowTransforms is TRUE then this is a 'full build' and we should remember which are scripts so that // .class file output can be suppressed if (projectName != null && eclipseFile != null) { ScriptFolderSelector scriptFolderSelector = scriptFolderSelectorCache.get(projectName); if (scriptFolderSelector == null) { scriptFolderSelector = new ScriptFolderSelector(ResourcesPlugin.getWorkspace().getRoot().getProject(projectName)); scriptFolderSelectorCache.put(projectName, scriptFolderSelector); } if (scriptFolderSelector.isScript(eclipseFile)) { gcuDeclaration.tagAsScript(); } } if (debugRequestor != null) { debugRequestor.acceptCompilationUnitDeclaration(gcuDeclaration); } return gcuDeclaration; } /** * ProgressListener is called back when parsing of a file or generation of a classfile completes. By calling back to the build * notifier we ignore those long pauses where it look likes it has hung! * * Note: this does not move the progress bar, it merely updates the text */ static class ProgressListenerImpl implements ProgressListener { private BuildNotifier notifier; public ProgressListenerImpl(BuildNotifier notifier) { this.notifier = notifier; } public void parseComplete(int phase, String sourceUnitName) { try { // Chop it down to the containing package folder int lastSlash = sourceUnitName.lastIndexOf("/"); if (lastSlash == -1) { lastSlash = sourceUnitName.lastIndexOf("\\"); } if (lastSlash != -1) { StringBuffer msg = new StringBuffer(); msg.append("Parsing groovy source in "); msg.append(sourceUnitName, 0, lastSlash); notifier.subTask(msg.toString()); } } catch (Exception e) { // doesn't matter } notifier.checkCancel(); } public void generateComplete(int phase, ClassNode classNode) { try { String pkgName = classNode.getPackageName(); if (pkgName != null && pkgName.length() > 0) { StringBuffer msg = new StringBuffer(); msg.append("Generating groovy classes in "); msg.append(pkgName); notifier.subTask(msg.toString()); } } catch (Exception e) { // doesn't matter } notifier.checkCancel(); } } private CompilationUnit makeCompilationUnit(GroovyClassLoader loader, GroovyClassLoader transformLoader, boolean isReconcile, boolean allowTransforms) { // FIXASC (M3) need our own tweaked subclass of CompilerConfiguration? CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); if (compilerOptions.groovyCustomizerClassesList != null && transformLoader != null) { List<CompilationCustomizer> customizers = new ArrayList<CompilationCustomizer>(); if (loader != null) { StringTokenizer tokenizer = new StringTokenizer(compilerOptions.groovyCustomizerClassesList, ","); ClassLoader savedLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(transformLoader); while (tokenizer.hasMoreTokens()) { String classname = tokenizer.nextToken(); try { Class<?> clazz = transformLoader.loadClass(classname); CompilationCustomizer cc = (CompilationCustomizer) clazz.newInstance(); customizers.add(cc); } catch (Exception e) { e.printStackTrace(); } } } finally { Thread.currentThread().setContextClassLoader(savedLoader); } compilerConfiguration.addCompilationCustomizers(customizers.toArray(new CompilationCustomizer[customizers.size()])); } } CompilationUnit cu = new CompilationUnit( compilerConfiguration, null, // CodeSource loader, transformLoader, allowTransforms, compilerOptions.groovyTransformsToRunOnReconcile, compilerOptions.groovyExcludeGlobalASTScan); this.resolver = new JDTResolver(cu); cu.setResolveVisitor(resolver); cu.tweak(isReconcile); // Grails add if (allowTransforms && transformLoader != null && compilerOptions != null && (compilerOptions.groovyFlags & CompilerUtils.IsGrails) != 0) { cu.addPhaseOperation(new GrailsInjector(transformLoader), Phases.CANONICALIZATION); new Grails20TestSupport(compilerOptions, transformLoader).addGrailsTestCompilerCustomizers(cu); cu.addPhaseOperation(new GrailsGlobalPluginAwareEntityInjector(transformLoader), Phases.CANONICALIZATION); // This code makes Grails 1.4.M1 AST transforms work. try { Class<?> klass = Class.forName("org.codehaus.groovy.grails.compiler.injection.GrailsAwareInjectionOperation", true, transformLoader); if (klass != null) { ClassLoader savedLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(transformLoader); PrimaryClassNodeOperation op = (PrimaryClassNodeOperation) klass.newInstance(); cu.addPhaseOperation(op, Phases.CANONICALIZATION); } finally { Thread.currentThread().setContextClassLoader(savedLoader); } } } catch (Throwable t) { // Ignore... probably means its not grails 1.4 project } } // Grails end return cu; } }