/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/util/tags/sakai_2-3-1/util-util/util/src/java/org/sakaiproject/util/Blob.java $
 * $Id: Blob.java 6832 2006-03-21 20:43:34Z ggolden@umich.edu $
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
 * 
 * Licensed under the Educational Community License, Version 1.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.opensource.org/licenses/ecl1.php
 * 
 * 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 org.sakaiproject.util;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.NoSuchElementException;


/**
* A blob is a Binary Large OBject.
* The Blob uses a linked list structure internally so it is
* not susceptible to running out of memory when growing.  This can
* be a problem with very large Vectors -- when they grow, a new, larger
* internal array is created and elements copied over.  Thus, for that
* short amount of time, there are two copies of the array.  That, plus
* the added overhead of storing bytes in object form, make the Blob the
* preferrable method for storing raw data.
* <P>Since it uses ints as indicies, a single Blob
* can hold up to Integer.MAX_VALUE (2,147,483,647) bytes.
* Could be modified to use longs and then could hold up to
* Long.MAX_VALUE (9.2233e18) bytes if your computer had that much storage.  :^)
* <P>This is my 'pet' class.
* @author T. Gee
*
*/
public class Blob implements Cloneable, Serializable {

	/**
   * 
   */
  private static final long serialVersionUID = 3832623997476484914L;

  /**
	* The default internal data storage node size.
	*/
	public static final int NODE_SIZE = 512;

	// THE FOLLOWING THREE FIELDS ARE TRANSIENT BECAUSE WE DO THE
	// SERIALIZATION OURSELVES.  WE DO THIS BECAUSE OF JAVA'S
	// LIMITED STACK SIZE FOR SERIALIZATION.

	// The head of the linked list of data nodes.
	protected transient BlobNode head;

	// The current tail of the linked list of data nodes.
	protected transient BlobNode tail;

	// The current size of the Blob.
	// It is possible to compute this, but it is stored
	// for convience.
	protected transient int size;


	// The actual internal data storage node size.
	protected int nodeSize;

	// Current Node -- set when seek() is called
	protected transient BlobNode curr;

	// For the internal enumeration methods
	protected transient BlobNode enumerationNode = null;
	protected transient int enumerationPos = 0;

	/**
	* An inclusive between function (for chars).
	* @param test The char to test.
	* @param low The lower bound.
	* @param high The upper bound.
	* @return true if test is between low and high (inclusive),
	* false otherwise.
	*
	*/
	protected static final boolean between(char test, char low, char high) {
		return ((test >= low) && (test <= high));
	}

	/**
	* An inclusive between function (for ints).
	* @param test The number to test.
	* @param low The lower bound.
	* @param high The upper bound.
	* @return true if test is between low and high (inclusive),
	* false otherwise.
	*
	*/
	public static final boolean between(int test, int low, int high) {
		return ((test >= low) && (test <= high));
	}

	/**
	* Returns a hex representation of a byte.
	* @param b The byte to convert to hex.
	* @return The 2-digit hex value of the supplied byte.
	*
	*/
	public static final String toHex(byte b) {

		char ret[] = new char[2];

		ret[0] = hexDigit((b >>> 4) & (byte)0x0F);
		ret[1] = hexDigit((b >>> 0) & (byte)0x0F);

		return new String(ret);
	}


	/**
	* Returns the hex representation of a short.
	* @param s The short to convert to hex.
	* @return The 4-digit hex value of the supplied short.
	*
	*/
	public static final String toHex(short s) {

		StringBuffer sb = new StringBuffer(5);

		sb.append(toHex((byte)(s >>> 8)));
		sb.append(' ');
		sb.append(toHex((byte)(s >>> 0)));

		return sb.toString();
	}


	/**
	* Returns the hex representation of an int.
	* @param i The int to convert to hex.
	* @return The 8-digit hex value of the supplied int.
	*
	*/
	public static final String toHex(int i) {

		StringBuffer sb = new StringBuffer(11);

		sb.append(toHex((byte)(i >>> 24)));
		sb.append(' ');
		sb.append(toHex((byte)(i >>> 16)));
		sb.append(' ');
		sb.append(toHex((byte)(i >>> 8)));
		sb.append(' ');
		sb.append(toHex((byte)(i >>> 0)));

		return sb.toString();
	}

	/**
	* Returns the hex representation of an long.
	* @param l The long to convert to hex.
	* @return The 16-digit hex value of the supplied long.
	*
	*/
	public static final String toHex(long l) {

		StringBuffer sb = new StringBuffer(11);

		sb.append(toHex((byte)(l >>> 56)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 48)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 40)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 32)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 24)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 16)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 8)));
		sb.append(' ');
		sb.append(toHex((byte)(l >>> 0)));

		return sb.toString();
	}


	/**
	* Returns the hex representation of the characters of a String.
	* @param s The String to convert to hex.
	* @return A String where each character of the original string
	* is given as a space seperated sequence of hex values.
	*
	*/
	public static final String toHex(String s) {

		StringBuffer sb = new StringBuffer();

		char chars[] = s.toCharArray();
		for (int x = 0 ; x < chars.length ; x++) {
			sb.append(toHex((byte)chars[x]));
			if (x != (chars.length - 1)) {
				sb.append(' ');
			}
		}

		return sb.toString();

	}


	/**
	* Returns the hex digit cooresponding to a number between 0 and 15.
	* @param i The number to get the hex digit for.
	* @return The hex digit cooresponding to that number.
	* @exception java.lang.IllegalArgumentException If supplied digit
	* is not between 0 and 15 inclusive.
	*
	*/
	public static final char hexDigit(int i) {

		switch (i) {
		case 0:
			return '0';
		case 1:
			return '1';
		case 2:
			return '2';
		case 3:
			return '3';
		case 4:
			return '4';
		case 5:
			return '5';
		case 6:
			return '6';
		case 7:
			return '7';
		case 8:
			return '8';
		case 9:
			return '9';
		case 10:
			return 'A';
		case 11:
			return 'B';
		case 12:
			return 'C';
		case 13:
			return 'D';
		case 14:
			return 'E';
		case 15:
			return 'F';
		}

		throw new IllegalArgumentException("Invalid digit:" + i);
	}

	/**
	* Returns a string of a specified number of a specified character.
	* @param n The number of characters to create in the return String.
	* @param c The character to create.
	* @return A String of the requested number of the requested character.
	*
	*/
	public static final String strstr(int n, char c) {

		StringBuffer ret = new StringBuffer(n);

		for (int x = 0 ; x < n ; x++) {
			ret.append(c);
		}

		return ret.toString();
	}

	/**
	* Returns a string of a specified number of spaces.
	* @param n The number of spaces to create.
	* @return A String of the requested number of spaces.
	* @see #strstr
	*
	*/
	public static final String spaces(int n) {
		return strstr(n, ' ');
	}

	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	//
	//       CONSTRUCTORS
	//


	/**
	* Creates a new, empty Blob.
	* Uses default internal node size.
	*
	*/
	public Blob() {
		this(NODE_SIZE);
	}

	/**
	* Creates a new, empty Blob and specifies the default internal
	* node size.
	* @param growSize The number of bytes to allocate for a new node
	* in the internal data storage structure.
	*
	*/
	public Blob(int nodeSize) {
		this.nodeSize = nodeSize;

		// Create a new, empty head node
		head = new BlobNode(nodeSize);
		tail = head;

		size = 0;
	}

	/**
	* Creates a new blob that is a copy of an existing blob.
	* @param b The Blob to copy.
	*
	*/
	public Blob(Blob b) {
		this();

		append(b);
	}

	/**
	* Creates a new blob and initialized it with a byte array.
	* @param arr A byte array to copy entirely into the new Blob.
	*
	*/
	public Blob(byte arr[]) {
		this(arr, 0, arr.length);
	}

	/**
	* Creates a new blob and initialized it with a byte array.
	* @param arr A byte array to copy into the new Blob.
	* @param startPos The location to start extracting bytes from.
	* @param len The length of the array to copy in.
	*
	*/
	public Blob(byte arr[], int startPos, int len) {
		this();

		append(arr, startPos, len);
	}



	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	//
	//       EXTERNAL METHODS
	//



	//
	// Append Methods
	//

	/**
	* Appends a byte to the end of the Blob.
	* @param b The byte to add.
	*
	*/
	public synchronized void append(byte b) {

		// Ensure the capacity of the current tail node
		if (tail.freespace() == 0) {
			appendNode(nodeSize);
		}

		// Add the byte to the end node
		tail.data[tail.size] = b;
		tail.size++;

		// incriment our total.
		size++;
	}

	/**
	* Appends one byte (in the form of a char) to the end of the Blob.
	* Strips the high byte off.
	* @param c The character to add.
	*
	*/
	public synchronized void append(char c) {
		append(false, c);
	}

	/**
	* Appends one or two bytes (in the form of a char) to the end
	* of the Blob.
	* Can optionally add the high byte or not.
	* @param addHighByte 'true' to add the character's high byte to the
	* Blob; 'false' to strip it.
	* @param c The character to add.
	*
	*/
	public synchronized void append(boolean addHighByte, char c) {

		if (addHighByte) {
			// Byte 1
			append((byte)(c >> 8));
		}

		// Byte 2
		append((byte)(c >> 0));
	}

	/**
	* Appends two bytes (in the form of a short) to the end of the Blob.
	* @param s The short integer to add.
	*
	*/
	public synchronized void append(short s) {

		// Byte 1
		append((byte)(s >> 8));

		// Byte 2
		append((byte)(s >> 0));
	}

	/**
	* Appends four bytes (in the form of an int) to the end of the Blob.
	* @param i The integer to add.
	*
	*/
	public synchronized void append(int i) {

		// Byte 1
		append((byte)(i >> 24));

		// Byte 2
		append((byte)(i >> 16));

		// Byte 3
		append((byte)(i >> 8));

		// Byte 4
		append((byte)(i >> 0));
	}

	/**
	* Appends eight bytes (in the form of a long) to the end of the Blob.
	* @param l The long integer to add.
	*
	*/
	public synchronized void append(long l) {

		// Byte 1
		append((byte)(l >> 56));

		// Byte 2
		append((byte)(l >> 48));

		// Byte 3
		append((byte)(l >> 40));

		// Byte 4
		append((byte)(l >> 32));

		// Byte 5
		append((byte)(l >> 24));

		// Byte 6
		append((byte)(l >> 16));

		// Byte 7
		append((byte)(l >> 8));

		// Byte 8
		append((byte)(l >> 0));
	}

	/**
	* Appends four bytes (in the form of a float) to the end of the Blob.
	* @param f The float to add.
	*
	*/
	public synchronized void append(float f) {

		// Append the integer created from the bytes of this float
		append(Float.floatToIntBits(f));
	}

	/**
	* Appends eight bytes (in the form of a double) to the end of the Blob.
	* @param d The double to add.
	*
	*/
	public synchronized void append(double d) {

		// Append the long created from the bytes of this double
		append(Double.doubleToLongBits(d));
	}

	/**
	* Appends bytes from a String to the end of the Blob.
	* Strips the high bytes from the characters.
	* @param s The String to add.
	*
	*/
	public synchronized void append(String s) {
		append(false, s);
	}

	/**
	* Appends bytes from a String to the end of the Blob.
	* Can optionally add the high bytes from the characters or not.
	* @param addHighByte 'true' to add the characters' high byte to the
	* Blob; 'false' to strip it.
	* @param s The String to add.
	*
	*/
	public synchronized void append(boolean addHighByte, String s) {

		for (int x = 0 ; x < s.length() ; x++) {
			append(addHighByte, s.charAt(x));
		}
	}

	/**
	* Appends an entire byte array to the end of the Blob.
	* @param arr The array to add bytes from.
	*
	*/
	public synchronized void append(byte arr[]) {
		append(arr, 0, arr.length);
	}

	/**
	* Appends a byte array to the end of the Blob.
	* @param arr The array to add bytes from.
	* @param startPos The position to start the byte extraction
	* from the array at.
	* @param len The number of bytes to read from the array.
	*
	*/
	public synchronized void append(byte arr[], int startPos, int len) {

		// If the current tail node has enuff storage for this
		// new addition, use it, otherwise, append a new node.
		if (tail.freespace() < len) {
			BlobNode oldTail = tail;
			appendNode(Math.max(len, nodeSize));

			// If old tail node was empty, we'll eliminate it
			if (oldTail.size == 0) {
				BlobNode bn = findBefore(oldTail);
				if (bn == null) {
					// oldTail == head
					head = tail;
				} else {
					bn.next = tail;
				}
			} // endif

		} // endif

		System.arraycopy(arr, startPos, tail.data, tail.size, len);
		tail.size += len;

		size += len;

	} // end append()


	/**
	* Appends the bytes from a Blob to the end of the Blob.
	* @param b The Blob to draw bytes from.
	*
	*/
	public synchronized void append(Blob b) {

		BlobNode bn;
		boolean setHead = false;

		// We're going to clone the target Blob's nodes and
		// tack them on the end.

		// if our current node is empty, we'll back up one before appending
		if (tail.size == 0) {
			bn = findBefore(tail);
			if (bn == null) {
				// tail == head
				setHead = true;
			} else {
				tail = bn;
				tail.next = null;
			}
		}

		// Now append all of the target's nodes
		bn = b.head;

		if (setHead) {
			// set the head node
			// (the head node now is empty)
			head = (BlobNode)bn.clone();
			tail = head;
			size = head.size;

			// we'll start with the next node down the line
			// (since we just used the first one)
			bn = bn.next;
		}

		while (bn != null) {
			tail.next = (BlobNode)bn.clone();
			tail = tail.next;
			size += tail.size;

			bn = bn.next;
		}

	} // end append()

	//
	// Data insert Methods
	//

	/**
	* Inserts a byte into the blob at the position pos.
	* Everything else is moved back.
	* @param pos The postition to add the byte at (0 -> beginning).
	* @param b The byte to add.
	* @see Blob#insertBytes
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized void insertByte(int pos, byte b) {

		byte arr[] = new byte[1];
		arr[0] = b;
		insertBytes(pos, arr, 0, 1);

	} // end insertByte()

	/**
	* Inserts a byte into the blob at the position pos.
	* Everything else is moved back.
	* @param pos The postition to begin adding the bytes at (0 -> beginning).
	* @param arr The array of bytes to add from.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized void insertBytes(int pos, byte arr[]) {
		this.insertBytes(pos, arr, 0, arr.length);
	}

	/**
	* Inserts a byte into the blob at the position pos.
	* Everything else is moved back.
	* @param pos The postition to begin adding the bytes at (0 -> beginning).
	* @param arr The array of bytes to add from.
	* @param startPos The position to begin byte copy from.
	* @param len The number of bytes to add from the array.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized void insertBytes(int pos, byte arr[],
	                                     int startPos, int len) {

		// are we just appending?
		if (pos == size) {
			append(arr, startPos, len);
			return;
		}

		// Is the position valid?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		} // endif

		// Find the correct node and index into that node
		int currIndex = seek(pos);

		// Do we have enough space in the current node for the
		// new information
		if (curr.freespace() >= len) {
			// Insert the info into the current node

			// Move the old stuff back
			System.arraycopy(curr.data, currIndex,
			                 curr.data, (currIndex + len),
			                 (curr.size - currIndex));

			// Copy in the new stuff
			System.arraycopy(arr, startPos, curr.data, currIndex, len);

			// Set the new size
			this.curr.size += len;

		} else {
			// not enough room in the inn.
			// erect a barn, errr, a new node for it.
			BlobNode newNode = new BlobNode(nodeSize, arr, startPos, len);
			BlobNode before = findBefore(curr);

			// Where do we stick it?
			if (currIndex == 0) {
				// put it before the current node
				if (before == null) {
					// inserting before head
					newNode.next = head;
					head = newNode;
				} else {
					// inserting in middle
					newNode.next = before.next;
					before.next = newNode;
				}
			} else {
				// We'll have to split the current node
				BlobNode a = new BlobNode(Math.max(nodeSize, currIndex));
				BlobNode b = new BlobNode(Math.max(nodeSize,
				                                   (curr.size - currIndex)));

				// Copy the data
				a.size = currIndex;
				System.arraycopy(curr.data, 0, a.data, 0, a.size);
				b.size = curr.size - currIndex;
				System.arraycopy(curr.data, currIndex, b.data, 0, b.size);

				// Set up the links.
				b.next = (before == null) ? head : before.next;
				newNode.next = b;
				a.next = newNode;
				if (before == null) {
					head = a;
				} else {
					before.next = a;
				}
			} // endif

		} // endif

		// adjust size
		size += len;

	} // end insertBytes()


	//
	// Data remove methods
	//

	/**
	* Truncates the Blob to the specified position.
	* The Blob will have the given number of bytes left.
	* @param len The size to truncate the Blob to.
	* @exception java.lang.IndexOutOfBoundsException If len is
	* outside range of Blob.
	*
	*/
	public synchronized void truncate(int len) {

		// Anything to do?
		if (len == size) {
			return;
		}

		// Is the data within bounds?
		if (!between(len, 0, size)) {
			throw new IndexOutOfBoundsException();
		}

		// find the correct node and truncate that node
		// to the required number of bytes to make the
		// whole thing work.
		// We have to do the seek() and truncate() on different
		// lines as seek() sets 'curr'..
		int currIndex = seek(len);
		curr.size = currIndex;

		// Set the new size;
		size = len;

		// set the next pointer of this node to null
		curr.next = null;
		tail = curr;

	} // end truncate()


	/**
	* Removes a byte from the Blob at the requested position.
	* Everything else is moved up.
	* @param pos The position to remove a byte from (0 -> beginning).
	* @see Blob#removeBytes
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized void removeByte(int pos) {
		removeBytes(pos, 1);
	} // end removeByte()

	/**
	* Removes a number of bytes from the Blob at the requested position.
	* Everything else is moved up.
	* @param pos The position to remove the bytes from (0 -> beginning).
	* @param len The number of bytes to remove.
	* @exception java.lang.IndexOutOfBoundsException If pos and len are
	* outside range of Blob.
	*
	*/
	public synchronized void removeBytes(int pos, int len) {

		// Is the data within bounds?
		if (!(between(pos, 0, (size - 1)) &&
            between((pos + len), 0, size))) {

			throw new IndexOutOfBoundsException();
		}

		BlobNode startNode, endNode;
		int startPos, endPos;

		// Get the starting and ending locations
		startPos = seek(pos);
		startNode = curr;

		endPos = seek(pos + len);
		endNode = curr;

		if (startNode == endNode) {
			// Just removing stuff from one node

			// move the stuff up and adjust length
			System.arraycopy(curr.data, endPos,
			                 curr.data, startPos, (curr.size - endPos));
			curr.size -= len;

		} else {
			// Removing stuff across several nodes
			// Create a new node to hold the info
			BlobNode newNode =
			     new BlobNode(Math.max(nodeSize,
			                           (startPos + (endNode.size - endPos))));

			// Move stuff into this new node
			System.arraycopy(startNode.data, 0,
			                 newNode.data, 0, startPos);

			System.arraycopy(endNode.data, endPos,
			                 newNode.data,  startPos, (endNode.size - endPos));

			newNode.size = (startPos + 1) + (endNode.size - endPos);

			// update the pointers
			newNode.next = endNode.next;

			// Find the node PREVIOUS to the startNode
			BlobNode before = findBefore(startNode);
			if (before == null) {
				head = newNode;
			} else {
				before.next = newNode;
			}
		} // endif

		// Set the new size
		size -= len;

	} // end removeBytes()


	//
	// Data Retrieval methods
	//

	/**
	* Gets the bytes of the blob as a byte array.
	* @return An array of the bytes that compose the Blob.
	*
	*/
	public byte[] getBytes() {
		 return getBytes(0, size);
	}

	/**
	* Returns a subset of the bytes of the blob as a byte array.
	* @param start start index to begin draw (included in get).
	* @param len number of bytes to extract.
	* @return An array of the bytes that compose the Blob.
	* @exception java.lang.IndexOutOfBoundsException If start and len are
	* outside range of Blob.
	*
	*/
	public synchronized byte[] getBytes(int start, int len) {

		// Special case
		if ((start == 0) && (len == 0)) {
			return new byte[0];
		}

		// Is the data within bounds?
		if (!(between(start, 0, (size - 1)) &&
            between((start + len), 0, size))) {
			throw new IndexOutOfBoundsException();
		}

		byte ret[] = new byte[len];
		int bytesCopied = 0;
		int copyFromThis = 0;

		int currIndex = seek(start);
		while (bytesCopied < len) {
			copyFromThis = Math.min((curr.size - currIndex),
			                        (len - bytesCopied));

			System.arraycopy(curr.data, currIndex,
			                 ret, bytesCopied, copyFromThis);
			bytesCopied += copyFromThis;

			curr = curr.next;
			currIndex = 0;
		} // endwhile

		return ret;

	} // end getBytes()


	/**
	* Returns the byte at a specific location.
	* @param pos The position to from which to return the byte
	* (0 -> beginning).
	* @return The byte at requested position.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized byte byteAt(int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		}

		// Find the correct node and index into that node
		int currIndex = seek(pos);

		// Get the byte
		return curr.data[currIndex];
	}

	/**
	* Returns a character reconstructed from one byte at a specific location.
	* Sets the high byte in the character to 0x0.
	* @param pos The position to get the byte to reconstuct the character
	* (0 -> beginning).
	* @return The reconstructed character.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* char is outside the Blob.
	*
	*/
	public synchronized char charAt(int pos) {
		return charAt(false, pos);
	} // end charAt()

	/**
	* Returns a character reconstructed from one or two bytes at a
	* specific location.
	* @param useHighByte 'true' to use two bytes (one for the high byte)
	* to reconstruct a character; 'false' to use one byte and set the
	* high byte to 0x0.
	* @param pos The position to get the byte(s) to reconstuct the character
	* (0 -> beginning).
	* @return The reconstructed character.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized char charAt(boolean useHighByte, int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - (useHighByte ? 2 : 1)))) {
			throw new IndexOutOfBoundsException();
		}

		// Return character
		char ret;

		if (useHighByte) {
			// get a short and cast it as a char.
			ret = (char)shortAt(pos);

		} else {
			// get a byte and cast it as a char.
			ret = (char)byteAt(pos);

		} // endif

		return ret;
	} // end charAt()

	/**
	* Returns a short int reconstructed from two bytes at a specific
	* location.
	* @param pos The position to get the bytes to reconstuct the short
	* (0 -> beginning).
	* @return The reconstructed short.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized short shortAt(int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 2))) {
			throw new IndexOutOfBoundsException();
		}

		// Return value
		short ret = 0x0, temp;

		// Find proper location
		beginEnumeration(pos);

		for (int x = 0 ; x < 2 ; x++) {

			// get next byte
			// We shift the input byte up all the way to the high
			// byte and then back down to the req'd position because
			// java won't let us cast an a byte as a larger integer
			// type without propigating the sign bit thruout the
			// higher bytes.  This ain't what we need so we jump
			// thru some hoops to avoid it.
			temp = (short)((nextByte() << (1 * 8)) >>> (1 * 8));
			ret |= (temp << (8 * (1 - x)));

		} // endfor

		return ret;
	} // end shortAt()

	/**
	* Returns an int reconstructed from four bytes at a specific
	* location.
	* @param pos The position to get the bytes to reconstuct the int
	* (0 -> beginning).
	* @return The reconstructed int.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized int intAt(int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 4))) {
			throw new IndexOutOfBoundsException();
		}

		// Return value
		int ret = 0x0, temp;

		// Find proper location
		beginEnumeration(pos);

		for (int x = 0 ; x < 4 ; x++) {

			// get next byte
			// We shift the input byte up all the way to the high
			// byte and then back down to the req'd position because
			// java won't let us cast an a byte as a larger integer
			// type without propigating the sign bit thruout the
			// higher bytes.  This ain't what we need so we jump
			// thru some hoops to avoid it.
			temp = ((int)nextByte() << (3 * 8)) >>> (3 * 8);
			ret |= (temp << (8 * (3 - x)));

		} // endfor

		return ret;
	} // end intAt()


	/**
	* Returns a long int reconstructed from eight bytes at a specific
	* location.
	* @param pos The position to get the bytes to reconstuct the long
	* (0 -> beginning).
	* @return The reconstructed long.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized long longAt(int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 8))) {
			throw new IndexOutOfBoundsException();
		}

		// Return value
		long ret = 0x0, temp;

		// Find proper location
		beginEnumeration(pos);

		for (int x = 0 ; x < 8 ; x++) {

			// get next byte
			// We shift the input byte up all the way to the high
			// byte and then back down to the req'd position because
			// java won't let us cast an a byte as a larger integer
			// type without propigating the sign bit thruout the
			// higher bytes.  This ain't what we need so we jump
			// thru some hoops to avoid it.
			temp = ((long)nextByte() << (7 * 8)) >>> (7 * 8);
			ret |= (temp << (8 * (7 - x)));

		} // endfor

		return ret;
	} // end longAt()


	/**
	* Returns a floating point number reconstructed from four bytes
	* at a specific
	* location.
	* @param pos The position to get the bytes to reconstuct the float
	* (0 -> beginning).
	* @return The reconstructed float.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	protected synchronized float floatAt(int pos) {

		return Float.intBitsToFloat(intAt(pos));

	} // end floatAt()

	/**
	* Returns a double precision floating point number reconstructed
	* from eight bytes at a specific
	* @param pos The position to get the bytes to reconstuct the double
	* (0 -> beginning).
	* @return The reconstructed double.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized double doubleAt(int pos) {

		return Double.longBitsToDouble(longAt(pos));

	} // end floatAt()


	/**
	* Returns a String reconstructed from the bytes of the Blob.
	* Sets the high byte in each character to 0x0.
	* @return The reconstructed String.
	*
	*/
	public synchronized String getString() {

		return getString(false, 0, size);
	} // end getString()

	/**
	* Returns a String reconstructed from bytes at a specific location.
	* Sets the high byte in each character to 0x0.
	* @param pos The position to start retrieving the bytes to reconstuct
	* the String (0 -> beginning).
	* @param len The length of the to-be-returned String.
	* @return The reconstructed String.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized String getString(int pos, int len) {
		return getString(false, pos, len);
	} // end getString()

	/**
	* Returns a String reconstructed from bytes at a specific location.
	* @param useHighByte 'true' to use two bytes (one for the high byte)
	* to reconstruct each character; 'false' to use one byte and set the
	* high byte to 0x0.
	* @param pos The position to start retrieving the bytes to reconstuct
	* the String (0 -> beginning).
	* @param len The length of the to-be-returned String.
	* @return The reconstructed String.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized String getString(boolean useHighByte,
	                                     int pos, int len) {

		// Special case
		if ((pos == 0) && (len == 0)) {
			return new String();
		}

		// Is the data within bounds?
		if (!(between(pos, 0, (size - 1)) &&
		      between((pos + (len * (useHighByte ? 2 : 1))),
				               0, size))) {
			throw new IndexOutOfBoundsException();
		}

		// Return String
		StringBuffer ret = new StringBuffer(len);

		// hold character
		char c;

		// Find proper location
		beginEnumeration(pos);

		for (int x = 0 ; x < len ; x++) {

			if (useHighByte) {
				// Use both bytes

				// high byte
				c = (char)(nextByte() << 8);

				// low byte
				c |= nextByte();

			} else {
				// just get one (low) byte
				c = (char)nextByte();

			} // endif

			// Add the new character
			ret.append(c);

		} // endfor

		return ret.toString();
	} // end getString()


	/**
	* Clones this Blob.
	* The resulting Blob will have the same data as the first.
	* (altho possibly not the same internal configuration).
	*
	*/
	public synchronized Object clone() {
		// We can just call our getBlob method to do this.
		return getBlob(0, size);
	}

	/**
	* Returns a new Blob drawn from bytes at a specific location.
	* @param pos The position to start retrieving the bytes to build
	* the new Blob (0 -> beginning).
	* @param len The number of bytes to put into the new Blob.
	* @return The newly constructed Blob.
	* @exception java.lang.IndexOutOfBoundsException If the requested
	* bytes are outside the Blob.
	*
	*/
	public synchronized Blob getBlob(int pos, int len) {
		// cop out and use our other methods.  Speed be damned -- this
		// isn't that important anyway.
		// We *should* just go thru and clone the appropiate
		// BlobNodes and just modifiy the ones on the end....
		return new Blob(getBytes(pos, len));

	} // end getBlob()

	//
	// Misc Methods
	//

	/**
	* Returns an enumeration of the bytes in this Blob.
	* The objects returned from the calls to nextElement are
	* of type Byte.
	* @param pos The location from which to begin enumerating bytes.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized Enumeration enumerateBytes(int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		}

		final int cep = seek(pos);
		final BlobNode cen = curr;

		return new Enumeration() {

			int currEnumerationPos = cep;
			BlobNode currEnumerationNode = cen;

			public synchronized boolean hasMoreElements() {
				return (currEnumerationNode != null);
			}

			public synchronized Object nextElement() {
				if (currEnumerationNode == null) {
					throw new NoSuchElementException("Past end of current Enumeration");
				}

				byte ret = currEnumerationNode.data[currEnumerationPos++];

				// check to see if we're at the end of the current node
				if (currEnumerationPos == currEnumerationNode.size) {
					// At end, go to next node
					currEnumerationNode = currEnumerationNode.next;
					currEnumerationPos = 0;
				}

				// All done
				return new Byte(ret);
			} // end nextElement()

		}; // end Enumeration
	} // end enumerateBytes

	/**
	* Sets a byte at a particular position.
	* @param pos The position to set the byte at.
	* @param b The value to set that postion to.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized void setByteAt(int pos, byte b) {
		// Is the data within bounds?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		}

		// Find the correct node and index into that node
		int currIndex = seek(pos);

		// Set the byte
		curr.data[currIndex] = b;
	}


	/**
	* Gets the number of bytes in the Blob.
	* @return The number of bytes in the Blob.
	*
	*/
	public synchronized int length() {
		return size;
	}

	/**
	* Returns true if the blob is equal to the given object.
	* True only if supplied object is a Blob and the two contain
	* exactly the same data.
	*
	*/
	public synchronized boolean equals(Object o) {
		if ((o == null) || !(o instanceof Blob)) {
			return false;
		}

		Blob b = (Blob)o;

		// Are they the same length?
		if (size != b.size) {
			return false;
		}

		// Compare the data
		beginEnumeration(0);
		b.beginEnumeration(0);
		while (hasMoreBytes()) {
			if (nextByte() != b.nextByte()) {
				return false;
			}
		}

		// If we got here, they must be equal
		return true;

	} // end equals()


	/**
	* Searches for a byte and returns an index to the first one found.
	* @param b The byte to search for.
	* @return The index of the first match or -1 if not found.
	*
	*/
	public synchronized int indexOf(byte b) {
		return indexOf(b, 0);
	}

	/**
	* Searches for a byte and returns an index to the first one found.
	* Search starts at given index and includes that index.
	* @param b The byte to search for.
	* @param pos The position to begin searching at (0 -> beginning).
	* @return The index of the first match or -1 if not found.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	public synchronized int indexOf(byte b, int pos) {

		// Is the data within bounds?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		}

		beginEnumeration(pos);
		int currentPos = pos;

		while (hasMoreBytes()) {

			if (nextByte() == b) {
					return currentPos;
			}

			currentPos++;
		} // endwhile

		return -1;
	}

	//
	// IO Methods
	//

	/**
	* Reads bytes from an InputStream into the Blob.
	* @param len The number of bytes to attempt to read.
	* @param in The InputStream to read from.
	* @return The number of bytes read and appended to the blob
	* or -1 if the end of the stream was reached.
	* @exception java.io.IOException If there is a problem reading.
	*
	*/
	public synchronized int read(int len, InputStream in)
	                              throws IOException {

		byte b[] = new byte[len];
		int bytesRead;
		try {
			bytesRead = in.read(b);
		} catch (EOFException e) {
			// We'll just return a -1 just as if
			// we had gotten a -1 in response from the read
			return -1;

		} catch (IOException e) {
			// Throw this one to the caller
			throw (IOException)e.fillInStackTrace();
		}
		append(b, 0, bytesRead);
		return bytesRead;

	} // end read();

	/**
	* Reads all bytes from an InputStream into the Blob.
	* This will read an InputStream byte-by-byte until EOF and
	* append the data to the end of the Blob.
	* @param in The InputStream to read from.
	* @return The number of bytes read and appended to the blob
	* or -1 if the end of the stream was reached immediately.
	* @exception java.io.IOException If there is a problem reading.
	*
	*/
	public synchronized int read(InputStream in) throws IOException {

		byte b[] = new byte[nodeSize];
		int bytesRead, totalBytesRead = -1;
		while (true) {
			try {
				bytesRead = in.read(b);
			} catch (EOFException e) {
				// We'll just break out of the loop just as if
				// we had gotten a -1 in response from the read
				break;

			} catch (IOException e) {
				// Throw this one to the caller
				throw (IOException)e.fillInStackTrace();
			}

			// if we're at EOF, get out of the loop.
			if (bytesRead == -1) {
				break;
			}

			// We have totalBytesRead set to -1 initially
			// so that we'll return -1 if the stream is at EOF
			// right off the bat.
			// Now, we have to set it to 0 so that our byte sum
			// will be correct.
			if (totalBytesRead == -1) {
				totalBytesRead = 0;
			}

			append(b, 0, bytesRead);
			totalBytesRead += bytesRead;
		}
		return totalBytesRead;

	} // end read();


	/**
	* Reads most bytes from an InputStream into the Blob -
	* not to exceed <max> bytes by much (it may read up to nodeSize more).
	* This will read an InputStream byte-by-byte until EOF and
	* append the data to the end of the Blob.
	* @param in The InputStream to read from.
	* @param max the max (sort of) bytes to read
	* @return The number of bytes read and appended to the blob
	* or -1 if the end of the stream was reached immediately.
	* or -2 if we cut off due to max before end
	* @exception java.io.IOException If there is a problem reading.
	*
	*/
	public synchronized int readLimiting(InputStream in, long max)
		throws IOException
	{
		byte b[] = new byte[nodeSize];
		int bytesRead, totalBytesRead = -1;
		while (true) {
			try {
				bytesRead = in.read(b);
			} catch (EOFException e) {
				// We'll just break out of the loop just as if
				// we had gotten a -1 in response from the read
				break;

			} catch (IOException e) {
				// Throw this one to the caller
				throw (IOException)e.fillInStackTrace();
			}

			// if we're at EOF, get out of the loop.
			if (bytesRead == -1) {
				break;
			}

			// We have totalBytesRead set to -1 initially
			// so that we'll return -1 if the stream is at EOF
			// right off the bat.
			// Now, we have to set it to 0 so that our byte sum
			// will be correct.
			if (totalBytesRead == -1) {
				totalBytesRead = 0;
			}
			
			// if we're already over the limit, get out!
			if ((totalBytesRead > max) && (bytesRead > 0))
			{
				totalBytesRead = -2;
				break;
			}

			append(b, 0, bytesRead);
			totalBytesRead += bytesRead;
		}
		return totalBytesRead;

	}	// readLimiting


	/**
	* provide an output stream that writes to the END of the blob (appends)
	*/
	public OutputStream outputStream()
	{
		return new OutputStream ()
		{
			public void write(int b)
			{
				append((byte)b);
			}
			
			public void write(byte[] b)
			{
				append(b);
			}
			
			public void write(byte[] b, int off, int len)
			{
				append(b, off, len);
			}
		};

	}	// outputStream

	/**
	* provide an input stream that reads the blob contents
	*/
	public InputStream inputStream()
	{
		return new InputStream ()
		{
			/** next byte to return */
			private int m_pos = 0;
			
			/*public int read(byte b[], int off, int len)
				throws IOException
			{
				return super.read(b, off, len);
			}*/

			public int read()
				throws IOException
			{
				int rv = -1;
				try
				{
					rv = byteAt(m_pos);
					
					// input streams must return values 0..255, but our bytes are signed
					if (rv < 0) rv = 256 + rv;

					m_pos++;
				}
				catch (IndexOutOfBoundsException e) {}
				
				return rv;
			}

			public int available()
			{
				return size-m_pos;
			}
		};

	}	// inputStream


	/**
	* Writes the entire contents of the Blob to an OutputStream.
	* @param out The OutputStream to write to.
	* @exception java.io.IOException If there is a problem writing.
	*
	*/
	public synchronized void write(OutputStream out) throws IOException {
		write(0, size, out);
	}

	/**
	* Writes the contents of a part of the Blob to an OutputStream.
	* @param pos The position to begin writing from.
	* @param len The number of bytes to write.
	* @param out The OutputStream to write to.
	* @exception java.lang.IndexOutOfBoundsException If pos and len are
	* outside range of Blob.
	* @exception java.io.IOException If there is a problem writing.
	*
	*/
	public synchronized void write(int pos, int len,
	                               OutputStream out) throws IOException {

		// Is the data within bounds?
		if (!(between(pos, 0, (size - 1)) &&
		      between((pos + len), 0, size))) {
			throw new IndexOutOfBoundsException();
		}

		// Go to the beginning of the data
		int bytesWritten = 0;
		int writeFromThis = 0;

		int currIndex = seek(pos);
		while (bytesWritten < len) {
			writeFromThis = Math.min((curr.size - currIndex),
			                         (len - bytesWritten));

			// Write out the stuff,
			out.write(curr.data, currIndex, writeFromThis);
			bytesWritten += writeFromThis;

			curr = curr.next;
			currIndex = 0;
		} // endwhile

	} // end write();


	/**
	* Generates and returns an 8-byte checksum.
	*
	*/
	public synchronized long checksum() {

		long ret = 0, temp = 0, hold = 0;
		int index = 0;

		if (size == 0) {
			return ret;
		}

		beginEnumeration(0);
		while (hasMoreBytes()) {

			// put the byte into the low end byte of a long
			temp = ((long)nextByte() << (7 * 8)) >>> (7 * 8);

			// put the byte into a holder long
			hold |= (temp << (8 * (7 - index)));

			if (++index == 8) {
				// XOR the last eight bytes with the checksum value
				ret ^= hold;

				// reset the holder long
				hold = 0;
				index = 0;
			}
		} // endwhile

		// Get any remaining bytes in hold
		if (index != 0) {
			ret ^=hold;
		}

		return ret;
	}


	/**
	* Returns a string representation of the Blob.
	* Includes length and checksum.
	* @see #printContents
	*
	*/
	public synchronized String toString() {
		return new String("Blob[length=" + size +
		                  ";checksum=" + toHex(checksum()) + "]");
	}


	/**
	* Prints the contents of this Blob.
	* Formats the data neatly and prints it to System.out.
	* @see #toString
	*
	*/
	public synchronized void printContents() {

		int x, bytesPast;
		String holdStr;
		char rep[] = new char[16];
		byte b;
		long cksum = checksum();

		// where to go?
		PrintStream o = System.out;

		// current output width
		int currWidth = 86;

		holdStr = ("Blob: length = " + size +
		           " -- Checksum = " + toHex(cksum) + " ");
		o.print(holdStr);
		o.println(strstr((currWidth - holdStr.length()), '-'));

		// If no data, just print the last line and get outta' here
		if (size == 0) {
			o.println(strstr(currWidth, '-'));
			return;
		}

		o.print("     0 | ");

		bytesPast = 0;
		beginEnumeration(0);
		while (hasMoreBytes()) {

				// print next byte
				b = nextByte();
				o.print(toHex(b));

				// get the representation
				if (between(b, 32, 126)) {
					rep[bytesPast % 16] = (char)b;
				} else {
					rep[bytesPast % 16] = '.';
				}

				// incriment pointer
				bytesPast++;

				if ((bytesPast % 16) == 0) {
					// print out representation
					o.print("   <");
					for (x = 0 ; x < 16 ; x++) {
						o.print(rep[x]);
					}
					o.println(">");

					// Start of new line
					if (hasMoreBytes()) {
						holdStr = Integer.toString(bytesPast);
						o.print(spaces(6 - holdStr.length()));
						o.print(holdStr);
						o.print(" | ");
					}

				} else if ((bytesPast % 4) == 0) {
					// end of 4-byte chunk
					o.print("   ");

				} else {
					o.print(' ');
				} // endif

		} // endwhile

		if ((bytesPast % 16) != 0) {
			// write out some filler spaces
			for (x = (bytesPast % 16) ; x < 16 ; x++) {
				o.print("  ");
				rep[x] = ' ';

				if ((x % 4) == 0) {
					// end of (what would have been a) 4-byte chunk
					o.print("   ");
				} else {
					o.print(' ');
				} // endif
			}

			// print out representation
			o.print("   <");
			for (x = 0 ; x < 16 ; x++) {
				o.print(rep[x]);
			}
			o.println(">");
		}

		// Write last dashed line
		holdStr = ("---------------" +
		           strstr(String.valueOf(size).length(), '-') +
		           "--- Checksum = " + toHex(cksum) + " ");
		o.print(holdStr);
		o.println(strstr((currWidth - holdStr.length()), '-'));
	}

	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	////////////////////////////////////////////////////////////
	//
	//       INTERNAL METHODS
	//


	/**
	* Appends a new node to the end of the internal list.
	* @param newNodeSize Size of the new node to create.
	*
	*/
	protected void appendNode(int newNodeSize) {

		// Create a new node -- put it on the end of the list
		tail.next = new BlobNode(newNodeSize);

		// Set the new tail
		tail = tail.next;
	}

	/**
	* Seeks a position within the internal list.
	* Takes an offset from the beginning and sets the 'curr' pointer
	* to the reqested node and returns the offset within that node
	* to the correct position.
	* @param pos The position within the Blob to seek.
	* @return The offest into the current node where the requested
	* byte can be found.
	* @exception java.lang.IndexOutOfBoundsException If pos is
	* outside range of Blob.
	*
	*/
	protected int seek(int pos) {
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException("Seek past end:" +
			                                    "pos=" + pos + "   " +
			                                    "size=" + size);

		}

		int bytesPast = 0;
		curr = this.head;

		while ((curr.size + bytesPast) <= pos) {

			// Go to next node...
			bytesPast += curr.size;
			curr = curr.next;
		}

		// Return the offset into the current node.
		return (pos - bytesPast);
	}

	/**
	* Finds the node before the given node.
	* This is the one whose next pointer is the given node.
	* @return the node before the supplied node or
	* null if the given node is the head node.
	* @exception java.util.NoSuchElementException if the node is
	* not found in the list.
	*
	*/
	protected BlobNode findBefore(BlobNode target) {

		BlobNode bn;

		if (target == head) {
			return null;
		}

		bn = head;
		while (bn.next != target) {
			if (bn.next == null) {
				throw new NoSuchElementException("Couldn't find BlobNode");
			}
			bn = bn.next;
		}
		return bn;

	} // end findBefore()



	//
	// Internal Enumeration Methods
	//


	/**
	* Sets up an enumeration of the bytes of the Blob
	* starting from a particular point.
	* @param pos The location from which to begin enumerating bytes.
	* @exception java.lang.IndexOutOfBoundsException If pos and len are
	* outside range of Blob.
	*
	*/
	protected synchronized void beginEnumeration(int pos) {

		// Set the current position null
		enumerationNode = null;

		// Is the data within bounds?
		if (!between(pos, 0, (size - 1))) {
			throw new IndexOutOfBoundsException();
		}

		// Find the position
		enumerationPos = seek(pos);
		enumerationNode = curr;

		// We're ready to begin enumerating the bytes
	} // end beginInternalEnumeration()

	/**
	* Returns 'true' if the Blob has more bytes, 'false' if empty.
	*
	*/
	protected synchronized boolean hasMoreBytes() {
		return (enumerationNode != null);
	}

	/**
	* Returns the next byte in the Blob.
	* @exception java.util.NoSuchElementException If there are no more bytes
	* in the current enumeration
	*
	*/
	protected synchronized byte nextByte() {

		if (enumerationNode == null) {
			throw new NoSuchElementException("Past end of current Enumeration");
		}

		byte ret = enumerationNode.data[enumerationPos++];

		// check to see if we're at the end of the current node
		if (enumerationPos == enumerationNode.size) {

			// At end, go to next node
			enumerationNode = enumerationNode.next;
			enumerationPos = 0;
		}

		// All done
		return ret;
	} // end nextByte()


	//
	// Serialization Methods
	//


	/**
	* A specialized object write routine.
	* This is because java gets a stack overflow error when
	* trying to write the linked list.  Damn!
	*
	*/
	protected void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();

		byte a[] = getBytes();
		out.writeObject(a);
	}

	/**
	* A specialized object read routine.
	*
	*/
	protected void readObject(ObjectInputStream in) throws IOException,
	                                                     ClassNotFoundException {
		in.defaultReadObject();

		// Create a new node to contain the one massive chunk of data.
		byte a[] = (byte [])in.readObject();
		head = new BlobNode(nodeSize, a, 0, a.length);

		tail = head;
		size = a.length;
	}

/*
	// to test blob as an input stream ...
	public static void main(String args[])
		throws Exception
	{
		Blob b1 = new Blob();
		Blob b2 = new Blob();
		Blob b3 = new Blob();

		int size = 2048;
		try
		{
			size = Integer.parseInt(args[0]);
		}
		catch (Exception ignore) {}

		System.out.println("size: " + size);

		// load b1
		for (int i = 0; i < size; i++)
		{
			int c = i;
			b1.append((byte)c);
			// System.out.println(i + ": appending: " + c + " " + (int)(b1.byteAt(i)));
		}

		// load b2 from b1 via the input stream
		b2.read(b1.inputStream());

		// load b3 from b1 (not using stream)
		b3.append(b1);

		System.out.println("b1 size: " + b1.length());
		System.out.println("b2 size: " + b2.length());
		System.out.println("b3 size: " + b3.length());

		// compare
		if (b2.equals(b1))
		{
			System.out.println(" b2.equals(b1) passed");
		}
		else
		{
			System.out.println(" b2.equals(b1) failed");
		}

		// compare
		if (b3.equals(b1))
		{
			System.out.println(" b3.equals(b1) passed");
		}
		else
		{
			System.out.println(" b3.equals(b1) failed");
		}

		try
		{
			System.out.println("comparing b1 and b2 byte by byte");
			for (int i = 0; i < b1.length(); i++)
			{
				if (b1.byteAt(i) != b2.byteAt(i))
				{
					System.out.println("mismatch at: " + i + " : b1 = " + b1.byteAt(i) + " : b2 = " + b2.byteAt(i));
				}
			}
			System.out.println(b1.length() + " bytes compared");
		}
		catch (Exception e) { System.out.println(e); }

		try
		{
			System.out.println("comparing b1 and b3 byte by byte");
			for (int i = 0; i < b1.length(); i++)
			{
				if (b1.byteAt(i) != b3.byteAt(i))
				{
					System.out.println("mismatch at: " + i + " : b1 = " + b1.byteAt(i) + " : b3 = " + b3.byteAt(i));
				}
			}
			System.out.println(b1.length() + " bytes compared");
		}
		catch (Exception e) { System.out.println(e); }

		InputStream in = b1.inputStream();
		for (int i = 0; i < b1.length(); i++)
		{
			int available = in.available();
			int b = in.read();
			System.out.println(i + " : avail= " + available + " : read = " + b);
		}
		System.out.println("final avail= " + in.available());
		in.close();

	}	// main
*/

} // end Blob


/**
* A node in the underlying data-storage structure of the Blob class.
* @see Blob
* @author T. Gee
*
*/
class BlobNode implements Cloneable, Serializable {

	/**
   * 
   */
  private static final long serialVersionUID = 3833749897282336560L;

  /**
	* The next BlobNode in the list, null if last node.
	*/
	BlobNode next = null;

	/**
	* The number of bytes currently in this node
	*/
	int size;

	/**
	* The actual data held by this node
	*/
	byte data[];


	/**
	* Constructs a new, empty node with the requested capacity.
	* @param capacity The size of this node's internal storage.
	*
	*/
	public BlobNode(int capacity) {
		data = new byte[capacity];
		size = 0;
	}

	/**
	* Constructs a new node initialized with the supplied data.
	* @param capacity The size of the new node -- if smaller than
	* 'data', will be increased to accomidate all of data.
	* @param data The data to use to initialize this BlobNode.
	*
	*/
	public BlobNode(int capacity, byte arr[], int startPos, int len) {
		data = new byte[Math.max(capacity, len)];
		System.arraycopy(arr, startPos, data, 0, len);
		size = len;
	}

	/**
	* Gets the number of bytes of free storage within this node.
	* @return the number of free bytes.
	*
	*/
	public int freespace() {
		return data.length - size;
	}

	/**
	* Clones the node.
	*
	*/
	public Object clone() {
		BlobNode b = new BlobNode(data.length);

		System.arraycopy(this.data, 0, b.data, 0, size);
		b.size = this.size;

		return b;
	}

} // end BlobNode



