001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.io; 020 021import java.io.IOException; 022import java.io.DataInput; 023import java.io.DataOutput; 024import java.io.InputStream; 025import java.util.Arrays; 026import java.security.*; 027 028import org.apache.hadoop.classification.InterfaceAudience; 029import org.apache.hadoop.classification.InterfaceStability; 030 031/** A Writable for MD5 hash values. 032 */ 033@InterfaceAudience.Public 034@InterfaceStability.Stable 035public class MD5Hash implements WritableComparable<MD5Hash> { 036 public static final int MD5_LEN = 16; 037 038 private static final ThreadLocal<MessageDigest> DIGESTER_FACTORY = 039 new ThreadLocal<MessageDigest>() { 040 @Override 041 protected MessageDigest initialValue() { 042 try { 043 return MessageDigest.getInstance("MD5"); 044 } catch (NoSuchAlgorithmException e) { 045 throw new RuntimeException(e); 046 } 047 } 048 }; 049 050 private byte[] digest; 051 052 /** Constructs an MD5Hash. */ 053 public MD5Hash() { 054 this.digest = new byte[MD5_LEN]; 055 } 056 057 /** Constructs an MD5Hash from a hex string. */ 058 public MD5Hash(String hex) { 059 setDigest(hex); 060 } 061 062 /** Constructs an MD5Hash with a specified value. */ 063 public MD5Hash(byte[] digest) { 064 if (digest.length != MD5_LEN) 065 throw new IllegalArgumentException("Wrong length: " + digest.length); 066 this.digest = digest; 067 } 068 069 // javadoc from Writable 070 @Override 071 public void readFields(DataInput in) throws IOException { 072 in.readFully(digest); 073 } 074 075 /** Constructs, reads and returns an instance. */ 076 public static MD5Hash read(DataInput in) throws IOException { 077 MD5Hash result = new MD5Hash(); 078 result.readFields(in); 079 return result; 080 } 081 082 // javadoc from Writable 083 @Override 084 public void write(DataOutput out) throws IOException { 085 out.write(digest); 086 } 087 088 /** Copy the contents of another instance into this instance. */ 089 public void set(MD5Hash that) { 090 System.arraycopy(that.digest, 0, this.digest, 0, MD5_LEN); 091 } 092 093 /** Returns the digest bytes. */ 094 public byte[] getDigest() { return digest; } 095 096 /** Construct a hash value for a byte array. */ 097 public static MD5Hash digest(byte[] data) { 098 return digest(data, 0, data.length); 099 } 100 101 /** 102 * Create a thread local MD5 digester 103 */ 104 public static MessageDigest getDigester() { 105 MessageDigest digester = DIGESTER_FACTORY.get(); 106 digester.reset(); 107 return digester; 108 } 109 110 /** Construct a hash value for the content from the InputStream. */ 111 public static MD5Hash digest(InputStream in) throws IOException { 112 final byte[] buffer = new byte[4*1024]; 113 114 final MessageDigest digester = getDigester(); 115 for(int n; (n = in.read(buffer)) != -1; ) { 116 digester.update(buffer, 0, n); 117 } 118 119 return new MD5Hash(digester.digest()); 120 } 121 122 /** Construct a hash value for a byte array. */ 123 public static MD5Hash digest(byte[] data, int start, int len) { 124 byte[] digest; 125 MessageDigest digester = getDigester(); 126 digester.update(data, start, len); 127 digest = digester.digest(); 128 return new MD5Hash(digest); 129 } 130 131 /** Construct a hash value for a String. */ 132 public static MD5Hash digest(String string) { 133 return digest(UTF8.getBytes(string)); 134 } 135 136 /** Construct a hash value for a String. */ 137 public static MD5Hash digest(UTF8 utf8) { 138 return digest(utf8.getBytes(), 0, utf8.getLength()); 139 } 140 141 /** Construct a half-sized version of this MD5. Fits in a long **/ 142 public long halfDigest() { 143 long value = 0; 144 for (int i = 0; i < 8; i++) 145 value |= ((digest[i] & 0xffL) << (8*(7-i))); 146 return value; 147 } 148 149 /** 150 * Return a 32-bit digest of the MD5. 151 * @return the first 4 bytes of the md5 152 */ 153 public int quarterDigest() { 154 int value = 0; 155 for (int i = 0; i < 4; i++) 156 value |= ((digest[i] & 0xff) << (8*(3-i))); 157 return value; 158 } 159 160 /** Returns true iff <code>o</code> is an MD5Hash whose digest contains the 161 * same values. */ 162 @Override 163 public boolean equals(Object o) { 164 if (!(o instanceof MD5Hash)) 165 return false; 166 MD5Hash other = (MD5Hash)o; 167 return Arrays.equals(this.digest, other.digest); 168 } 169 170 /** Returns a hash code value for this object. 171 * Only uses the first 4 bytes, since md5s are evenly distributed. 172 */ 173 @Override 174 public int hashCode() { 175 return quarterDigest(); 176 } 177 178 179 /** Compares this object with the specified object for order.*/ 180 @Override 181 public int compareTo(MD5Hash that) { 182 return WritableComparator.compareBytes(this.digest, 0, MD5_LEN, 183 that.digest, 0, MD5_LEN); 184 } 185 186 /** A WritableComparator optimized for MD5Hash keys. */ 187 public static class Comparator extends WritableComparator { 188 public Comparator() { 189 super(MD5Hash.class); 190 } 191 192 @Override 193 public int compare(byte[] b1, int s1, int l1, 194 byte[] b2, int s2, int l2) { 195 return compareBytes(b1, s1, MD5_LEN, b2, s2, MD5_LEN); 196 } 197 } 198 199 static { // register this comparator 200 WritableComparator.define(MD5Hash.class, new Comparator()); 201 } 202 203 private static final char[] HEX_DIGITS = 204 {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; 205 206 /** Returns a string representation of this object. */ 207 @Override 208 public String toString() { 209 StringBuilder buf = new StringBuilder(MD5_LEN*2); 210 for (int i = 0; i < MD5_LEN; i++) { 211 int b = digest[i]; 212 buf.append(HEX_DIGITS[(b >> 4) & 0xf]); 213 buf.append(HEX_DIGITS[b & 0xf]); 214 } 215 return buf.toString(); 216 } 217 218 /** Sets the digest value from a hex string. */ 219 public void setDigest(String hex) { 220 if (hex.length() != MD5_LEN*2) 221 throw new IllegalArgumentException("Wrong length: " + hex.length()); 222 byte[] digest = new byte[MD5_LEN]; 223 for (int i = 0; i < MD5_LEN; i++) { 224 int j = i << 1; 225 digest[i] = (byte)(charToNibble(hex.charAt(j)) << 4 | 226 charToNibble(hex.charAt(j+1))); 227 } 228 this.digest = digest; 229 } 230 231 private static final int charToNibble(char c) { 232 if (c >= '0' && c <= '9') { 233 return c - '0'; 234 } else if (c >= 'a' && c <= 'f') { 235 return 0xa + (c - 'a'); 236 } else if (c >= 'A' && c <= 'F') { 237 return 0xA + (c - 'A'); 238 } else { 239 throw new RuntimeException("Not a hex character: " + c); 240 } 241 } 242 243 244}