package photoSpreadUtilities;
/**
* @author paepcke
*
*/
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.image.PixelGrabber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
import javax.swing.ImageIcon;
import photoSpread.PhotoSpread;
import photoSpread.PhotoSpreadException;
import photoSpread.PhotoSpreadException.BadUUIDStringError;
import photoSpread.PhotoSpreadException.CannotLoadImage;
import photoSpreadLoaders.PhotoSpreadFileImporter;
import photoSpreadObjects.PhotoSpreadImage;
import photoSpreadObjects.PhotoSpreadTextFile;
import com.planetj.math.rabinhash.RabinHashFunction64;
/**
* @author paepcke
*
* The UUID class produces (hopefully) globally unique
* identifiers for a number of Java build-in, and PhotoSpread
* specific objects. We use an implementation of the Rabin hash
* function. This method is fast, and offers a probability
* of collision bounded by:
* n*m^2
* -----
* 2^64
* where n is the number of binary strings hashed, and
* m is the length of the longest string.
*
* See the constructors for the hashing strategies used
* for different objects. In particular, image files can
* be hashed using only a sampling of the files, speeding
* up the process.
*/
public class UUID {
// For calls to UUID(File) and UUID(PhotoSpreadImage):
public static enum FileHashMethod {
USE_FILE_SAMPLING,
USE_WHOLE_FILE,
AUTOMATIC
}
private long theUUID = 0;
private RabinHashFunction64 hasher = RabinHashFunction64.DEFAULT_HASH_FUNCTION;
/* Parameters that Determine File Sampling
NOTE: if you change these parameters, then files
will produce *different* UUIDs. Therefore,
previously computed UUIDs for a given file will
be different from the UUIDs computed from the same
file after the parameters were changed.
*/
// Number of bytes to sample at each sampling site within the file:
private final int numBytesToSample = 100;
// Percentages of bytes into the file were samples are to be taken.
// (i.e. the sampling sites where the samplings start):
private final int[] sampleLocPercentages = new int[] { 20, 40, 60 }; // Percentages into the file
private final int MAX_SAMPLE_BYTES = numBytesToSample
* sampleLocPercentages.length;
private final byte[] allSamples = new byte[MAX_SAMPLE_BYTES];
private final double[] sampleLocMultipliers = new double[sampleLocPercentages.length];
private boolean samplingInitialized = false;
/**
* Create a UUID from the stringification of a UUID instance that existed in the past.
* That is, given a UUID A, one can: aStr = A.toString(). In a different session one
* could then UUID A' = createFromUUIDString(aStr) to receive a UUID instance that
* fingerprints the same item as A did.
*
* Note that if the original UUID is still around, you'll have two distinct UUID objects that
* fingerprint the same item. Try not to do that.
*
* @param stringifiedUUID Result of toString() of a UUID instance that existed in the past.
* @return New UUID instance that is equal() to the UUID that produced the parameter.
*
* @throws BadUUIDStringError If the passed-in string is not recognizable as a
* stringified UUID.
*/
public static UUID createFromUUIDString(String stringifiedUUID) throws BadUUIDStringError {
UUID newUUID = new UUID();
try {
newUUID.theUUID = Long.parseLong(stringifiedUUID);
} catch (NumberFormatException e) {
throw new BadUUIDStringError("Attempt to create UUID from '" +
stringifiedUUID +
"', which was not created by a previous UUID instance.");
}
return newUUID;
}
private UUID() {
// Create just a stub in which theUUID is
// not yet initialized. This by itself does
// not make a valid UUID. This constructor is
// only a helper for some of the other constructors.
}
/**
* Given an ImageIcon, which wraps an image create a UUID.
* NOTE: A UUID computed from in-memory pixels is different
* from a UUID computed from the file from which the
* image was loaded.
*
* @param imgIcon
* @throws CannotLoadImage
*/
public UUID(ImageIcon imgIcon) throws CannotLoadImage {
// Make error reporting a as reasonable as we
// can, given that we're not given the file name
// from which the image was/is being loaded:
this(imgIcon, "Filename unknown");
}
/**
* Given an ImageIcon, which usually wraps an image that was
* taken from the file system, create a UUID.
*
* NOTE: A UUID computed from in-memory pixels is different
* from a UUID computed from the file from which the
* image was loaded.
*
* @param imgIcon
* @param fileName This file name is only passed to improve error messages in case of failure.
* @throws CannotLoadImage
*/
public UUID(ImageIcon imgIcon, String fileName) throws CannotLoadImage {
final int imgID = 0;
final int maxImgLoadWaitTime = 1000; // msec
final int zeroX = 0;
final int zeroY = 0;
Image theImage = imgIcon.getImage();
int imgWidth = theImage.getWidth(imgIcon.getImageObserver());
int imgHeight = theImage.getHeight(imgIcon.getImageObserver());
// Make sure the image has loaded:
if ((imgWidth < 0) || (imgHeight < 0)) {
MediaTracker tracker = new MediaTracker(PhotoSpread.getCurrentSheetWindow());
tracker.addImage(theImage, imgID);
try {
if (!tracker.waitForID(imgID, maxImgLoadWaitTime)) {
throw new PhotoSpreadException.CannotLoadImage(
"Timed out while loading image '" +
fileName + "'.");
} else {
// Image loading is done, get the size again:
imgWidth = theImage.getWidth(imgIcon.getImageObserver());
imgHeight = theImage.getHeight(imgIcon.getImageObserver());
}
} catch (InterruptedException e) {
throw new PhotoSpreadException.CannotLoadImage(
"Timed out while loading image '" +
fileName + "'. Loading interrupted by outside forces.");
}
}
int[] pixels = new int[imgWidth * imgHeight];
int scansize = imgWidth;
PixelGrabber grabber = new PixelGrabber(
theImage,
zeroX,
zeroY,
imgWidth,
imgHeight,
pixels,
0, // offset
scansize);
try {
// Get all the pixels into our pixels[] buffer
grabber.grabPixels();
} catch (InterruptedException e) {
throw new PhotoSpreadException.CannotLoadImage("Interrupted waiting for image to load (" + fileName + ").");
}
/* ******
System.out.println("Pixels size: " + pixels.length);
boolean foundNonZeroPixel = false;
for (int i=0; i<pixels.length; i++) {
if (pixels[i] != 0) {
foundNonZeroPixel = true;
break;
}
}
if (foundNonZeroPixel) System.out.println("Found some non-zero pixels");
else System.out.println("No non-zero pixels found");
****** */
theUUID = hasher.hash(pixels);
}
/**
* Create a unique UUID for a given string.
* @param a string
* The Rabin hash method does not distinguish
* well between strings that only differ by
* very few characters. So we use Java's string
* hash method, which is unique per string, and
* computed the same across sessions.
*/
public UUID(String str) {
theUUID = str.hashCode();
}
/**
* Create a unique UUID for a file, given a file object that wraps it.
* We offer two methods for doing the hashing: We can use the contents
* of the entire file to compute the Rabin hash, or we can just use
* a sample, which is computed in a prescribed manner (see class documentation).
* For text files FileHashMethod.USE_WHOLE_FILE is recommended, b/c their content can be
* very similar for two files. For image files, FileHashMethod.USE_FILE_SAMPLING is much
* faster.
*
* The method can be asked to decide on its own which of these two methods to use.
* This choice is requested by setting method to FileHashMethod.AUTOMATIC.
* The decision is based on the file path extension. Any image file extension
* triggers sampling. Other file types are hashed by its full contents.
*
* @param fileObj The file whose content is to be hashed
* @param method is either FileHashMethod.USE_FILE_SAMPLING, USE_WHOLE_FILE, or FileHashMethod.AUTOMATIC
* @throws java.io.FileNotFoundException
* @throws IOException
*/
public UUID(File fileObj, FileHashMethod method) throws java.io.FileNotFoundException, IOException {
if (method == FileHashMethod.USE_WHOLE_FILE)
theUUID = hasher.hash(fileObj);
else if (method == FileHashMethod.USE_FILE_SAMPLING)
theUUID = hasher.hash(getFileSample(fileObj.getAbsolutePath()));
else { // decide based on file extension:
if (PhotoSpreadFileImporter.isImage(fileObj))
theUUID = hasher.hash(getFileSample(fileObj.getAbsolutePath()));
else
theUUID = hasher.hash(fileObj);
}
}
public UUID(PhotoSpreadImage photoSpreadImgObj) throws FileNotFoundException, IOException {
this(new File(photoSpreadImgObj.getFilePath()), FileHashMethod.USE_FILE_SAMPLING);
}
public UUID(PhotoSpreadTextFile photoSpreadTxtFileObj) throws FileNotFoundException, IOException {
this(new File(photoSpreadTxtFileObj.getFilePath()), FileHashMethod.USE_WHOLE_FILE);
}
/*
public UUID(URI uri) {
theUUID = hasher.hash(uri);
}
*/
public UUID (byte[] allBytes) {
theUUID = hasher.hash(allBytes);
}
public UUID(Serializable obj) {
theUUID = hasher.hash(obj);
}
public UUID(Double num) {
this(num.doubleValue());
}
public UUID(double num) {
// Convert the Double into a long first.
// Then get the hash:
theUUID = hasher.hash(Double.doubleToRawLongBits(num));
}
public Boolean equals(UUID obj) {
return theUUID == obj.theUUID;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
* I could use (int)theUUID as a hash code.
* But I'm not sure how unique theUUID is
* after conversion from long to int. So
* I do the more expensive turning the long into
* a string, and returning that string's hash code.
*/
public int hashCode() {
return toString().hashCode();
}
public String toString () {
return Long.toString(theUUID);
}
/**
* Compares this UUID object to another UUID object.
*
* @param otherUUID
* @return The value 0 if this UUID is equal to the argument UUID;
* a value less than 0 if this UUID is numerically less than the argument UUID;
* and a value greater than 0 if this UUID is numerically greater
* than the argument UUID.
*/
public int compareTo(UUID otherUUID) {
int res = 0;
if (theUUID < otherUUID.theUUID)
res = -1;
if (theUUID > otherUUID.theUUID)
res = 1;
return res;
}
/**
* Sample bytes from a file. The resulting byte[] of
* the concatenated samples is then used by the caller
* to compute a UUID. All constants involved
* are defined at the top of this class definition.
*
* NOTE: If this sampling is changed in any way,
* previously computed UUIDs for files will
* not be the same as those computed with the
* modified code.
*
* @param Path of file to sample
* @return A byte[] of the concatenated samples.
* @throws IOException
*/
public byte[] getFileSample(String fileName) throws IOException {
RandomAccessFile randFd = new RandomAccessFile(fileName, "r");
long fileLen = randFd.length();
int samplesRead = 0;
int readRes;
if (!samplingInitialized) {
initSamplingParameters();
samplingInitialized = true;
}
// Go to sampleLocMultipliers.length different locations
// in the file. At each location, collect numBytesToSample
// bytes as one sample:
try {
for (int sampleSeq = 0; sampleSeq < sampleLocMultipliers.length; sampleSeq++) {
randFd.seek(Math.round(fileLen
* sampleLocMultipliers[sampleSeq]));
readRes = randFd
.read(allSamples, samplesRead, numBytesToSample);
if (readRes > -1)
samplesRead += readRes;
else
break;
}
} finally {
randFd.close();
}
int bytesToPad = MAX_SAMPLE_BYTES - samplesRead;
if (bytesToPad <= 0)
return allSamples;
for (int i = samplesRead; i < MAX_SAMPLE_BYTES; i++) {
allSamples[i] = 0;
}
return allSamples;
}
/**
* Pre-compute multipliers for indexing into files
* to sampling sites. Just for speed.
*/
private void initSamplingParameters() {
for (int i = 0; i < sampleLocMultipliers.length; i++)
sampleLocMultipliers[i] = ((double) sampleLocPercentages[i]) / 100.;
}
}
/*
*
*
// Testing:
public static void main(String[] args) {
UUIDTypeDependent tString =
new UUIDTypeDependent("00000000-0000-002a-0000-00000000002a");
tString.doIt();
System.out.println("String printing: " + tString);
UUIDTypeDependent tPhotoSpread =
new UUIDTypeDependent(new PhotoSpreadImage());
System.out.println("String printing: " + tPhotoSpread);
System.out.println("Equals non-image to itself:" +
tString.equals(tString));
System.out.println("Equals image to itself:" +
tPhotoSpread.equals(tPhotoSpread));
System.out.println("Equals image to non-image:" +
tPhotoSpread.equals(tString));
System.out.println("Equals image to different image:" +
tPhotoSpread.equals(new PhotoSpreadImage()));
System.out.println("Equals non-image to different non-image:" +
tString.equals(new UUIDTypeDependent("00000000-0000-002b-0000-00000000002b")));
}
public class PhotoSpreadImage {
}
*/