DBLargeText.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.LargeTextColumn;
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.results.LargeTextResult;
import nz.co.gregs.dbvolution.utility.comparators.ByteArrayComparator;
import org.apache.commons.codec.binary.Base64;
/**
*
* Implements the abstractions necessary to handle exceptionally large texts
* stored in the database.
*
* <p>
* Use DBLargeText for exceptionally long text. Store Java instances/objects as
* {@link DBJavaObject} and files as {@link DBLargeBinary} for greater
* convenience.
*
* <p>
* Generally DBLargeText is declared inside your DBRow sub-class as:
* {@code @DBColumn public DBLargeText myByteColumn = new DBLargeText();}
*
* <p>
* DBLargeText is the standard type of
* {@link DBLargeObject CLOB and Text columns}.
*
* @author Gregory Graham
*/
public class DBLargeText extends DBLargeObject<byte[]> implements LargeTextResult {
private static final long serialVersionUID = 1;
transient InputStream byteStream = null;
/**
* The Default constructor for a DBByteObject.
*
*/
public DBLargeText() {
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 DBLargeText(LargeTextResult aThis) {
super(aThis);
}
public DBLargeText(byte[] blobExpression) {
super(blobExpression);
}
/**
*
* @return the standard SQL datatype that corresponds to this QDT as a String
*/
@Override
public String getSQLDatatype() {
return "CLOB";
}
/**
* Sets the value of this DBByteObject to the byte array supplied.
*
* @param byteArray byteArray
*/
@Override
public void setValue(byte[] byteArray) {
super.setLiteralValue(byteArray);
}
/**
* Sets the value of this DBByteObject 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) {
super.setLiteralValue(null);
}
/**
* Sets the value of this DBByteObject 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 {
setValue(setFromFileSystem(fileToRead));
}
/**
* Set the value of the DBByteObject to the String provided.
*
* @param string string
*/
public void setValue(String string) {
setValue(transformToStandardCharset(string));
}
public static byte[] transformToStandardCharset(String string) {
return string.getBytes(UTF_8);
}
public static String transformToStandardCharset(byte[] string) {
return new String(string, UTF_8);
}
void setValue(DBLargeText newLiteralValue) {
setValue(newLiteralValue.getValue());
}
private byte[] getFromBinaryStream(ResultSet resultSet, String fullColumnName) throws SQLException {
byte[] bytes = new byte[]{};
try ( InputStream inputStream = resultSet.getBinaryStream(fullColumnName)) {
if (resultSet.wasNull()) {
this.setToNull();
return bytes;
} else {
bytes = getBytesFromInputStream(inputStream);
}
} catch (IOException ex) {
Logger.getLogger(DBLargeText.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 {
try ( InputStream inputStream = blob.getBinaryStream()) {
bytes = getBytesFromInputStream(inputStream);
} catch (IOException ex) {
Logger.getLogger(DBLargeText.class.getName()).log(Level.SEVERE, null, ex);
}
}
return bytes;
}
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[] getBytesFromInputStream(InputStream inputStream) throws IOException {
byte[] bytes;
List<byte[]> byteArrays = new ArrayList<>();
try ( InputStream input = new BufferedInputStream(inputStream)) {
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);
}
}
bytes = concatAllByteArrays(byteArrays);
return bytes;
}
private byte[] getFromString(ResultSet resultSet, String fullColumnName) throws SQLException {
String gotString = resultSet.getString(fullColumnName);
if (gotString != null) {
return transformToStandardCharset(gotString);
} else {
return new byte[]{};
}
}
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(transformToStandardCharset(String.valueOf(resultSetBytes)));
} else {
char[] shortBytes = new char[bytesRead];
System.arraycopy(resultSetBytes, 0, shortBytes, 0, bytesRead);
byteArrays.add(transformToStandardCharset(String.valueOf(shortBytes)));
}
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();
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(transformToStandardCharset(String.valueOf(resultSetBytes)));
} else {
char[] shortBytes = new char[bytesRead];
System.arraycopy(resultSetBytes, 0, shortBytes, 0, bytesRead);
byteArrays.add(transformToStandardCharset(String.valueOf(shortBytes)));
}
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);
}
}
bytes = concatAllByteArrays(byteArrays);
}
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
* @return the byte[] of the contents of the file.
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*
*
*/
public byte[] setFromFileSystem(String originalFile) throws FileNotFoundException, IOException {
File file = new File(originalFile);
return 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
* @return the byte[] of the contents of the file.
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*
*
*/
public byte[] setFromFileSystem(DBString originalFile) throws FileNotFoundException, IOException {
File file = new File(originalFile.stringValue());
return setFromFileSystem(file);
}
/**
* Tries to set the DBDyteArray to the contents of the supplied file.
*
* @param originalFile originalFile
* @return the byte[] of the contents of the file.
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*
*
*/
public byte[] setFromFileSystem(File originalFile) throws FileNotFoundException, IOException {
byte[] bytes = new byte[(int) originalFile.length()];
try ( InputStream input = new BufferedInputStream(new FileInputStream(originalFile))) {
int totalBytesRead = 0;
while (totalBytesRead < bytes.length) {
int bytesRemaining = bytes.length - totalBytesRead;
//input.read() returns -1, 0, or more :
int bytesRead = input.read(bytes, totalBytesRead, bytesRemaining);
if (bytesRead > 0) {
totalBytesRead += bytesRead;
}
}
}
setValue(bytes);
return bytes;
}
/**
* Tries to write the contents of this DBByteObject 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 DBByteObject 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 DBByteObject 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()) {
OutputStream output = null;
try {
output = new BufferedOutputStream(new FileOutputStream(originalFile));
output.write(getBytes());
output.flush();
output.close();
output = null;
} finally {
if (output != null) {
output.close();
}
}
} else {
throw new FileNotFoundException("Unable Create File: the file \"" + originalFile.getAbsolutePath() + " could not be found or created.");
}
}
}
/**
* Returns the internal InputStream.
*
* @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 DBByteObject.
*
* @return the byte[] value of this DBByteObject.
*/
public byte[] getBytes() {
return this.getLiteralValue();
}
@Override
public String stringValue() {
byte[] value = this.getValue();
if (this.isNull()) {
return super.stringValue();
} else {
return new String(value, UTF_8);
}
}
@Override
public String toString() {
return super.stringValue();
}
@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 DBLargeText getQueryableDatatypeForExpressionValue() {
return new DBLargeText();
}
@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 = getFromByteArray(resultSet, fullColumnName);
break;
}
return bytes;
}
@Override
public boolean getIncludesNull() {
return false;
}
@Override
protected void setValueFromStandardStringEncoding(String encodedValue) {
throw new UnsupportedOperationException("DBLargeText does not support setValueFromStandardStringEncoding(String) yet."); //To change body of generated methods, choose Tools | Templates.
}
private byte[] getFromBase64(ResultSet resultSet, String fullColumnName) throws SQLException {
String gotString = resultSet.getString(fullColumnName);
if (gotString == null) {
return null;
} else {
return Base64.decodeBase64(transformToStandardCharset(gotString));
}
}
private byte[] getFromJavaObject(ResultSet resultSet, String fullColumnName) {
throw new UnsupportedOperationException("DBLargeText does not support getFromJavaObject(ResultSet, String) yet."); //To change body of generated methods, choose Tools | Templates.
}
private byte[] getFromByteArray(ResultSet resultSet, String fullColumnName) throws SQLException {
byte[] gotBytes = resultSet.getBytes(fullColumnName);
return gotBytes;
}
@Override
public LargeTextColumn getColumn(RowDefinition row) throws IncorrectRowProviderInstanceSuppliedException {
return new LargeTextColumn(row, this);
}
@Override
protected synchronized void setLiteralValue(byte[] newLiteralValue) {
if ((!hasBeenSet() && newLiteralValue != null)
|| (hasBeenSet() && getLiteralValue() != null && !(new String(getLiteralValue())).equals(new String(newLiteralValue, UTF_8)))
|| (hasBeenSet() && getLiteralValue() == null && newLiteralValue != null && newLiteralValue.length > 0)) {
super.setLiteralValue(newLiteralValue);
}
}
@Override
public Comparator<byte[]> getComparator() {
return new ByteArrayComparator();
}
@Override
public DBLargeText copy() {
return (DBLargeText) super.copy();
}
}