/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cayenne.access.types;
import org.apache.cayenne.CayenneException;
import org.apache.cayenne.util.IDUtil;
import org.apache.cayenne.util.MemoryBlob;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
/**
* Handles <code>byte[]</code>, mapping it as either of JDBC types - BLOB or
* (VAR)BINARY. Can be configured to trim trailing zero bytes.
*/
public class ByteArrayType implements ExtendedType<byte[]> {
private static final int BUF_SIZE = 8 * 1024;
protected boolean trimmingBytes;
protected boolean usingBlobs;
public static void logBytes(StringBuilder buffer, byte[] bytes) {
buffer.append("< ");
int len = bytes.length;
boolean trimming = false;
if (len > TRIM_VALUES_THRESHOLD) {
len = TRIM_VALUES_THRESHOLD;
trimming = true;
}
for (int i = 0; i < len; i++) {
if (i > 0) {
buffer.append(",");
}
IDUtil.appendFormattedByte(buffer, bytes[i]);
}
if (trimming) {
buffer.append("...");
}
buffer.append('>');
}
/**
* Strips null bytes from the byte array, returning a potentially smaller
* array that contains no trailing zero bytes.
*/
public static byte[] trimBytes(byte[] bytes) {
int bytesToTrim = 0;
for (int i = bytes.length - 1; i >= 0; i--) {
if (bytes[i] != 0) {
bytesToTrim = bytes.length - 1 - i;
break;
}
}
if (bytesToTrim == 0) {
return bytes;
}
byte[] dest = new byte[bytes.length - bytesToTrim];
System.arraycopy(bytes, 0, dest, 0, dest.length);
return dest;
}
public ByteArrayType(boolean trimmingBytes, boolean usingBlobs) {
this.usingBlobs = usingBlobs;
this.trimmingBytes = trimmingBytes;
}
@Override
public String getClassName() {
return "byte[]";
}
@Override
public byte[] materializeObject(ResultSet rs, int index, int type) throws Exception {
byte[] bytes = null;
if (type == Types.BLOB) {
bytes = (isUsingBlobs()) ? readBlob(rs.getBlob(index)) : readBinaryStream(rs, index);
} else {
bytes = rs.getBytes(index);
// trim BINARY type
if (bytes != null && type == Types.BINARY && isTrimmingBytes()) {
bytes = trimBytes(bytes);
}
}
return bytes;
}
@Override
public byte[] materializeObject(CallableStatement cs, int index, int type) throws Exception {
byte[] bytes = null;
if (type == Types.BLOB) {
if (!isUsingBlobs()) {
throw new CayenneException("Binary streams are not supported in stored procedure parameters.");
}
bytes = readBlob(cs.getBlob(index));
} else {
bytes = cs.getBytes(index);
// trim BINARY type
if (bytes != null && type == Types.BINARY && isTrimmingBytes()) {
bytes = trimBytes(bytes);
}
}
return bytes;
}
@Override
public void setJdbcObject(PreparedStatement st, byte[] val, int pos, int type, int scale) throws Exception {
// if this is a BLOB column, set the value as "bytes"
// instead. This should work with most drivers
if (type == Types.BLOB) {
if (isUsingBlobs()) {
st.setBlob(pos, writeBlob(val));
} else {
st.setBytes(pos, val);
}
} else {
if (scale != -1) {
st.setObject(pos, val, type, scale);
} else {
st.setObject(pos, val, type);
}
}
}
@Override
public String toString(byte[] value) {
if (value == null) {
return "NULL";
}
StringBuilder buffer = new StringBuilder();
logBytes(buffer, value);
return buffer.toString();
}
protected Blob writeBlob(byte[] bytes) {
// TODO: should we use Connection.createBlob() instead? (Like Oracle
// ByteArrayType does)
return bytes != null ? new MemoryBlob(bytes) : null;
}
protected byte[] readBlob(Blob blob) throws IOException, SQLException {
if (blob == null) {
return null;
}
// sanity check on size
if (blob.length() > Integer.MAX_VALUE) {
throw new IllegalArgumentException("BLOB is too big to be read as byte[] in memory: " + blob.length());
}
int size = (int) blob.length();
if (size == 0) {
return new byte[0];
}
return blob.getBytes(1, size);
}
protected byte[] readBinaryStream(ResultSet rs, int index) throws IOException, SQLException {
try (InputStream in = rs.getBinaryStream(index);) {
return (in != null) ? readValueStream(in, -1, BUF_SIZE) : null;
}
}
protected byte[] readValueStream(InputStream in, int streamSize, int bufSize) throws IOException {
byte[] buf = new byte[bufSize];
int read;
ByteArrayOutputStream out = (streamSize > 0) ? new ByteArrayOutputStream(streamSize)
: new ByteArrayOutputStream();
while ((read = in.read(buf, 0, bufSize)) >= 0) {
out.write(buf, 0, read);
}
return out.toByteArray();
}
/**
* Returns <code>true</code> if byte columns are handled as BLOBs
* internally.
*/
public boolean isUsingBlobs() {
return usingBlobs;
}
public void setUsingBlobs(boolean usingBlobs) {
this.usingBlobs = usingBlobs;
}
public boolean isTrimmingBytes() {
return trimmingBytes;
}
public void setTrimmingBytes(boolean trimingBytes) {
this.trimmingBytes = trimingBytes;
}
}