/***************************************************************** * 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; } }