DBLargeBinary.java

/*
 * Copyright 2013 Gregory Graham.
 *
 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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 nz.co.gregs.dbvolution.datatypes;

import java.io.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.sql.*;
import java.util.*;
import java.util.logging.*;
import nz.co.gregs.dbvolution.*;
import nz.co.gregs.dbvolution.columns.LargeBinaryColumn;
import nz.co.gregs.dbvolution.databases.definitions.DBDefinition;
import nz.co.gregs.dbvolution.internal.query.LargeObjectHandlerType;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.exceptions.IncorrectRowProviderInstanceSuppliedException;
import nz.co.gregs.dbvolution.query.RowDefinition;
import nz.co.gregs.dbvolution.utility.comparators.ByteArrayComparator;
import org.apache.commons.codec.binary.Base64;

/**
 *
 * Implements the abstractions necessary to handle arbitrary byte streams and
 * files stored in the database.
 *
 * <p>
 * Use DBLargeBinary for files and streams. Store exceptionally long text in
 * {@link DBLargeText} and Java instances/objects as {@link DBJavaObject} for
 * greater convenience.
 *
 * <p>
 * Generally DBLargeBinary is declared inside your DBRow sub-class as:
 * {@code @DBColumn public DBLargeBinary myBinaryColumn = new DBLargeBinary();}
 *
 * <p>
 * DBLargeBinary is the standard type of {@link DBLargeObject BLOB columns}.
 *
 * <p style="color: #F90;">Support DBvolution at
 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
 *
 * @author Gregory Graham
 */

import nz.co.gregs.dbvolution.results.LargeBinaryResult;

public class DBLargeBinary extends DBLargeObject<byte[]> implements LargeBinaryResult{

	private static final long serialVersionUID = 1;

	/**
	 * The Default constructor for a DBBinaryObject.
	 *
	 */
	public DBLargeBinary() {
		super();
	}

	/**
	 * Creates a column expression with a large object result from the expression
	 * provided.
	 *
	 * <p>
	 * Used in {@link DBReport}, and some {@link DBRow}, sub-classes to derive
	 * data from the database prior to retrieval.
	 *
	 * @param aThis an expression that will result in a large object value
	 */
	public DBLargeBinary(LargeBinaryResult aThis) {
		super(aThis);
	}

	public DBLargeBinary(byte[] value) {
		super();
		setByteArray(value);
	}

	/**
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return the standard SQL datatype that corresponds to this QDT as a String
	 */
	@Override
	public String getSQLDatatype() {
		return "BLOB";
	}

	/**
	 * Sets the value of this DBLargeBinary to the byte array supplied.
	 *
	 * @param byteArray	byteArray
	 */
	@Override
	public void setValue(byte[] byteArray) {
		setByteArray(byteArray);
	}

	private void setByteArray(byte[] byteArray) {
		super.setLiteralValue(byteArray);
	}

	/**
	 * Sets the value of this DBLargeBinary to the InputStream supplied.
	 *
	 * <p>
	 * The input stream will not be read until the containing DBRow is
	 * saved/inserted.
	 *
	 * @param inputViaStream	inputViaStream
	 */
	public void setValue(InputStream inputViaStream) {
		setValue(getBytesFromInputStream(inputViaStream));
	}

	/**
	 * Sets the value of this DBLargeBinary to the file supplied.
	 *
	 * <p>
	 * Unlike {@link #setValue(java.io.InputStream) setting an InputStream}, the
	 * file is read immediately and stored internally. If you would prefer to
	 * delay the reading of the file, wrap the file in a {@link FileInputStream}.
	 *
	 * @param fileToRead fileToRead
	 * @throws java.io.IOException java.io.IOException
	 */
	public void setValue(File fileToRead) throws IOException {
		setFromFileSystem(fileToRead);
	}

	/**
	 * Set the value of the DBLargeBinary to the String provided.
	 *
	 * @param string	string
	 */
	public void setValue(String string) {
		setValue(string.getBytes(UTF_8));
	}

	void setValue(DBLargeBinary newLiteralValue) {
		setValue(newLiteralValue.getValue());
	}

	private byte[] getFromBinaryStream(ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] bytes = new byte[]{};
		try (InputStream inputStream = resultSet.getBinaryStream(fullColumnName)) {
			if (inputStream == null) {
				this.setToNull();
			} else {
				bytes = getBytesFromInputStream(inputStream);
			}
		} catch (IOException ex) {
			Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
		}
		return bytes;
	}

	private byte[] getFromBLOB(ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] bytes = new byte[]{};
		Blob blob = resultSet.getBlob(fullColumnName);
		if (resultSet.wasNull()) {
			blob = null;
		}
		if (blob == null) {
			this.setToNull();
		} else {
			InputStream inputStream = blob.getBinaryStream();
			bytes = getBytesFromInputStream(inputStream);
			try {
				inputStream.close();
			} catch (IOException ex) {
				Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
		return bytes;
	}

	private byte[] getBytesFromInputStream(InputStream inputStream) {
		InputStream input = new BufferedInputStream(inputStream);
		List<byte[]> byteArrays = new ArrayList<>();
		try {
			byte[] resultSetBytes;
			final int byteArrayDefaultSize = 100000;
			resultSetBytes = new byte[byteArrayDefaultSize];
			int bytesRead = input.read(resultSetBytes);
			while (bytesRead > 0) {
				if (bytesRead == byteArrayDefaultSize) {
					byteArrays.add(resultSetBytes);
				} else {
					byte[] shortBytes = new byte[bytesRead];
					System.arraycopy(resultSetBytes, 0, shortBytes, 0, bytesRead);
					byteArrays.add(shortBytes);
				}
				resultSetBytes = new byte[byteArrayDefaultSize];
				bytesRead = input.read(resultSetBytes);
			}
		} catch (IOException ex) {
			Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
			throw new DBRuntimeException("Failed to read input", ex);
		} finally {
			try {
				input.close();
			} catch (IOException ex) {
				Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
				throw new DBRuntimeException("Failed to close input", ex);
			}
		}
		return concatAllByteArrays(byteArrays);
	}

	public static byte[] concatAllByteArrays(List<byte[]> bytes) {
		if (bytes.isEmpty()) {
			return new byte[]{};
		} else {
			byte[] first = bytes.get(0);
			bytes.remove(0);
			byte[][] rest = bytes.toArray(new byte[][]{});
			int totalLength = first.length;
			for (byte[] array : rest) {
				totalLength += array.length;
			}
			byte[] result = Arrays.copyOf(first, totalLength);
			int offset = first.length;
			for (byte[] array : rest) {
				System.arraycopy(array, 0, result, offset, array.length);
				offset += array.length;
			}
			return result;
		}
	}

	private byte[] getFromGetBytes(ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] bytes = resultSet.getBytes(fullColumnName);
		return bytes;
	}

	private byte[] getFromCharacterReader(ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] decodeBuffer = new byte[]{};
		Reader inputReader = null;
		try {
			inputReader = resultSet.getCharacterStream(fullColumnName);
		} catch (NullPointerException nullEx) {
			;// NullPointerException is thrown by a SQLite-JDBC bug sometimes.
		}
		if (inputReader != null) {
			if (resultSet.wasNull()) {
				this.setToNull();
			} else {
				BufferedReader input = new BufferedReader(inputReader);
				List<byte[]> byteArrays = new ArrayList<>();

				try {
					char[] resultSetBytes;
					final int byteArrayDefaultSize = 100000;
					resultSetBytes = new char[byteArrayDefaultSize];
					int bytesRead = input.read(resultSetBytes);
					while (bytesRead > 0) {
						if (bytesRead == byteArrayDefaultSize) {
							byteArrays.add(String.valueOf(resultSetBytes).getBytes(UTF_8));
						} else {
							char[] shortBytes = new char[bytesRead];
							System.arraycopy(resultSetBytes, 0, shortBytes, 0, bytesRead);
							byteArrays.add(String.valueOf(shortBytes).getBytes(UTF_8));
						}
						resultSetBytes = new char[byteArrayDefaultSize];
						bytesRead = input.read(resultSetBytes);
					}
				} catch (IOException ex) {
					Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
					throw new DBRuntimeException("Failed to read input", ex);
				} finally {
					try {
						input.close();
					} catch (IOException ex) {
						Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
						throw new DBRuntimeException("Failed to close input", ex);
					}
				}
				byte[] bytes = concatAllByteArrays(byteArrays);
				decodeBuffer = Base64.decodeBase64(bytes);
			}
		}
		return decodeBuffer;
	}

	private byte[] getFromCLOB(ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] bytes = new byte[]{};
		Clob clob = resultSet.getClob(fullColumnName);
		if (resultSet.wasNull() || clob == null) {
			this.setToNull();
		} else {
			final Reader characterStream = clob.getCharacterStream();
			try {
				BufferedReader input = new BufferedReader(characterStream);
				List<byte[]> byteArrays = new ArrayList<>();

				try {
					char[] resultSetBytes;
					final int byteArrayDefaultSize = 100000;
					resultSetBytes = new char[byteArrayDefaultSize];
					int bytesRead = input.read(resultSetBytes);
					while (bytesRead > 0) {
						if (bytesRead == byteArrayDefaultSize) {
							byteArrays.add(String.valueOf(resultSetBytes).getBytes(UTF_8));
						} else {
							char[] shortBytes = new char[bytesRead];
							System.arraycopy(resultSetBytes, 0, shortBytes, 0, bytesRead);
							byteArrays.add(String.valueOf(shortBytes).getBytes(UTF_8));
						}
						resultSetBytes = new char[byteArrayDefaultSize];
						bytesRead = input.read(resultSetBytes);
					}
				} catch (IOException ex) {
					Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
					throw new DBRuntimeException("Failed to read input", ex);
				} finally {
					try {
						input.close();
					} catch (IOException ex) {
						Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
					}
				}
				bytes = concatAllByteArrays(byteArrays);
			} finally {
				try {
					characterStream.close();
				} catch (IOException ex) {
					Logger.getLogger(DBLargeBinary.class.getName()).log(Level.SEVERE, null, ex);
					throw new DBRuntimeException("Failed to close input", ex);
				}
			}
		}
		return bytes;
	}

	@Override
	public String formatValueForSQLStatement(DBDefinition db) {
		throw new UnsupportedOperationException("Binary datatypes like " + this.getClass().getSimpleName() + " do not have a simple SQL representation. Do not call getSQLValue(), use the getInputStream() method instead.");
	}

	/**
	 * Tries to set the DBDyteArray to the contents of the supplied file.
	 *
	 * <p>
	 * Convenience method for {@link #setFromFileSystem(java.io.File) }.
	 *
	 * @param originalFile	originalFile
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 *
	 *
	 */
	public void setFromFileSystem(String originalFile) throws FileNotFoundException, IOException {
		File file = new File(originalFile);
		setFromFileSystem(file);
	}

	/**
	 * Tries to set the DBDyteArray to the contents of the supplied file.
	 *
	 * <p>
	 * Convenience method for {@link #setFromFileSystem(java.io.File) }.
	 *
	 * @param originalFile	originalFile
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 *
	 *
	 */
	public void setFromFileSystem(DBString originalFile) throws FileNotFoundException, IOException {
		File file = new File(originalFile.stringValue());
		setFromFileSystem(file);
	}

	/**
	 * Tries to set the DBDyteArray to the contents of the supplied file.
	 *
	 * @param originalFile	originalFile
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 *
	 *
	 */
	private void setFromFileSystem(File originalFile) throws FileNotFoundException, IOException {
		byte[] bytes = new byte[(int) originalFile.length()];
		InputStream input = null;
		try {
			int totalBytesRead = 0;
			input = new BufferedInputStream(new FileInputStream(originalFile));
			while (totalBytesRead < bytes.length) {
				int bytesRemaining = bytes.length - totalBytesRead;
				int bytesRead = input.read(bytes, totalBytesRead, bytesRemaining);
				if (bytesRead > 0) {
					totalBytesRead += bytesRead;
				}
			}
			/*
			 the above style is a bit tricky: it places bytes into the 'result' array;
			 'result' is an output parameter;
			 the while loop usually has a single iteration only.
			 */
		} finally {
			if (input != null) {
				input.close();
			}
		}
		setValue(bytes);
	}

	/**
	 * Tries to write the contents of this DBLargeBinary to the file supplied.
	 *
	 * <p>
	 * Convenience method for {@link #writeToFileSystem(java.io.File) }.
	 *
	 * @param originalFile originalFile
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 */
	public void writeToFileSystem(String originalFile) throws FileNotFoundException, IOException {
		File file = new File(originalFile);
		writeToFileSystem(file);
	}

	/**
	 * Tries to write the contents of this DBLargeBinary to the file supplied.
	 *
	 * <p>
	 * Convenience method for {@link #writeToFileSystem(java.io.File) }.
	 *
	 * @param originalFile originalFile
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 */
	public void writeToFileSystem(DBString originalFile) throws FileNotFoundException, IOException {
		writeToFileSystem(originalFile.toString());
	}

	/**
	 * Tries to write the contents of this DBLargeBinary to the file supplied.
	 *
	 * <p>
	 * Convenience method for {@link #writeToFileSystem(java.io.File) }.
	 *
	 * @param originalFile originalFile
	 * @throws java.io.FileNotFoundException java.io.FileNotFoundException
	 * @throws java.io.IOException java.io.IOException
	 */
	public void writeToFileSystem(File originalFile) throws FileNotFoundException, IOException {
		if (getLiteralValue() != null && originalFile != null) {
			if (!originalFile.exists()) {
				boolean createNewFile = originalFile.createNewFile();
				if (!createNewFile) {
					boolean delete = originalFile.delete();
					if (!delete) {
						throw new IOException("Unable to delete file: " + originalFile.getPath() + " could not be deleted, check the permissions of the file, directory, drive, and current user.");
					}
					createNewFile = originalFile.createNewFile();
					if (!createNewFile) {
						throw new IOException("Unable to create file: " + originalFile.getPath() + " could not be created, check the permissions of the file, directory, drive, and current user.");
					}
				}
			}
			if (originalFile.exists()) {
				try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(originalFile))) {
					output.write(getBytes());
					output.flush();
				}
			} else {
				throw new FileNotFoundException("Unable Create File: the file \"" + originalFile.getAbsolutePath() + " could not be found or created.");
			}
		}
	}

	/**
	 * Returns the internal InputStream.
	 *
	 * <p>
	 * Remember to close the InputStream.
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return an InputStream to read the bytes.
	 */
	@Override
	public InputStream getInputStream() {
		return new BufferedInputStream(new ByteArrayInputStream(getBytes()));
	}

	/**
	 * Returns the byte[] used internally to store the value of this
	 * DBLargeBinary.
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return the byte[] value of this DBLargeBinary.
	 */
	public byte[] getBytes() {
		final byte[] litVal = this.getLiteralValue();
		if (litVal != null) {
			return litVal;
		}
		return new byte[]{};
	}

	@Override
	public String stringValue() {
		byte[] value = this.getValue();
		if (this.isNull()) {
			return super.stringValue();
		} else {
			return new String(value, UTF_8);
		}
	}

	@Override
	public int getSize() {
		final byte[] bytes = getBytes();
		if (bytes != null) {
			return bytes.length;
		} else {
			return 0;
		}
	}

	@Override
	public byte[] getValue() {
		return getBytes();
	}

	@Override
	public DBLargeBinary getQueryableDatatypeForExpressionValue() {
		return new DBLargeBinary();
	}

	@Override
	public boolean isAggregator() {
		return false;
	}

	@Override
	public Set<DBRow> getTablesInvolved() {
		return new HashSet<>();
	}

	@Override
	protected byte[] getFromResultSet(DBDefinition defn, ResultSet resultSet, String fullColumnName) throws SQLException {
		byte[] bytes = new byte[]{};
		LargeObjectHandlerType handler = defn.preferredLargeObjectReader(this);
		switch (handler) {
			case BLOB:
				bytes = getFromBLOB(resultSet, fullColumnName);
				break;
			case BASE64:
				bytes = getFromBase64(resultSet, fullColumnName);
				break;
			case BINARYSTREAM:
				bytes = getFromBinaryStream(resultSet, fullColumnName);
				break;
			case CHARSTREAM:
				bytes = getFromCharacterReader(resultSet, fullColumnName);
				break;
			case CLOB:
				bytes = getFromCLOB(resultSet, fullColumnName);
				break;
			case STRING:
				bytes = getFromString(resultSet, fullColumnName);
				break;
			case JAVAOBJECT:
				bytes = getFromJavaObject(resultSet, fullColumnName);
				break;
			case BYTE:
				bytes = getFromGetBytes(resultSet, fullColumnName);
				break;
		}
		return bytes;
	}

	@Override
	public boolean getIncludesNull() {
		return false;
	}

	@Override
	protected void setValueFromStandardStringEncoding(String encodedValue) {
		throw new UnsupportedOperationException("DBLargeBinaryObject does not support setValueFromStandardStringEncoding(String) yet.");
	}

	private byte[] getFromBase64(ResultSet resultSet, String fullColumnName) {
		throw new UnsupportedOperationException("DBLargeBinaryObject does not support getFromBase64(ResultSet, String) yet.");
	}

	private byte[] getFromString(ResultSet resultSet, String fullColumnName) {
		throw new UnsupportedOperationException("DBLargeBinaryObject does not support getFromString(ResultSet,String) yet.");
	}

	private byte[] getFromJavaObject(ResultSet resultSet, String fullColumnName) {
		throw new UnsupportedOperationException("DBLargeBinaryObject does not support getFromJavaObject(ResultSet, String) yet.");
	}

	@Override
	public LargeBinaryColumn getColumn(RowDefinition row) throws IncorrectRowProviderInstanceSuppliedException {
		return new LargeBinaryColumn(row, this);
	}

	/**
	 * Indicates whether object is NULL within the database
	 *
	 * <p>
	 * Databases and Java both use the term NULL but for slightly different
	 * meanings.
	 *
	 * <p>
	 * This method indicates whether the field represented by this object is NULL
	 * in the database sense.
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return TRUE if this object represents a NULL database value, otherwise
	 * FALSE
	 */
	@Override
	public boolean isNull() {
		return super.isNull() && getLiteralValue() == null;//&& this.byteStream == null;
	}

	@Override
	public synchronized DBLargeBinary copy() {
		DBLargeBinary result = (DBLargeBinary) super.copy();
		return result;
	}

	@Override
	public Comparator<byte[]> getComparator() {
		return new ByteArrayComparator();
	}
}