/** * Copyright (c) 2014 Takari, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package io.takari.maven.plugins.compile.jdt; import static org.eclipse.jdt.internal.compiler.util.SuffixConstants.SUFFIX_STRING_class; import static org.eclipse.jdt.internal.compiler.util.SuffixConstants.SUFFIX_STRING_java; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.inject.Inject; import javax.inject.Named; import org.apache.maven.execution.MavenSession; import org.apache.maven.execution.scope.MojoExecutionScoped; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.DirectoryScanner; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader; import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Stopwatch; import com.google.common.hash.Funnels; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.common.io.Files; @Named @MojoExecutionScoped public class ClasspathDigester { private final Logger log = LoggerFactory.getLogger(getClass()); private static final Map<File, Map<String, byte[]>> CACHE = new ConcurrentHashMap<File, Map<String, byte[]>>(); private final ClassfileDigester digester; @Inject public ClasspathDigester(MavenProject project, MavenSession session, ClassfileDigester digester) { this.digester = digester; // this is only needed for unit tests, but won't hurt in general CACHE.remove(new File(project.getBuild().getOutputDirectory())); CACHE.remove(new File(project.getBuild().getTestOutputDirectory())); } public HashMap<String, byte[]> digestDependencies(List<File> dependencies) throws IOException { Stopwatch stopwatch = Stopwatch.createStarted(); HashMap<String, byte[]> digest = new HashMap<String, byte[]>(); // scan dependencies backwards to properly deal with duplicate type definitions for (int i = dependencies.size() - 1; i >= 0; i--) { File file = dependencies.get(i); if (file.isFile()) { digest.putAll(digestJar(file)); } else if (file.isDirectory()) { digest.putAll(digestDirectory(file)); } else { // happens with reactor dependencies with empty source folders continue; } } log.debug("Analyzed {} classpath dependencies ({} ms)", dependencies.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); return digest; } private Map<String, byte[]> digestJar(final File file) throws IOException { Map<String, byte[]> digest = CACHE.get(file); if (digest == null) { digest = new HashMap<String, byte[]>(); Map<String, byte[]> sourcesDigest = new HashMap<String, byte[]>(); JarFile jar = new JarFile(file); try { for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String path = entry.getName(); if (path.endsWith(SUFFIX_STRING_class)) { String type = toJavaType(path, SUFFIX_STRING_class); try { digest.put(type, digester.digest(ClassFileReader.read(jar, path))); } catch (ClassFormatException e) { // as far as jdt is concerned, the type does not exist } } else if (path.endsWith(SUFFIX_STRING_java)) { String type = toJavaType(path, SUFFIX_STRING_java); Hasher hasher = Hashing.sha1().newHasher(); try (InputStream in = jar.getInputStream(entry)) { ByteStreams.copy(in, Funnels.asOutputStream(hasher)); } sourcesDigest.put(type, hasher.hash().asBytes()); } } } finally { jar.close(); } mergeAll(digest, sourcesDigest); CACHE.put(file, digest); } return digest; } private Map<String, byte[]> digestDirectory(final File directory) throws IOException { Map<String, byte[]> digest = CACHE.get(directory); if (digest == null) { digest = new HashMap<String, byte[]>(); Map<String, byte[]> sourcesDigest = new HashMap<String, byte[]>(); DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(directory); scanner.setIncludes(new String[] {"**/*" + SUFFIX_STRING_class, "**/*" + SUFFIX_STRING_java}); scanner.scan(); for (String path : scanner.getIncludedFiles()) { if (path.endsWith(SUFFIX_STRING_class)) { String type = toJavaType(path, SUFFIX_STRING_class); try { digest.put(type, digester.digest(ClassFileReader.read(new File(directory, path)))); } catch (ClassFormatException e) { // as far as jdt is concerned, the type does not exist } } else { String type = toJavaType(path, SUFFIX_STRING_java); sourcesDigest.put(type, Files.hash(new File(directory, path), Hashing.sha1()).asBytes()); } } mergeAll(digest, sourcesDigest); CACHE.put(directory, digest); } return digest; } private void mergeAll(Map<String, byte[]> target, Map<String, byte[]> source) { for (Map.Entry<String, byte[]> entry : source.entrySet()) { byte[] value = target.get(entry.getKey()); if (value != null) { byte[] temp = new byte[value.length + entry.getValue().length]; System.arraycopy(value, 0, temp, 0, value.length); System.arraycopy(entry.getValue(), 0, temp, value.length, entry.getValue().length); value = temp; } else { value = entry.getValue(); } target.put(entry.getKey(), value); } } public static String toJavaType(String path, String suffix) { path = path.substring(0, path.length() - suffix.length()); return path.replace('/', '.').replace('\\', '.'); } public static void flush() { CACHE.clear(); } }