diff --git a/ivy.xml b/ivy.xml index a695f6f5..065ca7ca 100644 --- a/ivy.xml +++ b/ivy.xml @@ -26,6 +26,7 @@ + diff --git a/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java b/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java index 3a59c9fc..5ecb0328 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java +++ b/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java @@ -18,9 +18,15 @@ import static de.thetaphi.forbiddenapis.Checker.Option.*; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.factory.ArtifactFactory; +import org.apache.maven.artifact.repository.ArtifactRepository; +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.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.plexus.util.DirectoryScanner; @@ -33,6 +39,8 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.RetentionPolicy; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLClassLoader; import java.net.MalformedURLException; import java.net.URL; @@ -41,6 +49,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Set; /** * Base class for forbiddenapis Mojos. @@ -56,6 +65,36 @@ public abstract class AbstractCheckMojo extends AbstractMojo { @Parameter(required = false) private File[] signaturesFiles; + /** + * Lists all Maven artifacts, which contain signatures and comments for forbidden API calls. + * The artifact needs to be specified like a Maven dependency. Resolution is not transitive. + * You can refer to plain text Maven artifacts ({@code type="txt"}, e.g., with a separate {@code classifier}): + *
+   * <signaturesArtifact>
+   *   <groupId>org.apache.foobar</groupId>
+   *   <artifactId>example</artifactId>
+   *   <version>1.0</version>
+   *   <classifier>signatures</classifier>
+   *   <type>txt</type>
+   * </signaturesArtifact>
+   * 
+ * Alternatively, refer to signatures files inside JAR artifacts. In that case, the additional + * parameter {@code path} has to be given: + *
+   * <signaturesArtifact>
+   *   <groupId>org.apache.foobar</groupId>
+   *   <artifactId>example</artifactId>
+   *   <version>1.0</version>
+   *   <type>jar</type>
+   *   <path>path/inside/jar/file/signatures.txt</path>
+   * </signaturesArtifact>
+   * 
+ *

The signatures are resolved against the compile classpath. + * @since 2.0 + */ + @Parameter(required = false) + private SignaturesArtifact[] signaturesArtifacts; + /** * Gives a multiline list of signatures, inline in the pom.xml. Use an XML CDATA section to do that! * The signatures are resolved against the compile classpath. @@ -163,6 +202,18 @@ public abstract class AbstractCheckMojo extends AbstractMojo { /** The project packaging (pom, jar, etc.). */ @Parameter(defaultValue = "${project.packaging}", readonly = true, required = true) private String packaging; + + @Component + private ArtifactFactory artifactFactory; + + @Component + private ArtifactResolver artifactResolver; + + @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true) + private List remoteRepositories; + + @Parameter(defaultValue = "${localRepository}", readonly = true, required = true) + private ArtifactRepository localRepository; /** provided by the concrete Mojos for compile and test classes processing */ protected abstract List getClassPathElements(); @@ -174,10 +225,46 @@ public abstract class AbstractCheckMojo extends AbstractMojo { protected String getTargetVersion() { return targetVersion; } + + private File resolveSignaturesArtifact(SignaturesArtifact signaturesArtifact) throws ArtifactResolutionException, ArtifactNotFoundException { + final Artifact artifact = signaturesArtifact.createArtifact(artifactFactory); + artifactResolver.resolve(artifact, this.remoteRepositories, this.localRepository); + return artifact.getFile(); + } + + private String encodeUrlPath(String path) { + try { + // hack to encode the URL path by misusing URI class: + return new URI(null, path, null).toASCIIString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private URL createJarUrl(File f, String jarPath) throws MalformedURLException { + final URL fileUrl = f.toURI().toURL(); + final URL jarBaseUrl = new URL("jar", null, fileUrl.toExternalForm() + "!/"); + return new URL(jarBaseUrl, encodeUrlPath(jarPath)); + } @Override public void execute() throws MojoExecutionException { - final Log log = getLog(); + final Logger log = new Logger() { + @Override + public void error(String msg) { + getLog().error(msg); + } + + @Override + public void warn(String msg) { + getLog().warn(msg); + } + + @Override + public void info(String msg) { + getLog().info(msg); + } + }; if (skip) { log.info("Skipping forbidden-apis checks."); @@ -202,7 +289,6 @@ public void execute() throws MojoExecutionException { urls[i++] = new File(cpElement).toURI().toURL(); } assert i == urls.length; - if (log.isDebugEnabled()) log.debug("Compile Classpath: " + Arrays.toString(urls)); } catch (MalformedURLException e) { throw new MojoExecutionException("Failed to build classpath.", e); } @@ -218,22 +304,7 @@ public void execute() throws MojoExecutionException { if (failOnMissingClasses) options.add(FAIL_ON_MISSING_CLASSES); if (failOnViolation) options.add(FAIL_ON_VIOLATION); if (failOnUnresolvableSignatures) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES); - final Checker checker = new Checker(new Logger() { - @Override - public void error(String msg) { - log.error(msg); - } - - @Override - public void warn(String msg) { - log.warn(msg); - } - - @Override - public void info(String msg) { - log.info(msg); - } - }, loader, options); + final Checker checker = new Checker(log, loader, options); if (!checker.isSupportedJDK) { final String msg = String.format(Locale.ENGLISH, @@ -287,9 +358,27 @@ public void info(String msg) { checker.parseBundledSignatures(bs, targetVersion); } } - if (signaturesFiles != null) for (final File f : new LinkedHashSet(Arrays.asList(signaturesFiles))) { + final Set sigFiles = new LinkedHashSet(); + final Set sigUrls = new LinkedHashSet(); + if (signaturesFiles != null) { + sigFiles.addAll(Arrays.asList(signaturesFiles)); + } + if (signaturesArtifacts != null) { + for (final SignaturesArtifact artifact : signaturesArtifacts) { + final File f = resolveSignaturesArtifact(artifact); + if (artifact.path != null) { + sigUrls.add(createJarUrl(f, artifact.path)); + } else { + sigFiles.add(f); + } + } + } + for (final File f : sigFiles) { checker.parseSignaturesFile(f); } + for (final URL u : sigUrls) { + checker.parseSignaturesFile(u); + } final String sig = (signatures != null) ? signatures.trim() : null; if (sig != null && sig.length() != 0) { checker.parseSignaturesString(sig); @@ -298,11 +387,15 @@ public void info(String msg) { throw new MojoExecutionException("IO problem while reading files with API signatures.", ioe); } catch (ParseException pe) { throw new MojoExecutionException("Parsing signatures failed: " + pe.getMessage(), pe); + } catch (ArtifactResolutionException e) { + throw new MojoExecutionException("Problem while resolving Maven artifact.", e); + } catch (ArtifactNotFoundException e) { + throw new MojoExecutionException("Maven artifact does not exist.", e); } if (checker.hasNoSignatures()) { if (failOnUnresolvableSignatures) { - throw new MojoExecutionException("No API signatures found; use parameters 'signatures', 'bundledSignatures', and/or 'signaturesFiles' to define those!"); + throw new MojoExecutionException("No API signatures found; use parameters 'signatures', 'bundledSignatures', 'signaturesFiles', and/or 'signaturesArtifacts' to define those!"); } else { log.info("Skipping execution because no API signatures are available."); return; diff --git a/src/main/java/de/thetaphi/forbiddenapis/maven/SignaturesArtifact.java b/src/main/java/de/thetaphi/forbiddenapis/maven/SignaturesArtifact.java new file mode 100644 index 00000000..01da5c11 --- /dev/null +++ b/src/main/java/de/thetaphi/forbiddenapis/maven/SignaturesArtifact.java @@ -0,0 +1,60 @@ +package de.thetaphi.forbiddenapis.maven; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.factory.ArtifactFactory; + +/* + * (C) Copyright Uwe Schindler (Generics Policeman) and others. + * + * 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. + */ + +/** + * Defines coordinates of a Maven artifact that provides signatures files. + * It may be a plain text file ({@link #path} is not given) or alternatively + * refer to a text file inside a JAR file. For that, define the resource path + * using {@link #path}. + * @since 2.0 + */ +public final class SignaturesArtifact { + + /** Artifact's group ID (required) */ + public String groupId; + + /** Artifact's ID (required) */ + public String artifactId; + + /** Version (required) */ + public String version; + + /** Classifier (optional) */ + public String classifier; + + /** + * Type (required; {@code txt} or {@code jar}). + * If the artifact refers to a JAR file, the {@link #path} should be + * given, that identifies the signatures file inside the JAR. + * */ + public String type; + + /** Path to resource inside JAR artifacts. If given, the {@link #type} must be {@code "jar"} or {@code "zip"}. */ + public String path; + + /** Used by the mojo to fetch the artifact */ + Artifact createArtifact(ArtifactFactory artifactFactory) { + if (groupId == null || artifactId == null || version == null || type == null) { + throw new NullPointerException("signaturesArtifact is missing some properties. Required are: groupId, artifactId, version, type"); + } + return artifactFactory.createArtifactWithClassifier(groupId, artifactId, version, type, classifier); + } +} diff --git a/src/test/antunit/TestMavenMojo.xml b/src/test/antunit/TestMavenMojo.xml index 4c0edeb0..821eaa9f 100644 --- a/src/test/antunit/TestMavenMojo.xml +++ b/src/test/antunit/TestMavenMojo.xml @@ -109,4 +109,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/antunit/pom-sigArtifacts.xml b/src/test/antunit/pom-sigArtifacts.xml new file mode 100644 index 00000000..d5a60df1 --- /dev/null +++ b/src/test/antunit/pom-sigArtifacts.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + de.thetaphi + forbiddenapi-test3 + 0.0-SNAPSHOT + Dummy Project to test signatures from Maven artifacts + + + + + ${groupId} + ${artifactId} + ${version} + + . + false + false + + + ${groupId} + ${artifactId} + ${version} + jar + de/thetaphi/forbiddenapis/signatures/jdk-deprecated-${jdk.version}.txt + + + + + + + check + + + + + + + +