diff options
Diffstat (limited to 'sdkmanager/libs')
-rw-r--r-- | sdkmanager/libs/sdklib/src/com/android/sdklib/build/JarListSanitizer.java | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/build/JarListSanitizer.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/JarListSanitizer.java new file mode 100644 index 0000000..bd4d9a4 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/JarListSanitizer.java @@ -0,0 +1,442 @@ +/* + * 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 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.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.ArrayList; +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; + + public DifferentLibException(String message) { + super(message); + } + } + + /** + * 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; + + /** + * Creates a sanitizer. + * @param out the project output where the cache is to be stored. + */ + public JarListSanitizer(File out) { + mOut = out; + } + + /** + * 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(List<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 2ndary 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 dups. 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) { + printEntityDetails(filename, list); + throw new DifferentLibException("Jar mismatch! Fix your dependencies"); + } + + } + } + + /** + * 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) { + System.out.println("WARNING: Found both android-support-v4 and android-support-v13 in the dependency list."); + System.out.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), + "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); + try { + OutputStreamWriter writer = new OutputStreamWriter( + new FileOutputStream(cacheFile), "UTF-8"); + + writer.write("# cache for current jar dependecy. 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())); + } + } + } + + writer.close(); + } catch (IOException e) { + System.err.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. + } + } + + private void printEntityDetails(String filename, List<JarEntity> list) throws Sha1Exception { + System.err.println( + String.format("Found %d versions of %s in the dependency list,", + list.size(), filename)); + System.err.println("but not all the versions are identical (check is based on SHA-1 only at this time)."); + System.err.println("All versions of the libraries must be the same at this time."); + System.err.println("Versions found are:"); + for (JarEntity entity : list) { + System.err.println("Path: " + entity.getFile().getAbsolutePath()); + System.err.println("\tLength: " + entity.getLength()); + System.err.println("\tSHA-1: " + entity.getSha1()); + } + } + + /** + * 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) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + FileInputStream 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); + } + } + } + + private static String byteArray2Hex(final byte[] hash) { + Formatter formatter = new Formatter(); + for (byte b : hash) { + formatter.format("%02x", b); + } + return formatter.toString(); + } +} |