content: /*****************************************************************
   *   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
   *
   *    https://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.CayenneRuntimeException;
  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++) {
              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 CayenneRuntimeException("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;
  	}
  }
  