Java tutorial
package com.massfords.maven.spel; /* * Copyright 2001-2005 The Apache Software Foundation. * * 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. */ import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.reflections.Reflections; import org.reflections.scanners.MethodAnnotationsScanner; import org.reflections.util.ConfigurationBuilder; import org.springframework.expression.ParseException; import javax.annotation.Generated; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; /** * Goal for scanning class files looking for annotations with Spring Expression * Language expressions and parsing them during the build to report any errors. * * This is preferrable to finding these errors at runtime. */ @Mojo(name = "spel", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, requiresDependencyResolution = ResolutionScope.TEST) public class SpelPlugin extends AbstractMojo { /** * Injected by maven to give us a reference to the project so we can get the * output directory and other properties. */ @Component private MavenProject project; /** * The maximum number of errors we will tolerate in a module before failing * the build. This is similar to a compiler's threshold. A single error will * fail the build, but we'll continue processing the annotations until we * hit this cap. */ @Parameter(property = "maxValidationErrors", required = false, defaultValue = "100") private int maxValidationErrors; /** * List of annotations that we'll scan for. */ @Parameter(property = "annotations", required = false) private List<SpelAnnotation> annotations; /** * Injected value of our classpath elements. This is used in conjunction with * the Reflections API in order to help identify where annotations appear on * methods. */ @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true) private List<String> projectClasspathElements; @Parameter(property = "failOnError", required = false, defaultValue = "true") private boolean failOnError; /** * Used to validation expressions */ private ExpressionValidator validator = new ExpressionValidator(); private SpelValidationReport report = new SpelValidationReport(); /** * Scans the source code for this module to look for instances of the * annotations we're looking for. * * @throws MojoExecutionException thrown if there are errors during analysis */ public void execute() throws MojoExecutionException { // There's nothing to do if there are no annotations configured. if (annotations == null || annotations.isEmpty()) { getLog().warn("There are no annotations configured so there's nothing for this plugin to do."); return; } int processedCount = 0; try { // these paths should include our module's source root plus any generated code List<String> compileSourceOutputs = Collections.singletonList(project.getBuild().getOutputDirectory()); URL[] sourceFiles = buildMavenClasspath(compileSourceOutputs); // the project classpath includes the source roots plus the transitive // dependencies according to our Mojo annotation's requiresDependencyResolution // attribute. URL[] projectClasspath = buildMavenClasspath(this.projectClasspathElements); URLClassLoader projectClassloader = new URLClassLoader(projectClasspath); // todo - is there any savings to be had here by caching the reflections but tweaking the filters for the given module? Reflections reflections = new Reflections(new ConfigurationBuilder().setUrls(sourceFiles) .addClassLoaders(projectClassloader).setScanners(new MethodAnnotationsScanner()) // todo here is where to filter the packages // .filterInputsBy(new Predicate<String>() { // @Override // public boolean apply(String input) { // return true; // } // }) ); // using the newly created classloaders and reflections API, scan the // classpath looking for instances of the annotations processedCount += processAnnotations(projectClassloader, reflections); } catch (SpelValidationException e) { if (failOnError) { throw new MojoExecutionException("A fatal error occurred while validating Spel annotations," + " see stack trace for details.", e); } } catch (Throwable e) { throw new MojoExecutionException( "A fatal error occurred while validating Spel annotations," + " see stack trace for details.", e); } getLog().info(String.format("Processed %d annotations", processedCount)); report.createReportFile( new File(this.project.getModel().getBuild().getDirectory(), "spel-maven-plugin/report.json")); if (failOnError && report.hasErrors()) { throw new MojoExecutionException("Spel validation failed on one or more annotations. See the specific" + "error output for more information."); } } /** * Walks all of the annotations and looks for instances of the annotation * on various methods using the Reflections API. * * @param projectClassloader * @param reflections * @return * @throws Exception */ private int processAnnotations(URLClassLoader projectClassloader, Reflections reflections) throws Exception { int processedCount = 0; for (SpelAnnotation sa : annotations) { initAnnotationType(projectClassloader, sa); // if we don't know about the class for the annotation then it's // likely that the module was configured to scan for an annotation // that's not on its classpath. This could be a parent module or // a misconfiguration. Either way, we would have logged it above // and hopefully at some point we'll encounter a module that knows // about the annotation we're trying to validate. if (sa.getClazz() != null) { processedCount += validateAllAnnotationExpressions(reflections, sa); } } return processedCount; } /** * Scans the classpath looking for instances of the annotation on methods * to validate the expressions on the annotation if present. * * @param reflections * @param sa * @return * @throws Exception */ private int validateAllAnnotationExpressions(Reflections reflections, SpelAnnotation sa) throws Exception { int processedCount = 0; Class<? extends Annotation> annoType = sa.getClazz(); Set<Method> set = reflections.getMethodsAnnotatedWith(annoType); for (Method m : set) { Annotation anno = m.getAnnotation(annoType); Method attrGetter = annoType.getDeclaredMethod(sa.getAttribute()); Object expressionObj = attrGetter.invoke(anno); if (expressionObj instanceof String) { getLog().debug(String.format("Validating expression: %s", expressionObj)); String expression = (String) attrGetter.invoke(anno); try { processedCount++; validator.validate(expression, sa.getExpressionRootClass()); report.success(); } catch (ParseException e) { String pattern = "Spel annotation %s.%s with expression %s failed to parse. Error message: %s"; String formatted = String.format(pattern, sa.getName(), sa.getAttribute(), expression, e.getMessage()); reportError(formatted); } catch (ExpressionValidationException e) { String message = "Spel annotation %s.%s with expression %s has validation errors. Validation error message: %s"; String formatted = String.format(message, sa.getName(), sa.getAttribute(), expression, e.getMessage()); reportError(formatted); } } else if (expressionObj != null) { String message = "Spel annotation %s.%s is not configured with a string."; reportError(String.format(message, sa.getName(), sa.getAttribute())); } } return processedCount; } /** * Initializes the class for the given SpelAnnotation if it can be loaded * from the current classpath. This method handles the possibility that the * annotation cannot be loaded in case the user configured the plugin at * a parent module or similar in the idea that child modules would inherit * the configuration. * * @param projectClassloader * @param sa */ private void initAnnotationType(URLClassLoader projectClassloader, SpelAnnotation sa) { if (sa.getClazz() == null) { try { //noinspection unchecked Class<? extends Annotation> clazz = (Class<? extends Annotation>) projectClassloader .loadClass(sa.getName()); sa.setClazz(clazz); getLog().info(String.format("Loaded annotation %s", sa.getName())); } catch (Exception e) { getLog().warn("Could not find and instantiate class for annotation with name: " + sa.getName()); } } if (sa.getExpressionRootClass() == null && sa.getExpressionRoot() != null) { try { //noinspection unchecked Class<?> clazz = projectClassloader.loadClass(sa.getExpressionRoot()); sa.setExpressionRootClass(clazz); getLog().info(String.format("Loaded annotation expressionRoot %s", sa.getExpressionRoot())); } catch (Exception e) { getLog().warn("Could not find and instantiate class for annotation expressionRoot with name: " + sa.getExpressionRoot()); } } } /** * Extracted this method simply for unit testing. * @param classpathElements List of class path entries from the maven project * @return array of URL's for the classpath * @throws MojoExecutionException only thrown if we can't convert a classpath element to a URL which shouldn't happen */ protected URL[] buildMavenClasspath(List<String> classpathElements) throws MojoExecutionException { List<URL> projectClasspathList = new ArrayList<>(); for (String element : classpathElements) { try { projectClasspathList.add(new File(element).toURI().toURL()); } catch (MalformedURLException e) { throw new MojoExecutionException(element + " is an invalid classpath element", e); } } return projectClasspathList.toArray(new URL[projectClasspathList.size()]); } /** * Reports the occurrence of an error while testing * @param message the error message to report * @throws SpelValidationException if the amount of errors exceeds the specified maximum */ private void reportError(String message) throws SpelValidationException { getLog().error(message); report.error(message); if (report.getErrorCount() >= maxValidationErrors) { throw new SpelValidationException("Reached Maximum Amount of Errors"); } } @Generated("generated by IDE") public MavenProject getProject() { return project; } @Generated("generated by IDE") public void setProject(MavenProject project) { this.project = project; } @Generated("generated by IDE") public List<SpelAnnotation> getAnnotations() { return annotations; } @Generated("generated by IDE") public void setAnnotations(List<SpelAnnotation> annotations) { this.annotations = annotations; } @Generated("generated by IDE") public SpelPlugin setMaxValidationErrors(int maxValidationErrors) { this.maxValidationErrors = maxValidationErrors; return this; } @Generated("generated by IDE") public List<String> getProjectClasspathElements() { return projectClasspathElements; } @Generated("generated by IDE") public void setProjectClasspathElements(List<String> projectClasspathElements) { this.projectClasspathElements = projectClasspathElements; } @Generated("generated by IDE") public boolean isFailOnError() { return failOnError; } @Generated("generated by IDE") public void setFailOnError(boolean failOnError) { this.failOnError = failOnError; } }