blob: a618d47c53da56f43333cb456f003fdd79ad4cfc [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.
*/
package com.android.sdklib.build;
import com.android.SdkConstants;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A Class to handle a list of jar files, finding and removing duplicates.
*
* Right now duplicates are based on:
* - same filename
* - same length
* - same content: using sha1 comparison.
*
* The length/sha1 are kept in a cache and only updated if the library is changed.
*/
public class JarListSanitizer {
private static final byte[] sBuffer = new byte[4096];
private static final String CACHE_FILENAME = "jarlist.cache";
private static final Pattern READ_PATTERN = Pattern.compile("^(\\d+) (\\d+) ([0-9a-f]+) (.+)$");
/**
* Simple class holding the data regarding a jar dependency.
*
*/
private static final class JarEntity {
private final File mFile;
private final long mLastModified;
private long mLength;
private String mSha1;
/**
* Creates an entity from cached data.
* @param path the file path
* @param lastModified when it was last modified
* @param length its length
* @param sha1 its sha1
*/
private JarEntity(String path, long lastModified, long length, String sha1) {
mFile = new File(path);
mLastModified = lastModified;
mLength = length;
mSha1 = sha1;
}
/**
* Creates an entity from a {@link File}.
* @param file the file.
*/
private JarEntity(File file) {
mFile = file;
mLastModified = file.lastModified();
mLength = file.length();
}
/**
* Checks whether the {@link File#lastModified()} matches the cached value. If not, length
* is updated and the sha1 is reset (but not recomputed, this is done on demand).
* @return return whether the file was changed.
*/
private boolean checkValidity() {
if (mLastModified != mFile.lastModified()) {
mLength = mFile.length();
mSha1 = null;
return true;
}
return false;
}
private File getFile() {
return mFile;
}
private long getLastModified() {
return mLastModified;
}
private long getLength() {
return mLength;
}
/**
* Returns the file's sha1, computing it if necessary.
* @return the sha1
* @throws Sha1Exception
*/
private String getSha1() throws Sha1Exception {
if (mSha1 == null) {
mSha1 = JarListSanitizer.getSha1(mFile);
}
return mSha1;
}
private boolean hasSha1() {
return mSha1 != null;
}
}
/**
* Exception used to indicate the sanitized list of jar dependency cannot be computed due
* to inconsistency in duplicate jar files.
*/
public static final class DifferentLibException extends Exception {
private static final long serialVersionUID = 1L;
private final String[] mDetails;
public DifferentLibException(String message, String[] details) {
super(message);
mDetails = details;
}
public String[] getDetails() {
return mDetails;
}
}
/**
* Exception to indicate a failure to check a jar file's content.
*/
public static final class Sha1Exception extends Exception {
private static final long serialVersionUID = 1L;
private final File mJarFile;
public Sha1Exception(File jarFile, Throwable cause) {
super(cause);
mJarFile = jarFile;
}
public File getJarFile() {
return mJarFile;
}
}
private final File mOut;
private final PrintStream mOutStream;
/**
* Creates a sanitizer.
* @param out the project output where the cache is to be stored.
*/
public JarListSanitizer(File out) {
mOut = out;
mOutStream = System.out;
}
public JarListSanitizer(File out, PrintStream outStream) {
mOut = out;
mOutStream = outStream;
}
/**
* Sanitize a given list of files
* @param files the list to sanitize
* @return a new list containing no duplicates.
* @throws DifferentLibException
* @throws Sha1Exception
*/
public List<File> sanitize(Collection<File> files) throws DifferentLibException, Sha1Exception {
List<File> results = new ArrayList<File>();
// get the cache list.
Map<String, JarEntity> jarList = getCachedJarList();
boolean updateJarList = false;
// clean it up of removed files.
// use results as a temp storage to store the files to remove as we go through the map.
for (JarEntity entity : jarList.values()) {
if (entity.getFile().exists() == false) {
results.add(entity.getFile());
}
}
// the actual clean up.
if (!results.isEmpty()) {
for (File f : results) {
jarList.remove(f.getAbsolutePath());
}
results.clear();
updateJarList = true;
}
Map<String, List<JarEntity>> nameMap = new HashMap<String, List<JarEntity>>();
// update the current jar list if needed, while building a secondary map based on
// filename only.
for (File file : files) {
String path = file.getAbsolutePath();
JarEntity entity = jarList.get(path);
if (entity == null) {
entity = new JarEntity(file);
jarList.put(path, entity);
updateJarList = true;
} else {
updateJarList |= entity.checkValidity();
}
String filename = file.getName();
List<JarEntity> nameList = nameMap.get(filename);
if (nameList == null) {
nameList = new ArrayList<JarEntity>();
nameMap.put(filename, nameList);
}
nameList.add(entity);
}
try {
// now look for duplicates. Each name list can have more than one file but they must
// have the same size/sha1
for (Entry<String, List<JarEntity>> entry : nameMap.entrySet()) {
List<JarEntity> list = entry.getValue();
checkEntities(entry.getKey(), list);
// if we are here, there's no issue. Add the first of the list to the results.
results.add(list.get(0).getFile());
}
// special case for android-support-v4/13
checkSupportLibs(nameMap, results);
} finally {
if (updateJarList) {
writeJarList(nameMap);
}
}
return results;
}
/**
* Checks whether a given list of duplicates can be replaced by a single one.
* @param filename the filename of the files
* @param list the list of dup files
* @throws DifferentLibException
* @throws Sha1Exception
*/
private void checkEntities(String filename, List<JarEntity> list)
throws DifferentLibException, Sha1Exception {
if (list.size() == 1) {
return;
}
JarEntity baseEntity = list.get(0);
long baseLength = baseEntity.getLength();
String baseSha1 = baseEntity.getSha1();
final int count = list.size();
for (int i = 1; i < count ; i++) {
JarEntity entity = list.get(i);
if (entity.getLength() != baseLength || entity.getSha1().equals(baseSha1) == false) {
throw new DifferentLibException("Jar mismatch! Fix your dependencies",
getEntityDetails(filename, list));
}
}
}
/**
* Checks for present of both support libraries in v4 and v13. If both are detected,
* v4 is removed from <var>results</var>
* @param nameMap the list of jar as a map of (filename, list of files).
* @param results the current list of jar file set to be used. it's already been cleaned of
* duplicates.
*/
private void checkSupportLibs(Map<String, List<JarEntity>> nameMap, List<File> results) {
List<JarEntity> v4 = nameMap.get("android-support-v4.jar");
List<JarEntity> v13 = nameMap.get("android-support-v13.jar");
if (v13 != null && v4 != null) {
mOutStream.println("WARNING: Found both android-support-v4 and android-support-v13 in the dependency list.");
mOutStream.println("Because v13 includes v4, using only v13.");
results.remove(v4.get(0).getFile());
}
}
private Map<String, JarEntity> getCachedJarList() {
Map<String, JarEntity> cache = new HashMap<String, JarListSanitizer.JarEntity>();
File cacheFile = new File(mOut, CACHE_FILENAME);
if (cacheFile.exists() == false) {
return cache;
}
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(cacheFile),
SdkConstants.UTF_8));
String line = null;
while ((line = reader.readLine()) != null) {
// skip comments
if (line.charAt(0) == '#') {
continue;
}
// get the data with a regexp
Matcher m = READ_PATTERN.matcher(line);
if (m.matches()) {
String path = m.group(4);
JarEntity entity = new JarEntity(
path,
Long.parseLong(m.group(1)),
Long.parseLong(m.group(2)),
m.group(3));
cache.put(path, entity);
}
}
} catch (FileNotFoundException e) {
// won't happen, we check up front.
} catch (UnsupportedEncodingException e) {
// shouldn't happen, but if it does, we just won't have a cache.
} catch (IOException e) {
// shouldn't happen, but if it does, we just won't have a cache.
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
return cache;
}
private void writeJarList(Map<String, List<JarEntity>> nameMap) {
File cacheFile = new File(mOut, CACHE_FILENAME);
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(
new FileOutputStream(cacheFile), SdkConstants.UTF_8);
writer.write("# cache for current jar dependency. DO NOT EDIT.\n");
writer.write("# format is <lastModified> <length> <SHA-1> <path>\n");
writer.write("# Encoding is UTF-8\n");
for (List<JarEntity> list : nameMap.values()) {
// clean up the list of files that don't have a sha1.
for (int i = 0 ; i < list.size() ; ) {
JarEntity entity = list.get(i);
if (entity.hasSha1()) {
i++;
} else {
list.remove(i);
}
}
if (list.size() > 1) {
for (JarEntity entity : list) {
writer.write(String.format("%d %d %s %s\n",
entity.getLastModified(),
entity.getLength(),
entity.getSha1(),
entity.getFile().getAbsolutePath()));
}
}
}
} catch (IOException e) {
mOutStream.println("WARNING: unable to write jarlist cache file " +
cacheFile.getAbsolutePath());
} catch (Sha1Exception e) {
// shouldn't happen here since we check that the sha1 is present first, meaning it's
// already been computing.
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
}
}
}
}
private String[] getEntityDetails(String filename, List<JarEntity> list) throws Sha1Exception {
ArrayList<String> result = new ArrayList<String>();
result.add(
String.format("Found %d versions of %s in the dependency list,",
list.size(), filename));
result.add("but not all the versions are identical (check is based on SHA-1 only at this time).");
result.add("All versions of the libraries must be the same at this time.");
result.add("Versions found are:");
for (JarEntity entity : list) {
result.add("Path: " + entity.getFile().getAbsolutePath());
result.add("\tLength: " + entity.getLength());
result.add("\tSHA-1: " + entity.getSha1());
}
return result.toArray(new String[result.size()]);
}
/**
* Computes the sha1 of a file and returns it.
* @param f the file to compute the sha1 for.
* @return the sha1 value
* @throws Sha1Exception if the sha1 value cannot be computed.
*/
private static String getSha1(File f) throws Sha1Exception {
synchronized (sBuffer) {
FileInputStream fis = null;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
fis = new FileInputStream(f);
while (true) {
int length = fis.read(sBuffer);
if (length > 0) {
md.update(sBuffer, 0, length);
} else {
break;
}
}
return byteArray2Hex(md.digest());
} catch (Exception e) {
throw new Sha1Exception(f, e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// ignore
}
}
}
}
}
private static String byteArray2Hex(final byte[] hash) {
Formatter formatter = new Formatter();
try {
for (byte b : hash) {
formatter.format("%02x", b);
}
return formatter.toString();
} finally {
formatter.close();
}
}
}