/* * 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.util.List; import groovy.lang.GroovyClassLoader; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationUnit.PrimaryClassNodeOperation; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.jdt.groovy.control.EclipseSourceUnit; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IPath; /** * Grails runs an ASTTransform org.codehaus.groovy.grails.compiler.injection.GlobalPluginAwareEntityASTTransformation. But this * transform doesn't execute when compiler is called from within STS/Eclipse because it requires plugin information inside of * GrailsBuildSettings to be intialized and present in BuildSettingsHolder. All of this is finicky and fragile to setup. So instead, * this somewhat hacky workaround in the Groovy Eclipse compiler does the same thing as the tranforms. * * @author Kris De Volder */ public class GrailsGlobalPluginAwareEntityInjector extends PrimaryClassNodeOperation { private static final boolean DEBUG = false; private static void debug(String msg) { System.out.println(msg); } private static class PluginInfo { final String name; final String version; public PluginInfo(String name, String version) { this.name = name; this.version = version; } @Override public String toString() { return "Plugin(name=" + name + ", version=" + version + ")"; } } private GroovyClassLoader groovyClassLoader; // If true then some part of injector has broken down so avoid trying again private boolean broken = false; public GrailsGlobalPluginAwareEntityInjector(GroovyClassLoader groovyClassLoader) { this.groovyClassLoader = groovyClassLoader; } @Override public void call(SourceUnit _sourceUnit, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { if (broken) { return; } if (_sourceUnit instanceof EclipseSourceUnit) { EclipseSourceUnit sourceUnit = (EclipseSourceUnit) _sourceUnit; try { if (classNode.isAnnotationDefinition()) { return; } if (!isFirstClassInModule(classNode)) { // The Grails version of the transform only walk the first class in a module return; } IFile file = sourceUnit.getEclipseFile(); PluginInfo info = getInfo(file); if (info != null) { if (DEBUG) { debug("APPLY transform: " + classNode); } // The transform should be applied. (code below lifted from // org.codehaus.groovy.grails.compiler.injection.GlobalPluginAwareEntityASTTransformation) Class<?> GrailsPlugin_class = Class.forName("org.codehaus.groovy.grails.plugins.metadata.GrailsPlugin", false, groovyClassLoader); final ClassNode annotation = new ClassNode(GrailsPlugin_class); final List<?> list = classNode.getAnnotations(annotation); if (!list.isEmpty()) { return; } final AnnotationNode annotationNode = new AnnotationNode(annotation); annotationNode.addMember("name", new ConstantExpression(info.name)); annotationNode.addMember("version", new ConstantExpression(info.version)); annotationNode.setRuntimeRetention(true); annotationNode.setClassRetention(true); classNode.addAnnotation(annotationNode); } else { if (DEBUG) { debug("SKIP transform: " + classNode); } } } catch (Exception e) { e.printStackTrace(System.err); broken = true; } } } private boolean isFirstClassInModule(ClassNode classNode) { ModuleNode module = classNode.getModule(); if (module != null) { List<ClassNode> classes = module.getClasses(); if (classes != null && classes.size() > 0) { return classes.get(0) == classNode; } } return false; } public static PluginInfo getInfo(IFile file) { if (file == null) { return null; } IPath path = file.getFullPath(); // The path is expected to have this form // Example: // /test-pro/.link_to_grails_plugins/audit-logging-0.5.4/grails-app/controllers/org/codehaus/groovy/grails/plugins/orm/auditable/AuditLogEventController.groovy // Pattern: // /<project-name>/.link_to_grails_plugins/<plugin-name>-<plugin-version>/<the-rest-of-it> // Also stuff in the 'test' folder is excluded from the transform // See grails.util.PluginBuildSettings.getPluginInfoForSource(String) // Test folder path looks like: // /<project-name>/.link_to_grails_plugins/<plugin-name>-<plugin-version>/test/<the-rest-of-it> if (path != null) { if (path.segmentCount() > 3) { String link = path.segment(1); if (link.equals(".link_to_grails_plugins")) { // Same as in JDT SourceFile String pluginNameAndVersion = path.segment(2); int split = findVersionDash(pluginNameAndVersion); if (split >= 0) { if ("test".equals(path.segment(3))) { // Exclude "test" folder in plugins return null; } else { // Pattern matched, extract relevant info. return new PluginInfo(pluginNameAndVersion.substring(0, split), pluginNameAndVersion.substring(split + 1)); } } } } } // If the expected pattern isn't found, no info is extracted // => the transform will not apply. return null; } /** * Find position of the dash separating plugin name from version. * * @return position of dash or -1 if dash not found. */ private static int findVersionDash(String pluginNameAndVersion) { int split = pluginNameAndVersion.lastIndexOf('-'); if (pluginNameAndVersion.endsWith("-SNAPSHOT")) { split = pluginNameAndVersion.lastIndexOf('-', split - 1); } return split; } }