001package test;
002
003import com.killcoding.log.Logger;
004import com.killcoding.log.LoggerFactory;
005
006import java.io.File;
007import java.io.IOException;
008import java.io.InputStream;
009import java.nio.charset.StandardCharsets;
010import java.nio.file.Files;
011import java.security.MessageDigest;
012import java.security.NoSuchAlgorithmException;
013import java.util.Map;
014import java.util.concurrent.ConcurrentHashMap;
015
016/**
017 * Includes methods to generate the MD5 and SHA1 checksum.
018 *
019 * @author Jeremy Long
020 * @version $Id: $Id
021 */
022public final class Checksum {
023
024    /**
025     * Hex code characters used in getHex.
026     */
027    private static final String HEXES = "0123456789abcdef";
028    /**
029     * Buffer size for calculating checksums.
030     */
031    private static final int BUFFER_SIZE = 1024;
032
033    /**
034     * The logger.
035     */
036    private static final Logger LOGGER = LoggerFactory.getLogger(Checksum.class);
037    /**
038     * MD5 constant.
039     */
040    private static final String MD5 = "MD5";
041    /**
042     * SHA1 constant.
043     */
044    private static final String SHA1 = "SHA-1";
045    /**
046     * SHA256 constant.
047     */
048    private static final String SHA256 = "SHA-256";
049    /**
050     * Cached file checksums for each supported algorithm.
051     */
052    private static final Map<File, FileChecksums> CHECKSUM_CACHE = new ConcurrentHashMap<>();
053
054    /**
055     * Private constructor for a utility class.
056     */
057    private Checksum() {
058        super();
059    }
060
061    /**
062     * <p>
063     * Creates the cryptographic checksum of a given file using the specified
064     * algorithm.</p>
065     *
066     * @param algorithm the algorithm to use to calculate the checksum
067     * @param file the file to calculate the checksum for
068     * @return the checksum
069     * @throws java.io.IOException when the file does not exist
070     * @throws java.security.NoSuchAlgorithmException when an algorithm is
071     * specified that does not exist
072     */
073    public static String getChecksum(String algorithm, File file) throws NoSuchAlgorithmException, IOException {
074        FileChecksums fileChecksums = CHECKSUM_CACHE.get(file);
075        if (fileChecksums == null) {
076            try (InputStream stream = Files.newInputStream(file.toPath())) {
077                final MessageDigest md5Digest = getMessageDigest(MD5);
078                final MessageDigest sha1Digest = getMessageDigest(SHA1);
079                final MessageDigest sha256Digest = getMessageDigest(SHA256);
080                final byte[] buffer = new byte[BUFFER_SIZE];
081                int read = stream.read(buffer, 0, BUFFER_SIZE);
082                while (read > -1) {
083                    // update all checksums together instead of reading the file multiple times
084                    md5Digest.update(buffer, 0, read);
085                    sha1Digest.update(buffer, 0, read);
086                    sha256Digest.update(buffer, 0, read);
087                    read = stream.read(buffer, 0, BUFFER_SIZE);
088                }
089                fileChecksums = new FileChecksums(
090                        getHex(md5Digest.digest()),
091                        getHex(sha1Digest.digest()),
092                        getHex(sha256Digest.digest())
093                );
094                CHECKSUM_CACHE.put(file, fileChecksums);
095            }
096        }
097        switch (algorithm.toUpperCase()) {
098            case MD5:
099                return fileChecksums.md5;
100            case SHA1:
101                return fileChecksums.sha1;
102            case SHA256:
103                return fileChecksums.sha256;
104            default:
105                throw new NoSuchAlgorithmException(algorithm);
106        }
107    }
108
109    /**
110     * Calculates the MD5 checksum of a specified file.
111     *
112     * @param file the file to generate the MD5 checksum
113     * @return the hex representation of the MD5 hash
114     * @throws java.io.IOException when the file passed in does not exist
115     * @throws java.security.NoSuchAlgorithmException when the MD5 algorithm is
116     * not available
117     */
118    public static String getMD5Checksum(File file) throws IOException, NoSuchAlgorithmException {
119        return getChecksum(MD5, file);
120    }
121
122    /**
123     * Calculates the SHA1 checksum of a specified file.
124     *
125     * @param file the file to generate the MD5 checksum
126     * @return the hex representation of the SHA1 hash
127     * @throws java.io.IOException when the file passed in does not exist
128     * @throws java.security.NoSuchAlgorithmException when the SHA1 algorithm is
129     * not available
130     */
131    public static String getSHA1Checksum(File file) throws IOException, NoSuchAlgorithmException {
132        return getChecksum(SHA1, file);
133    }
134
135    /**
136     * Calculates the SH256 checksum of a specified file.
137     *
138     * @param file the file to generate the MD5 checksum
139     * @return the hex representation of the SHA1 hash
140     * @throws java.io.IOException when the file passed in does not exist
141     * @throws java.security.NoSuchAlgorithmException when the SHA1 algorithm is
142     * not available
143     */
144    public static String getSHA256Checksum(File file) throws IOException, NoSuchAlgorithmException {
145        return getChecksum(SHA256, file);
146    }
147
148    /**
149     * Calculates the MD5 checksum of a specified bytes.
150     *
151     * @param algorithm the algorithm to use (md5, sha1, etc.) to calculate the
152     * message digest
153     * @param bytes the bytes to generate the MD5 checksum
154     * @return the hex representation of the MD5 hash
155     */
156    public static String getChecksum(String algorithm, byte[] bytes) {
157        return getHex(getMessageDigest(algorithm).digest(bytes));
158    }
159
160    /**
161     * Calculates the MD5 checksum of the specified text.
162     *
163     * @param text the text to generate the MD5 checksum
164     * @return the hex representation of the MD5
165     */
166    public static String getMD5Checksum(String text) {
167        return getChecksum(MD5, stringToBytes(text));
168    }
169
170    /**
171     * Calculates the SHA1 checksum of the specified text.
172     *
173     * @param text the text to generate the SHA1 checksum
174     * @return the hex representation of the SHA1
175     */
176    public static String getSHA1Checksum(String text) {
177        return getChecksum(SHA1, stringToBytes(text));
178    }
179
180    /**
181     * Calculates the SHA256 checksum of the specified text.
182     *
183     * @param text the text to generate the SHA1 checksum
184     * @return the hex representation of the SHA1
185     */
186    public static String getSHA256Checksum(String text) {
187        return getChecksum(SHA256, stringToBytes(text));
188    }
189
190    /**
191     * Converts the given text into bytes.
192     *
193     * @param text the text to convert
194     * @return the bytes
195     */
196    private static byte[] stringToBytes(String text) {
197        return text.getBytes(StandardCharsets.UTF_8);
198    }
199
200    /**
201     * <p>
202     * Converts a byte array into a hex string.</p>
203     *
204     * <p>
205     * This method was copied from <a
206     * href="http://www.rgagnon.com/javadetails/java-0596.html">http://www.rgagnon.com/javadetails/java-0596.html</a></p>
207     *
208     * @param raw a byte array
209     * @return the hex representation of the byte array
210     */
211    public static String getHex(byte[] raw) {
212        if (raw == null) {
213            return null;
214        }
215        final StringBuilder hex = new StringBuilder(2 * raw.length);
216        for (final byte b : raw) {
217            hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt(b & 0x0F));
218        }
219        return hex.toString();
220    }
221
222    /**
223     * Returns the message digest.
224     *
225     * @param algorithm the algorithm for the message digest
226     * @return the message digest
227     */
228    private static MessageDigest getMessageDigest(String algorithm) {
229        try {
230            return MessageDigest.getInstance(algorithm);
231        } catch (NoSuchAlgorithmException e) {
232            LOGGER.error(e.getMessage(), e);
233            final String msg = String.format("Failed to obtain the %s message digest.", algorithm);
234            throw new IllegalStateException(msg, e);
235        }
236    }
237
238    /**
239     * File checksums for each supported algorithm
240     */
241    private static class FileChecksums {
242
243        /**
244         * MD5.
245         */
246        private final String md5;
247        /**
248         * SHA1.
249         */
250        private final String sha1;
251        /**
252         * SHA256.
253         */
254        private final String sha256;
255
256        FileChecksums(String md5, String sha1, String sha256) {
257            this.md5 = md5;
258            this.sha1 = sha1;
259            this.sha256 = sha256;
260        }
261    }
262}