Java tutorial
/* * The MIT License * * Copyright (c) 2015 CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.jenkinsci.tools.bce; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import japicmp.cmp.JarArchiveComparator; import japicmp.cmp.JarArchiveComparatorOptions; import japicmp.config.Options; import japicmp.model.AccessModifier; import japicmp.model.JApiClass; import japicmp.output.stdout.StdoutOutputGenerator; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.DefaultArtifactHandler; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import javax.annotation.Nullable; import java.io.File; import java.io.StringReader; import java.util.List; import java.util.Set; import static japicmp.cli.JApiCli.ClassPathMode.TWO_SEPARATE_CLASSPATHS; /** * Mojo executing the Binary Compatibility Enforcement. * * @author Andres Rodriguez */ @Mojo(name = "check") public class JenkinsBCEMojo extends AbstractMojo { /** * Update center baseline specification. */ private static final String UPDATE_CENTER = "update:"; /** * Artifact version baseline specification. */ private static final String VERSION = "version:"; /** * Artifact baseline specification. */ private static final String ARTIFACT = "artifact:"; /** * Skip comparison baseline specification. */ private static final String SKIP = "skip"; /** * Current Maven Project. */ @Parameter(defaultValue = "${project}") private MavenProject mavenProject; /** * Project build directory. */ @Parameter(property = "project.build.directory", required = true) private File projectBuildDir; /** * Artifact resolver. */ @Component private ArtifactResolver artifactResolver; @Parameter(defaultValue = "${localRepository}") /** Local Maven Repository. */ private ArtifactRepository localRepository; /** * Project remote repositories. */ @Parameter(defaultValue = "${project.remoteArtifactRepositories}") private List<ArtifactRepository> artifactRepositories; /** * Baseline acquisition method. It can be: * <ul> * <li>{@code update:<i>url</i>}: use update center.</li> * <li>{@code version:<i>version</i>}: use the same artifact of the current project, with another version.</li> * <li>{@code artifact:<i>groupId</i>:<i>artifact</i>:<i>version</i>}: use the specified jar artifact. * </ul> */ @Parameter(defaultValue = "update:https://updates.jenkins-ci.org/update-center.json") private String baseline; /** * Dependency inclusion specification. It can be: * <ul> * <li>{@code none}: don't include dependencies.</li> * <li>{@code all}: include all dependencies.</li> * <li>{@code include:<i>pcoord</i>,<i>pcoord</i>,...}: include only the specified dependencies.</li> * <li>{@code exclude:<i>pcoord</i>,<i>pcoord</i>,...}: include all but the specified dependencies.</li> * <li>{@code artifact:<i>groupId</i>:<i>artifact</i>:<i>version</i>}: use the specified jar artifact. * </ul> * In the last two cases, {@code <i>pcoord</i>} can be {@code artifact:<i>groupId</i>:<i>artifact</i>} * to match every version of the specified artifact or {@code artifact:<i>groupId</i>} to match every * artifact from the specified group. */ @Parameter(defaultValue = "none") private String dependencySpec; private void error(CharSequence s) { getLog().error(s); } private void errorf(String s, Object... args) { error(String.format(s, args)); } private void warn(CharSequence s) { getLog().warn(s); } private void warnf(String s, Object... args) { warn(String.format(s, args)); } private void info(CharSequence s) { getLog().info(s); } private void infof(String s, Object... args) { info(String.format(s, args)); } private MojoFailureException failure(Throwable cause, String format, Object... args) { return new MojoFailureException(String.format(format, args), cause); } private MojoFailureException failure(String format, Object... args) { return new MojoFailureException(String.format(format, args)); } public void execute() throws MojoExecutionException, MojoFailureException { if (skip()) { warn("Skipping execution."); return; } // Check we are in a plugin // We will check core later final String packaging = mavenProject.getPackaging(); if (!"hpi".equals(packaging)) { warn("Not a Jenkins plugin. Skipping"); return; } try { // Get the new package file. // final List<File> newVersion = ImmutableList.of(new File(projectBuildDir, mavenProject.getArtifactId() + ".jar")); final Iterable<File> newVersion = getNewVersionFiles(); // Get the old package file final Iterable<File> oldVersion = getOldVersionFiles(); final Options options = createOptions(oldVersion, newVersion); JarArchiveComparator jarArchiveComparator = new JarArchiveComparator( JarArchiveComparatorOptions.of(options)); List<JApiClass> jApiClasses = jarArchiveComparator.compare(options.getOldArchives(), options.getNewArchives()); // Filter the list final BinaryChanges changes = BinaryChanges.of(jApiClasses); if (!changes.isEmpty()) { // TODO: analyze if custom reporting needed StdoutOutputGenerator stdoutOutputGenerator = new StdoutOutputGenerator(options, Lists.newLinkedList(changes.getChangedClasses())); errorf("Binary Incompatible Changes Detected\n %s", stdoutOutputGenerator.generate()); throw new MojoFailureException("Binary Incompatible Changes Detected"); } else { if (changes.isIgnored()) { warn("You have ignored binary compatibility issues. Please think again"); } if (changes.isAccepted()) { warn("You have accepted binary compatibility issues. Remember to document them in the release notes"); } } } catch (MojoFailureException e) { error(e.getMessage()); throw e; } } /** * @return Whether we should skip execution. */ private boolean skip() { return mavenProject == null || baseline == null || baseline.startsWith(SKIP); } private Iterable<File> getNewVersionFiles() throws MojoFailureException { return new ResolvedArtifact( createArtifact(mavenProject.getGroupId(), mavenProject.getArtifactId(), mavenProject.getVersion())) .getFiles(); } private Iterable<File> getOldVersionFiles() throws MojoFailureException { ResolvedArtifact resolved = getUpdateCenterBaseline(); if (resolved == null) { resolved = getVersionBaseline(); if (resolved == null) { resolved = getArtifactBaseline(); if (resolved == null) { throw failure("Unable to resolve baseline"); } } } return resolved.getFiles(); } private String getBaselinePayload(String prefix) { if (baseline.startsWith(prefix)) { return baseline.substring(prefix.length()); } return null; } /** * Creates a JAR artifact from Maven Coordinates. * * @return The created artifact. Must be resolved. */ private Artifact createArtifact(String groupId, String artifactId, String version) { return new DefaultArtifact(groupId, artifactId, VersionRange.createFromVersion(version), Artifact.SCOPE_COMPILE, "jar", null, new DefaultArtifactHandler("jar")); } /** * Parses a JAR artifact from Maven Coordinates. * * @param coordinates Coordinates to parse. * @return The parsed artifact. Must be resolved. */ private Artifact parseArtifact(String coordinates) throws MojoFailureException { final List<String> coords = Lists.newArrayList(Splitter.on(':').split(coordinates)); if (coords.size() != 3) { throw failure("Invalid coordinates [%s]", coordinates); } return createArtifact(coords.get(0), coords.get(1), coords.get(2)); } private ResolvedArtifact getUpdateCenterBaseline() throws MojoFailureException { final String url = getBaselinePayload(UPDATE_CENTER); if (url == null || url.isEmpty()) { return null; } final JsonElement updates; try { // TODO: look for internal Maven URL downloading methods (using proxies, etc.) final OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); String response = client.newCall(request).execute().body().string(); // TODO: review JSONP parsing response = response.substring(response.indexOf('{')); response = response.substring(0, response.lastIndexOf(')')); updates = new JsonParser().parse(new JsonReader(new StringReader(response))); } catch (Exception e) { throw failure(e, "Unable to get plugin information from update center [%s]", url); } // TODO: error reporting, new plugins, etc. final String coordinates; try { coordinates = updates.getAsJsonObject().get("plugins").getAsJsonObject() .get(mavenProject.getArtifactId()).getAsJsonObject().get("gav").getAsString(); } catch (RuntimeException e) { throw failure(e, "Unable to get plugin coordinates from update center [%s] info", url); } return new ResolvedArtifact(parseArtifact(coordinates)); } private ResolvedArtifact getVersionBaseline() throws MojoFailureException { final String version = getBaselinePayload(VERSION); if (version == null || version.isEmpty()) { return null; } return new ResolvedArtifact( createArtifact(mavenProject.getGroupId(), mavenProject.getArtifactId(), version)); } private ResolvedArtifact getArtifactBaseline() throws MojoFailureException { final String artifact = getBaselinePayload(ARTIFACT); if (artifact == null || artifact.isEmpty()) { return null; } return new ResolvedArtifact(parseArtifact(artifact)); } private Options createOptions(Iterable<File> oldVersion, Iterable<File> newVersion) throws MojoFailureException { final boolean oldOk = checkFiles(oldVersion, "old"); final boolean newOk = checkFiles(newVersion, "new"); if (!oldOk || !newOk) { throw failure("There are unreadable files to compare"); } final Options options = new Options(); Iterables.addAll(options.getOldArchives(), oldVersion); Iterables.addAll(options.getNewArchives(), newVersion); options.setOutputOnlyModifications(true); options.setAccessModifier(AccessModifier.PROTECTED); options.setOutputOnlyBinaryIncompatibleModifications(true); options.setIncludeSynthetic(true); options.setIgnoreMissingClasses(true); options.setClassPathMode(TWO_SEPARATE_CLASSPATHS); infof("Comparing %s with baseline %s for binary compatibility enforcement", options.getNewArchives(), options.getOldArchives()); return options; } private boolean checkFiles(Iterable<File> files, String collectionName) { final List<File> badFiles = Lists.newLinkedList(Iterables.filter(files, new Predicate<File>() { @Override public boolean apply(@Nullable File file) { return file == null || !file.exists() || !file.canRead(); } })); if (!badFiles.isEmpty()) { errorf("Unreadable %s version files: %s", collectionName, badFiles); return false; } return true; } /** * Object representing a resolved artifact and (optionally) its transitively resolved artifacts. * * @author Andres Rodriguez */ final class ResolvedArtifact { /** * Resolved Artifact. */ private final Artifact artifact; /** * Resolved files. */ private final List<File> files; /** * Constructor. */ ResolvedArtifact(Artifact artifact) throws MojoFailureException { final DependencyPolicy dependencyPolicy = DependencyPolicy.of(dependencySpec); final ArtifactResolutionRequest request = new ArtifactResolutionRequest(); request.setArtifact(artifact); request.setLocalRepository(localRepository); request.setRemoteRepositories(artifactRepositories); request.setResolutionFilter(dependencyPolicy); request.setResolveTransitively(dependencyPolicy.isTransitive()); final ArtifactResolutionResult resolutionResult = artifactResolver.resolve(request); if (resolutionResult.hasExceptions()) { List<Exception> exceptions = resolutionResult.getExceptions(); throw failure(exceptions.get(0), "Could not resolve artifact [%s]", artifact); } Set<Artifact> artifacts = resolutionResult.getArtifacts(); if (artifacts.isEmpty()) { throw failure("Could not resolve artifact [%s]", artifact); } this.artifact = resolutionResult.getOriginatingArtifact(); final ImmutableList.Builder<File> b = ImmutableList.builder(); for (Artifact a : artifacts) { if (a.isResolved() && a.getFile() != null) { b.add(a.getFile()); } } this.files = b.build(); } Artifact getArtifact() { return artifact; } List<File> getFiles() { return files; } } }