1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package vogar;
18
19import com.google.common.base.Charsets;
20
21import java.io.File;
22import java.io.FileInputStream;
23import java.security.MessageDigest;
24
25/**
26 * Caches content by MD5.
27 */
28public final class Md5Cache {
29
30    private final Log log;
31    private final String keyPrefix;
32    private final FileCache fileCache;
33
34    /**
35     * Creates a new cache accessor. There's only one directory on disk, so 'keyPrefix' is really
36     * just a convenience for humans inspecting the cache.
37     */
38    public Md5Cache(Log log, String keyPrefix, FileCache fileCache) {
39        this.log = log;
40        this.keyPrefix = keyPrefix;
41        this.fileCache = fileCache;
42    }
43
44    public boolean getFromCache(File output, String key) {
45        if (fileCache.existsInCache(key)) {
46            fileCache.copyFromCache(key, output);
47            return true;
48        }
49        return false;
50    }
51
52    /**
53     * Returns an ASCII hex representation of the MD5 of the content of 'file'.
54     */
55    private static String md5(File file) {
56        byte[] digest = null;
57        try {
58            MessageDigest digester = MessageDigest.getInstance("MD5");
59            byte[] bytes = new byte[8192];
60            FileInputStream in = new FileInputStream(file);
61            try {
62                int byteCount;
63                while ((byteCount = in.read(bytes)) > 0) {
64                    digester.update(bytes, 0, byteCount);
65                }
66                digest = digester.digest();
67            } finally {
68                in.close();
69            }
70        } catch (Exception cause) {
71            throw new RuntimeException("Unable to compute MD5 of \"" + file + "\"", cause);
72        }
73        return (digest == null) ? null : byteArrayToHexString(digest);
74    }
75
76    /**
77     * Returns an ASCII hex representation of the MD5 of 'string'.
78     */
79    private static String md5(String string) {
80        byte[] digest;
81        try {
82            MessageDigest digester = MessageDigest.getInstance("MD5");
83            digester.update(string.getBytes(Charsets.UTF_8));
84            digest = digester.digest();
85        } catch (Exception cause) {
86            throw new RuntimeException("Unable to compute MD5 of \"" + string + "\"", cause);
87        }
88        return (digest == null) ? null : byteArrayToHexString(digest);
89    }
90
91    private static String byteArrayToHexString(byte[] bytes) {
92        StringBuilder result = new StringBuilder();
93        for (byte b : bytes) {
94            result.append(Integer.toHexString((b >> 4) & 0xf));
95            result.append(Integer.toHexString(b & 0xf));
96        }
97        return result.toString();
98    }
99
100    /**
101     * Returns the appropriate key for a dex file corresponding to the contents of 'classpath'.
102     * Returns null if we don't think it's possible to cache the given classpath.
103     */
104    public String makeKey(Classpath classpath) {
105        // Do we have it in cache?
106        String key = keyPrefix;
107        for (File element : classpath.getElements()) {
108            // We only cache dexed .jar/.jack files, not directories.
109            String fileName = element.getName();
110            if (!fileName.endsWith(".jar") && !fileName.endsWith(".jack")) {
111                return null;
112            }
113            key += "-" + md5(element);
114        }
115        return key;
116    }
117
118    /**
119     * Returns a key corresponding to the MD5ed contents of {@code file}.
120     */
121    public String makeKey(File file) {
122        return keyPrefix + "-" + md5(file);
123    }
124
125    /**
126     * Returns a key corresponding to the MD5ed contents of the element.
127     */
128    public String makeKey(String... elements) {
129        StringBuilder sb = new StringBuilder();
130        for (String element : elements) {
131          sb.append(element);
132          sb.append('|');
133        }
134        return keyPrefix + "-" + md5(sb.toString());
135    }
136
137    /**
138     * Copy the file 'content' into the cache with the given 'key'.
139     * This method assumes you're using the appropriate key for the content (and has no way to
140     * check because the key is a function of the inputs that made the content, not the content
141     * itself).
142     * We accept a null so the caller doesn't have to pay attention to whether we think we can
143     * cache the content or not.
144     */
145    public void insert(String key, File content) {
146        if (key == null) {
147            return;
148        }
149        log.verbose("inserting " + key);
150        fileCache.copyToCache(content, key);
151    }
152}
153