| /* |
| * 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.size() > 0) { |
| 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(); |
| } |
| } |
| } |