package de.saumya.mojo.gem; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import org.apache.maven.artifact.Artifact; 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.filter.ArtifactFilter; import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; 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.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.apache.maven.repository.RepositorySystem; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; /** * installs a set of given gems without resolving any transitive dependencies */ @Mojo( name ="jars-lock", defaultPhase = LifecyclePhase.INITIALIZE, requiresDependencyResolution = ResolutionScope.TEST ) public class JarsLockMojo extends AbstractMojo { private static final String JARS_HOME = "JARS_HOME"; /** * reference to maven project for internal use. */ @Parameter( defaultValue = "${project}", readonly = true ) protected MavenProject project; /** * Jars.lock file to be updated or created. */ @Parameter( defaultValue = "Jars.lock", property = "jars.lock" ) public File jarsLock; /** * where to copy the jars - default to JARS_HOME environment if set. */ @Parameter( property = "jars.home" ) public File jarsHome; /** * force update of Jars.lock file. */ @Parameter( defaultValue = "false", property = "jars.force" ) public boolean force; /** * update of Jars.lock file for a given artifactId */ @Parameter( property = "jars.update" ) public String update; /** * list of gems. one line one gem: {gemname}:{version}:{scope} or * {gemname}:{version} where scope defaults to compile. */ @Parameter public List<String> gems = Collections.emptyList(); /** * log output file. * @parameter expression="${jars.outputFile}" */ @Parameter( property = "jars.outputFile" ) File outputFile; @Component protected RepositorySystem repositorySystem; /** * local repository for internal use. */ @Parameter( defaultValue = "${localRepository}", readonly = true ) protected ArtifactRepository localRepository; public void execute() throws MojoExecutionException, MojoFailureException { if (update != null) { updateArtifact(); } else { processJarsLock(); } } void processJarsLock() throws MojoExecutionException { List<String> lines = toLines(getArtifacts()); try { switch (checkForUpdates(lines)) { case NEEDS_FORCED_UPDATE: getLog().info(message(jarsLock() + " has outdated dependencies")); // resolve all artifacts from Jars.lock resolve(true); break; case CAN_UPDATE: // means Jars.lock misses some dependencies which can be safely // updated updateJarsLock(lines); break; case UP_TO_DATE: getLog().info(message(jarsLock() + " is up to date")); // ensure jars are vendored vendorJars(); default: } } catch (IOException e) { throw exception("can not read " + jarsLock, e); } } private List<Artifact> getArtifacts() { List<Artifact> artifacts = project.getRuntimeArtifacts(); artifacts.addAll(project.getSystemArtifacts()); for (String gem : gems) { if (!gem.endsWith(":")) gem += ":"; ArtifactResolutionRequest request = new ArtifactResolutionRequest(); // TODO instead of transitive just resolve pom and get the // dependency from it // via MavenProject and then resolve the jar dependencies // transitively. or similar. request.setResolveTransitively(true); request.setCollectionFilter(new ArtifactFilter() { public boolean include(Artifact artifact) { return artifact.getDependencyTrail() == null || artifact.getType().equals("jar"); } }); request.setResolveRoot(true); // type pom is enough here request.setArtifact(createArtifact("rubygems:" + gem, "pom")); request.setLocalRepository(localRepository); request.setRemoteRepositories(project .getRemoteArtifactRepositories()); ArtifactResolutionResult result = repositorySystem.resolve(request); artifacts.addAll(result.getArtifacts()); } return artifacts; } private void updateJarsLock(List<String> lines) throws MojoExecutionException { String action = jarsLock.exists() ? "updated" : "created"; try { writeJarsLock(lines); } catch (IOException e) { throw exception("can not write " + jarsLock(), e); } try { // vendor new jars vendorJars(); } catch (IOException e) { throw exception("can not vendor jars from " + jarsLock(), e); } getLog().info(message(jarsLock() + " " + action)); } private MojoExecutionException exception(String text, IOException e) { return new MojoExecutionException(message(text), e); } private String message(String text) { if (outputFile != null){ try { FileUtils.fileAppend(outputFile.getPath(), text + "\n"); } catch (IOException e) { throw new RuntimeException("error writing text to output-file: " + text, e); } } return text; } private List<String> toLines(Collection<Artifact> artifacts) { List<String> lines = new LinkedList<String>(); for (Artifact a : artifacts) { String line = toLine(a); if (line != null) lines.add(line); } return lines; } private void updateArtifact() throws MojoExecutionException { ArtifactResolutionResult result = resolveUpdate(); if (result == null) { getLog().error(message("no such artifact in " + jarsLock() + ": " + update)); } else if (result.isSuccess()) { for (Artifact a : result.getArtifacts()) { if (a.getArtifactId().equals(update)) { getLog().info(message("updated " + a)); break; } } updateJarsLock(toLines(result.getArtifacts())); } else { for (Exception e : result.getExceptions()) { getLog().error(message(e.getMessage())); } for (Artifact a : result.getMissingArtifacts()) { getLog().error(message("missing artifact: " + a)); } } } private ArtifactResolutionResult resolveUpdate() throws MojoExecutionException { return resolve(false); } private ArtifactResolutionResult resolve(boolean hasUpdate) throws MojoExecutionException { List<String> jars = loadJarsLock(); ArtifactResolutionRequest request = new ArtifactResolutionRequest(); Set<Artifact> artifacts = new HashSet<Artifact>(); for (String jar : jars) { Artifact a = createArtifact(jar, "jar"); if (a != null) { if (a.getArtifactId().equals(update)) { try { a.setVersionRange(VersionRange .createFromVersionSpec("[" + a.getVersion() + ",)")); } catch (InvalidVersionSpecificationException e) { throw new RuntimeException( "something wrong with creating version range", e); } hasUpdate = true; } artifacts.add(a); } } if (!hasUpdate) { return null; } request.setArtifactDependencies(artifacts); request.setResolveTransitively(false); request.setResolveRoot(false); request.setArtifact(project.getArtifact()); request.setLocalRepository(localRepository); request.setRemoteRepositories(project.getRemoteArtifactRepositories()); return repositorySystem.resolve(request); } private Artifact createArtifact(String jar, String type) { if (!jar.endsWith(":") || jar.startsWith("#")) return null; String[] parts = jar.split(":"); if (parts.length == 3) { return repositorySystem.createArtifact(parts[0], parts[1], parts[2], "compile", type); } if (parts.length == 4) { return repositorySystem.createArtifact(parts[0], parts[1], parts[2], parts[3], type); } if (parts.length == 5) { Artifact a = repositorySystem.createArtifactWithClassifier( parts[0], parts[1], parts[3], type, parts[2]); a.setScope(parts[4]); return a; } getLog().warn(message("ignore :" + jar)); return null; } private String jarsLock() { return jarsLock.getAbsolutePath().replace( project.getBasedir().getAbsolutePath() + File.separator, ""); } private void vendorJars() throws IOException { if (jarsHome == null) { if (System.getenv(JARS_HOME) != null) { jarsHome = new File(System.getenv(JARS_HOME)); } } if (jarsHome != null) { jarsHome.mkdirs(); } if (jarsHome == null || !jarsHome.exists() || !jarsHome.isDirectory()) { return; } getLog().info(message("vendor jars:")); for (Artifact a : getArtifacts()) { if (a.getType().equals("jar") && !a.getScope().equals(Artifact.SCOPE_SYSTEM)) { File target = new File(jarsHome, a.getGroupId().replace(".", File.separator) + File.separator + a.getArtifactId() + File.separator + a.getVersion() + File.separator + a.getFile().getName()); if (force || a.getFile().length() != target.length() && !a.getFile().equals(target)) { getLog().info(message("\t- create " + target)); FileUtils.copyFile(a.getFile(), target); } else { getLog().info(message("\t- exists " + target)); } } getLog().info(message("")); } } private void writeJarsLock(List<String> lines) throws FileNotFoundException { PrintWriter out = null; try { out = new PrintWriter(jarsLock); for (String line : lines) { out.println(line); } } finally { IOUtil.close(out); } } private static enum Status { CAN_UPDATE, NEEDS_FORCED_UPDATE, UP_TO_DATE } private Status checkForUpdates(List<String> lines) throws IOException, MojoExecutionException { if (force) { return Status.CAN_UPDATE; } if (jarsLock.exists()) { Set<String> newLines = new TreeSet<String>(lines); Set<String> oldLines = new TreeSet<String>(loadJarsLock()); Set<String> newLinesClone = new TreeSet<String>(newLines); Set<String> oldLinesClone = new TreeSet<String>(oldLines); oldLinesClone.removeAll(newLines); newLinesClone.removeAll(oldLines); Set<String> diffOld = new TreeSet<String>(); for( String dep : oldLinesClone ) { diffOld.add( dep.replaceFirst("^([^:]+:[^:]+):.*", "$1" ) ); } Set<String> diffNew = new TreeSet<String>(); for( String dep : newLinesClone ) { diffNew.add( dep.replaceFirst("^([^:]+:[^:]+):.*", "$1") ); } boolean disjoint = ! new TreeSet<String>(diffOld).removeAll(diffNew); disjoint = disjoint && ! diffNew.removeAll(diffOld); Status result = Status.NEEDS_FORCED_UPDATE; if (newLines.equals(oldLines) ) { result = Status.UP_TO_DATE; } else if (disjoint) { result = Status.CAN_UPDATE; } if (result != Status.UP_TO_DATE && getLog().isInfoEnabled()) { getLog().info("missing : " + newLinesClone); getLog().info("obsolete: " + oldLinesClone); } return result; } return Status.CAN_UPDATE; } @SuppressWarnings("unchecked") private List<String> loadJarsLock() throws MojoExecutionException { try { return FileUtils.loadFile(jarsLock); } catch (IOException e) { throw exception("can not read " + jarsLock, e); } } private String toLine(Artifact a) { if (!a.getType().equals("jar")) return null; StringBuilder line = new StringBuilder(a.toString().replace(":jar:", ":")); line.append(":"); if (a.getScope().equals(Artifact.SCOPE_SYSTEM)) { line.append(getSystemFile(a.getFile().getPath())); } return line.toString(); } private String getSystemFile(String file) { for (Entry<Object, Object> prop : System.getProperties().entrySet()) { String key = prop.getKey().toString(); String value = prop.getValue().toString(); int index = file.indexOf(value); if (index > -1 && new File(value).isDirectory() && !"file.separator".equals(key)) { return file.replace(value, "${" + key + "}"); } } return ""; } }