Java tutorial
package com.atlassian.maven.plugin.clover; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 com.atlassian.clover.ant.groovy.GroovycSupport; import com.atlassian.clover.cfg.instr.InstrumentationConfig; import com.atlassian.clover.remote.DistributedConfig; import com.atlassian.maven.plugin.clover.internal.AbstractCloverInstrumentMojo; import com.atlassian.maven.plugin.clover.internal.CompilerConfiguration; import com.atlassian.maven.plugin.clover.internal.instrumentation.MainInstrumenter; import com.atlassian.maven.plugin.clover.internal.instrumentation.TestInstrumenter; import com.atlassian.maven.plugin.clover.internal.scanner.LanguageFileExtensionFilter; import com.atlassian.maven.plugin.clover.internal.scanner.MainSourceScanner; import com.atlassian.maven.plugin.clover.internal.scanner.TestSourceScanner; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.model.Resource; import org.apache.maven.plugin.MojoExecutionException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; /* * TRICKY PART HOW JAVA AND GROOVY SOURCE FOLDERS ARE HANDLED * * PROBLEM DIMENSIONS: * #1 language: * -> java only * -> java + groovy * -> groovy only * #2 source folders: * -> src/xxx/java * -> src/xxx/groovy * -> generated-sources/xxx * #3 location of java source files: * -> in src/xxx/java * -> in src/xxx/groovy - NOT SUPPORTED BY CLOVER (see reasons below) * #4 location of groovy source files: * -> src/xxx/java * -> src/xxx/groovy * #5 definition of groovy source folders * -> by <sourceDirectory>, <testSourceDirectory> parameters in POM * -> by add-source, add-test-source goals in build-helper-maven-plugin * -> by extensions=true option in groovy-eclipse-compiler * -> not defined at all (but src/xxx/groovy location is then internally added by gmaven or groovy-eclipse-plugin) * #6 groovy plugins * -> gmaven * -> groovy-eclipse-plugin * #7 instrumentation scope * -> non-generated sources * -> all sources * #8 java/groovy compilation * -> separated (maven-compiler compiles java, gmaven compiles groovy) * -> joint compilation (groovy-eclipse-plugin compiles both java and groovy) * #9 build lifecycle * -> non-forked (clover:setup) * -> forked (clover:instrument) * * ===================================================================================================================== * 1) includeAllSourceRoots = false, java+groovy in 'src/(main|test)/java' * * -> source directories are read from getCompileSourceRoots() * -> *.java files are instrumented in the source and saved in 'src-(test-)instrumented' * -> *.groovy are copied "as is" to 'src-(test-)instrumented' too * -> source roots are overridden: * 'src/main/java' -> 'src-instrumented' * 'src/test/java' -> 'src-test-instrumented' * -> groovyc compiles sources from source roots listed above: * -> *.java files are already instrumented in the source, compile "as is" * -> *.groovy files are being instrumented in the AST * * ===================================================================================================================== * 1b) includeAllSourceRoots = true, java+groovy in 'src/(main|test)/java', generated sources in * 'target/generated-(test-)sources' * * -> as for case 1) but 'generated-(test-)sources' are instrumented too and stored in 'src-(test-)instrumented' * * ===================================================================================================================== * 2) includeAllSourceRoots = false, java in 'src/(main|test)/java', groovy in 'src/(main|test)/(java|groovy)' * * -> build-helper-maven-plugin adds 'src/(main|test)/groovy' as extra source root * -> source directories are read from getCompileSourceRoots() * -> *.java files are instrumented in the source and saved in 'src-(test-)instrumented' * -> *.groovy files * from 'src/(main|test)/java' are copied to 'src-(test-)instrumented' * from 'src/(main|test)/groovy' are not copied anywhere * -> source roots are switched: * 'src/main/java' -> 'src-instrumented' * 'src/test/java' -> 'src-test-instrumented' * 'src/main/groovy' -> (unchanged) * 'src/test/groovy' -> (unchanged) * -> groovyc compiles sources from all source roots listed above * -> *.java files are already instrumented in the source, compile "as is" * -> *.groovy files are instrumented in the AST * * ===================================================================================================================== * 2b) includeAllSourceRoots = true, java in 'src/(main|test)/java', groovy in 'src/(main|test)/(java|groovy)', * generated sources in 'target/generated-(test-)sources * * -> as for case 2) but 'generated-(test-)sources' are instrumented too and stored in 'src-(test-)instrumented' * * ===================================================================================================================== * 3) includeAllSourceRoots = false, java in 'src/(main|test)/java', groovy in 'src/(main|test)/(java|groovy)' * * -> the 'src/(main|test)/groovy' is NOT added as extra source root * -> so that getCompileSourceRoots() does not return it in the list * -> groovy-eclipse-plugin will add the 'src/(main|test)/groovy' location internally (typically after the * clover:setup goal is finished) * -> source directories are read from getCompileSourceRoots() * -> GroovyMain/TestSourceScanner contains additional hardcoded location for 'src/(main|test)/groovy' * -> *.java files are instrumented in the source and saved in 'src-(test-)instrumented' * -> *.groovy files * from 'src/(main|test)/java' are copied to 'src-(test-)instrumented' * from 'src/(main|test)/groovy' are not copied anywhere * -> source roots are switched: * 'src/main/java' -> 'src-instrumented' * 'src/test/java' -> 'src-test-instrumented' * -> groovyc compiles sources from all source roots listed above * -> *.java files are already instrumented, compile "as is" * -> *.groovy files are instrumented in the AST * -> groovy-eclipse-plugin internally adds 'src/(main|test)/groovy' to the list of source roots * * ===================================================================================================================== * 3b) includeAllSourceRoots = true, java in 'src/(main|test)/java', groovy in 'src/(main|test)/(java|groovy)' * generated sources in 'target/generated-(test-)sources * * -> as for case 3) but 'generated-(test-)sources' are instrumented too and stored in 'src-(test-)instrumented' * * ===================================================================================================================== * 4) includeAllSourceRoots = false, no java code, groovy in 'src/(main|test)/groovy', * sourceDirectory/testSourceDirectory are used on POM * * -> the 'src/(main|test)/groovy' is added as sourceDirectory and testSourceDirectory * -> so that getProject().getBuild().getSourceDirectory() returns it * -> so that getProject().getCompileSourceRoots() returns it * -> *.groovy files are not copied anywhere * -> source roots are NOT switched * -> groovyc compiles sources from original source roots ('src/(main|test)/groovy') * * ===================================================================================================================== * 4b) includeAllSourceRoots = true, no java code, groovy in 'src/(main|test)/groovy', * sourceDirectory/testSourceDirectory are used on POM * * -> as for case 4) but 'generated-(test-)sources' are passed to groovyc * * ===================================================================================================================== * 5) includeAllSourceRoots = false, java in src/xxx/java, no groovy code, no groovy plugin * * -> standard behaviour, 'src/(main|test)/java' is instrumented into 'target/clover/src-(test-)instrumented' * and compiled * * ===================================================================================================================== * 5b) includeAllSourceRoots = true, java in src/xxx/java, no groovy code, no groovy plugin * * -> standard behaviour, 'src/(main|test)/java' as well as 'target/generated-(test-)sources' are instrumented * into 'target/clover/src-(test-)instrumented' and compiled */ /** * <p>Instrument source roots.</p> * <p><b>Note 1: Do not call this MOJO directly. It is meant to be called in a custom forked lifecycle by the other * Clover plugin MOJOs.</b></p> * <p><b>Note 2: We bind this mojo to the "validate" phase so that it executes prior to any other mojos</b></p> * * @goal instrumentInternal * @phase validate * @requiresDependencyResolution test */ public class CloverInstrumentInternalMojo extends AbstractCloverInstrumentMojo { public static final String CLOVER_CORE_GROUP_ID = "com.atlassian.clover"; public static final String CLOVER_CORE_ARTIFACT_ID = "clover"; /** * <p>List of all artifacts for this Clover plugin provided by Maven. This is used internally to get a handle on * the Clover JAR artifact.</p> * <p>Note: This is passed by Maven and must not be configured by the user.</p> * * @parameter expression="${plugin.artifacts}" * @required */ private List<Artifact> pluginArtifacts; /** * @parameter expression="${component.org.apache.maven.artifact.factory.ArtifactFactory}" * @required * @readonly */ private ArtifactFactory artifactFactory; /** * Artifact resolver used to find clovered artifacts (artifacts with a clover classifier). * * @component role="org.apache.maven.artifact.resolver.ArtifactResolver" * @required * @readonly */ private ArtifactResolver artifactResolver; /** * Local maven repository. * * @parameter expression="${localRepository}" * @required */ private ArtifactRepository localRepository; /** * Remote repositories used for the project. * * @parameter expression="${project.remoteArtifactRepositories}" */ protected List<ArtifactRepository> repositories; // HACK: this allows us to reset the source directories to the originals private static Map<String, String> originalSrcMap = new HashMap<String, String>(); private static Map<String, String> originalSrcTestMap = new HashMap<String, String>(); public static String getOriginalSrcDir(final String module) { return originalSrcMap.get(module); } public static String getOriginalSrcTestDir(final String module) { return originalSrcTestMap.get(module); } /** * {@inheritDoc} * * @see com.atlassian.maven.plugin.clover.internal.AbstractCloverMojo#execute() */ @Override public void execute() throws MojoExecutionException { if (skip) { getLog().info("Skipping clover instrumentation."); return; } super.execute(); configureTestFailureIgnore(); resetSrcDirsOriginal(getProject().getArtifact(), this); final File outDir = new File(this.cloverOutputDirectory, getSrcName()); final String cloverOutputSourceDirectory = outDir.getPath(); final String cloverOutputTestSourceDirectory = new File(this.cloverOutputDirectory, getSrcTestName()) .getPath(); new File(resolveCloverDatabase()).getParentFile().mkdirs(); logArtifacts("before changes"); // Instrument both the main sources and the test sources if the user has configured it final MainInstrumenter mainInstrumenter = new MainInstrumenter(this, cloverOutputSourceDirectory); final TestInstrumenter testInstrumenter = new TestInstrumenter(this, cloverOutputTestSourceDirectory); if (isJavaProject()) { mainInstrumenter.instrument(); if (this.includesTestSourceRoots) { testInstrumenter.instrument(); } } // add clover.jar to classpath addCloverDependencyToCompileClasspath(); // deal with '-clover' artifacts in dependencies swizzleCloverDependencies(); // Modify Maven model so that it points to the new source directories and to the clovered // artifacts instead of the original values. final String originalSrcDir = mainInstrumenter.redirectSourceDirectories(); originalSrcMap.put(getProject().getArtifact().getId(), originalSrcDir); if (this.includesTestSourceRoots) { final String originalSrcTestDir = testInstrumenter.redirectSourceDirectories(); originalSrcTestMap.put(getProject().getArtifact().getId(), originalSrcTestDir); } // add instrumentation of groovy sources injectGrover(outDir); redirectOutputDirectories(); redirectArtifact(); logArtifacts("after changes"); } @Override protected boolean shouldRedirectArtifacts() { return true; } @Override protected boolean shouldRedirectOutputDirectories() { return true; } /** * Sets several properties related with test failures for Surefire, Failsafe, PMD and Checkstyle plugins. * Thanks to this, the build in default or forked lifecycle can continue and we can generate Clover report * even in presence of test failures. */ private void configureTestFailureIgnore() { if (setTestFailureIgnore) { getLog().debug("Configuring testFailureIgnore=true and failOnViolation=false"); final Properties properties = getProject().getProperties(); properties.put("maven.test.failure.ignore", "true"); // surefire and failsafe properties.put("checkstyle.failOnViolation", "false"); properties.put("pmd.failOnViolation", "false"); } } /** * @param outDir - output directory for temporary artifacts */ private void injectGrover(final File outDir) { if (skipGroverJar) { getLog().info( "Generation of Clover Groovy configuration is disabled. No Groovy instrumentation will occur."); return; } // create the groovy config for Clover's ASTTransformer InstrumentationConfig config = new InstrumentationConfig(); config.setProjectName(this.getProject().getName()); config.setInitstring(this.resolveCloverDatabase()); config.setTmpDir(outDir); final List<File> includeFiles = calcIncludedFilesForGroovy(); getLog().debug("Clover including the following files for Groovy instrumentation: " + includeFiles); config.setIncludedFiles(includeFiles); config.setEnabled(true); config.setEncoding(getEncoding()); //Don't pass in an instance of DistributedCoverage because it can't be deserialised //by Grover (ClassNotFoundException within the groovyc compiler) config.setDistributedConfig(getDistributedCoverage() == null ? null : new DistributedConfig(getDistributedCoverage().getConfigString())); try { File groverJar = GroovycSupport.extractGroverJar(this.groverJar, false); File groverConfigDir = GroovycSupport.newConfigDir(config, new File(getProject().getBuild().getOutputDirectory())); final Resource groverConfigResource = new Resource(); groverConfigResource.setDirectory(groverConfigDir.getPath()); getProject().addResource(groverConfigResource); // get the clover artifact, and use the same version number for grover... Artifact cloverArtifact = findCloverArtifact(this.pluginArtifacts); // add grover to the compilation classpath final Artifact groverArtifact = artifactFactory.createBuildArtifact(cloverArtifact.getGroupId(), "grover", cloverArtifact.getVersion(), "jar"); groverArtifact.setFile(groverJar); groverArtifact.setScope(Artifact.SCOPE_SYSTEM); addArtifactDependency(groverArtifact); } catch (IOException e) { getLog().error( "Could not create Clover Groovy configuration file. No Groovy instrumentation will occur. " + e.getMessage(), e); } } /** * @return List<File> * @see com.atlassian.maven.plugin.clover.internal.instrumentation.AbstractInstrumenter#instrument() * @see #redirectOutputDirectories() * @see <a href="http://groovy.codehaus.org/Groovy-Eclipse+compiler+plugin+for+Maven">Groovy-Eclipse+compiler+plugin+for+Maven</a> */ protected List<File> calcIncludedFilesForGroovy() { final MainSourceScanner groovyMainScanner = new MainSourceScanner(this, getProject().getBuild().getOutputDirectory()); final List<File> mainGroovyFiles = extractIncludes( groovyMainScanner.getSourceFilesToInstrument(LanguageFileExtensionFilter.GROOVY_LANGUAGE, false)); final TestSourceScanner groovyTestScanner = new TestSourceScanner(this, getProject().getBuild().getOutputDirectory()); final List<File> testGroovyFiles = extractIncludes( groovyTestScanner.getSourceFilesToInstrument(LanguageFileExtensionFilter.GROOVY_LANGUAGE, false)); // combine lists final List<File> allSources = new ArrayList<File>(mainGroovyFiles); allSources.addAll(testGroovyFiles); return allSources; } private ArrayList<File> extractIncludes(final Map<String, String[]> srcFiles) { final ArrayList<File> includeFiles = new ArrayList<File>(); for (final String dirName : srcFiles.keySet()) { final String[] includes = srcFiles.get(dirName); for (final String include : includes) { includeFiles.add(new File(dirName, include)); } } return includeFiles; } public static void resetSrcDirsOriginal(final Artifact artifact, final CompilerConfiguration config) { if (originalSrcMap.containsKey(artifact.getId())) { final String sourceDirectory = originalSrcMap.get(artifact.getId()); MainInstrumenter mainInstrumenter = new MainInstrumenter(config, sourceDirectory); mainInstrumenter.redirectSourceDirectories(); } if (originalSrcTestMap.containsKey(artifact.getId())) { final String testDirectory = originalSrcTestMap.get(artifact.getId()); TestInstrumenter instrumenter = new TestInstrumenter(config, testDirectory); instrumenter.redirectSourceDirectories(); } } protected String getSrcTestName() { return "src-test"; } protected String getSrcName() { return "src"; } private boolean isJavaProject() { final ArtifactHandler artifactHandler = getProject().getArtifact().getArtifactHandler(); if (!"java".equals(artifactHandler.getLanguage())) { getLog().debug("The reported language of this project is " + artifactHandler.getLanguage() + ", attempting to instrument sources anyway."); } return true; } protected void redirectOutputDirectories() { if (shouldRedirectOutputDirectories()) { // Explicitly set the output directory to be the Clover one so that all other plugins executing // thereafter output files in the Clover output directory and not in the main output directory. getProject().getBuild().setDirectory(this.cloverOutputDirectory); // TODO: Ugly hack below. Changing the directory should be enough for changing the values of all other // properties depending on it! getProject().getBuild().setOutputDirectory(new File(this.cloverOutputDirectory, "classes").getPath()); getProject().getBuild() .setTestOutputDirectory(new File(this.cloverOutputDirectory, "test-classes").getPath()); } } /** * Modify main artifact to add a "clover" classifier to it so that it's not mixed with the main artifact of * a normal build. */ protected void redirectArtifact() { if (shouldRedirectArtifacts()) { // Only redirect main artifact for non-pom projects if (!getProject().getPackaging().equals("pom")) { Artifact oldArtifact = getProject().getArtifact(); Artifact newArtifact = this.artifactFactory.createArtifactWithClassifier(oldArtifact.getGroupId(), oldArtifact.getArtifactId(), oldArtifact.getVersion(), oldArtifact.getType(), "clover"); getProject().setArtifact(newArtifact); final String finalName = getProject().getBuild().getFinalName() == null ? (getProject().getArtifactId() + "-" + getProject().getVersion()) : getProject().getBuild().getFinalName(); getProject().getBuild().setFinalName(finalName + (useCloverClassifier ? "-clover" : "")); } } } /** * Browse through all project dependencies and try to find a clovered version of the dependency. If found * replace the main depedencency by the clovered version. */ private void swizzleCloverDependencies() { final Set<Artifact> swizzledDependencyArtifacts = swizzleCloverDependencies( getProject().getDependencyArtifacts()); // only swizzle the difference between artifacts and dependency artifacts to ensure no dupes final Set<Artifact> artifacts = getProject().getArtifacts(); final Set<Artifact> dependencyArtifacts = getProject().getDependencyArtifacts(); artifacts.removeAll(dependencyArtifacts); final Set<Artifact> swizzledArtifacts = swizzleCloverDependencies(artifacts); swizzledArtifacts.addAll(swizzledDependencyArtifacts); getProject().setDependencyArtifacts(swizzledDependencyArtifacts); getProject().setArtifacts(swizzledArtifacts); } protected Set<Artifact> swizzleCloverDependencies(final Set<Artifact> artifacts) { Set<Artifact> resolvedArtifacts = new LinkedHashSet<Artifact>(); for (Artifact artifact : artifacts) { // Do not try to find Clovered versions for artifacts with classifiers. This is because Maven only // supports a single classifier per artifact and thus if we replace the original classifier with // a Clover classifier the artifact will fail to perform properly as intended originally. This is a // limitation. if (artifact.getClassifier() == null) { Artifact cloveredArtifact = this.artifactFactory.createArtifactWithClassifier(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), artifact.getType(), "clover"); // Try to resolve the artifact with a clover classifier. If it doesn't exist, simply add the original // artifact. If found, use the clovered artifact. try { this.artifactResolver.resolve(cloveredArtifact, new ArrayList(), localRepository); // Set the same scope as the main artifact as this is not set by createArtifactWithClassifier. cloveredArtifact.setScope(artifact.getScope()); // Check the timestamp of the artifact. If the found clovered version is older than the // non-clovered one we need to use the non-clovered version. This is to handle use case such as: // - Say you have a module B that depends on a module A // - You run Clover on A // - You make modifications on A such that B would fail if not built with the latest version of A // - You try to run the Clover plugin on B. The build would fail if we didn't pick the latest // version between the original A version and the clovered version. // // We provide a 'fudge-factor' of 2 seconds, as the clover artifact is created first. if (cloveredArtifact.getFile().lastModified() + cloveredArtifactExpiryInMillis < artifact .getFile().lastModified()) { getLog().warn("Using [" + artifact.getId() + "], built on " + new Date(artifact.getFile().lastModified()) + " even though a Clovered version exists " + "but it's older (lastModified: " + new Date(cloveredArtifact.getFile().lastModified()) + " ) and could fail the build. Please consider running Clover again on that " + "dependency's project."); resolvedArtifacts.add(artifact); } else { resolvedArtifacts.add(cloveredArtifact); } } catch (ArtifactResolutionException e) { getLog().warn("Skipped dependency [" + artifact.getId() + "] due to resolution error: " + e.getMessage()); resolvedArtifacts.add(artifact); } catch (ArtifactNotFoundException e) { getLog().debug("Skipped dependency [" + artifact.getId() + "] as the clovered artifact could not be found"); resolvedArtifacts.add(artifact); } } else { getLog().debug("Skipped dependency [" + artifact.getId() + "] as it has a classifier"); resolvedArtifacts.add(artifact); } } return resolvedArtifacts; } protected Artifact findCloverArtifact(final List<Artifact> pluginArtifacts) { Artifact cloverArtifact = null; Iterator<Artifact> artifactsIterator = pluginArtifacts.iterator(); while (artifactsIterator.hasNext() && cloverArtifact == null) { Artifact artifact = artifactsIterator.next(); // We identify the clover JAR by checking the groupId and artifactId. if (CLOVER_CORE_GROUP_ID.equals(artifact.getGroupId()) && CLOVER_CORE_ARTIFACT_ID.equals(artifact.getArtifactId())) { cloverArtifact = artifact; } } return cloverArtifact; } private void addCloverDependencyToCompileClasspath() throws MojoExecutionException { Artifact cloverArtifact = findCloverArtifact(this.pluginArtifacts); if (cloverArtifact == null) { throw new MojoExecutionException("Couldn't find [" + CLOVER_CORE_GROUP_ID + ":" + CLOVER_CORE_ARTIFACT_ID + "] artifact in plugin dependencies"); } final String jarScope = scope == null ? Artifact.SCOPE_PROVIDED : scope; cloverArtifact = artifactFactory.createArtifact(cloverArtifact.getGroupId(), cloverArtifact.getArtifactId(), cloverArtifact.getVersion(), jarScope, cloverArtifact.getType()); try { this.artifactResolver.resolve(cloverArtifact, repositories, localRepository); } catch (AbstractArtifactResolutionException e) { throw new MojoExecutionException("Could not resolve the clover artifact ( " + cloverArtifact.getId() + " ) in the localRepository: " + localRepository.getUrl(), e); } addArtifactDependency(cloverArtifact); } private void addArtifactDependency(final Artifact cloverArtifact) { // TODO: use addArtifacts when it's implemented, see http://jira.codehaus.org/browse/MNG-2197 Set<Artifact> set = new LinkedHashSet<Artifact>(getProject().getDependencyArtifacts()); set.add(cloverArtifact); getProject().setDependencyArtifacts(set); } private void logArtifacts(final String message) { if (getLog().isDebugEnabled()) { getLog().debug("[Clover] List of dependency artifacts " + message + ":"); logArtifacts(getProject().getDependencyArtifacts()); getLog().debug("[Clover] List of artifacts " + message + ":"); logArtifacts(getProject().getArtifacts()); } } private void logArtifacts(final Set<Artifact> artifacts) { for (Artifact artifact : artifacts) { getLog().debug("[Clover] Artifact [" + artifact.getId() + "], scope = [" + artifact.getScope() + "]"); } } protected void setArtifactFactory(final ArtifactFactory artifactFactory) { this.artifactFactory = artifactFactory; } protected void setArtifactResolver(final ArtifactResolver artifactResolver) { this.artifactResolver = artifactResolver; } }