/*
* Copyright (c) 2004-2016 TADA AB and other contributors, as listed below.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the The BSD 3-Clause License
* which accompanies this distribution, and is available at
* http://opensource.org/licenses/BSD-3-Clause
*
* Contributors:
* Thomas Hallgren
* Chapman Flack
*/
package org.postgresql.pljava.jdbc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.Ref;
import java.sql.RowId;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLDataException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLNonTransientException;
import java.sql.SQLOutput;
import java.sql.SQLXML;
import java.sql.Struct;
import java.sql.Time;
import java.sql.Timestamp;
import org.postgresql.pljava.internal.Backend;
/**
* The SQLOutputToChunk uses JNI to build a PostgreSQL StringInfo buffer in
* memory. A user should never make an attempt to create an instance of this
* class. Only internal JNI routines can do that. An instance is propagated
* in a call from the internal JNI layer to the Java layer will only survive
* during that single call. The handle of the instance will be invalidated when
* the call returns and subsequent use of the instance will yield a
* SQLException with the message "Stream is closed".
*
* @author Thomas Hallgren
*/
public class SQLOutputToChunk implements SQLOutput
{
private static final byte[] s_byteBuffer = new byte[8];
private long m_handle;
private ByteBuffer m_bb;
private static ByteOrder scalarOrder;
private static ByteOrder mirrorOrder;
public SQLOutputToChunk(long handle, ByteBuffer bb,
boolean isJavaBasedScalar)
throws SQLException
{
m_handle = handle;
m_bb = bb;
if ( isJavaBasedScalar )
{
if ( null == scalarOrder )
scalarOrder = getOrder(true);
m_bb.order(scalarOrder);
}
else
{
if ( null == mirrorOrder )
mirrorOrder = getOrder(false);
m_bb.order(mirrorOrder);
}
}
private ByteOrder getOrder(boolean isJavaBasedScalar) throws SQLException
{
ByteOrder result;
String key = "org.postgresql.pljava.udt.byteorder."
+ ( isJavaBasedScalar ? "scalar" : "mirror" ) + ".j2p";
String val = System.getProperty(key);
if ( "big_endian".equals(val) )
result = ByteOrder.BIG_ENDIAN;
else if ( "little_endian".equals(val) )
result = ByteOrder.LITTLE_ENDIAN;
else if ( "native".equals(val) )
result = ByteOrder.nativeOrder();
else
throw new SQLNonTransientException(
"System property " + key +
" must be big_endian, little_endian, or native", "F0000");
return result;
}
@Override
public void writeArray(Array value) throws SQLException
{
throw unsupportedOperationException("writeArray");
}
@Override
public void writeAsciiStream(InputStream value) throws SQLException
{
throw unsupportedOperationException("writeAsciiStream");
}
@Override
public void writeBigDecimal(BigDecimal value) throws SQLException
{
this.writeString(value.toString());
}
@Override
public void writeBinaryStream(InputStream value) throws SQLException
{
byte[] buf = new byte[65536]; /* one more than max representable */
int got;
try
{
got = value.read(buf);
}
catch ( IOException e )
{
throw new SQLException(
"Error making binary form of user-defined type from " +
"input stream", "58030", e);
}
if ( -1 == got )
got = 0;
if ( 65535 < got )
throw badRepresentation("writeBinaryStream");
ensureCapacity(2 + got);
m_bb.putShort((short)got).put(buf, 0, got);
}
@Override
public void writeBlob(Blob value) throws SQLException
{
throw unsupportedOperationException("writeBlob");
}
@Override
public void writeBoolean(boolean value) throws SQLException
{
this.writeByte(value ? (byte)1 : (byte)0);
}
@Override
public void writeByte(byte value) throws SQLException
{
try
{
m_bb.put(value);
}
catch ( Exception e )
{
throwOrRetry(e, 1, "writeByte");
m_bb.put(value);
}
}
@Override
public void writeBytes(byte[] buffer) throws SQLException
{
if ( 65535 < buffer.length )
throw badRepresentation("writeBytes");
ensureCapacity(2 + buffer.length);
m_bb.putShort((short)buffer.length).put(buffer);
}
@Override
public void writeCharacterStream(Reader value) throws SQLException
{
ByteBuffer bb = ByteBuffer.allocate(65535);
CharBuffer cb = CharBuffer.allocate(1024);
CharsetEncoder enc = UTF_8.newEncoder();
CoderResult cr;
try
{
while ( -1 != value.read(cb) )
{
cb.flip();
cr = enc.encode(cb, bb, false);
if ( ! cr.isUnderflow() )
cr.throwException();
cb.clear();
}
cb.flip();
cr = enc.encode(cb, bb, true);
if ( cr.isUnderflow() )
cr = enc.flush(bb);
if ( ! cr.isUnderflow() )
cr.throwException();
bb.flip();
ensureCapacity(2 + bb.limit());
m_bb.putShort((short)bb.limit()).put(bb);
}
catch ( Exception e )
{
throw badRepresentation(e);
}
}
@Override
public void writeClob(Clob value) throws SQLException
{
throw unsupportedOperationException("writeClob");
}
@Override
public void writeDate(Date value) throws SQLException
{
long v = value.getTime();
try
{
m_bb.putLong(v);
}
catch ( Exception e )
{
throwOrRetry(e, 8, "writeDate");
m_bb.putLong(v);
}
}
@Override
public void writeDouble(double value) throws SQLException
{
try
{
m_bb.putDouble(value);
}
catch ( Exception e )
{
throwOrRetry(e, 8, "writeDouble");
m_bb.putDouble(value);
}
}
@Override
public void writeFloat(float value) throws SQLException
{
try
{
m_bb.putFloat(value);
}
catch ( Exception e )
{
throwOrRetry(e, 4, "writeFloat");
m_bb.putFloat(value);
}
}
@Override
public void writeInt(int value) throws SQLException
{
try
{
m_bb.putInt(value);
}
catch ( Exception e )
{
throwOrRetry(e, 4, "writeInt");
m_bb.putInt(value);
}
}
@Override
public void writeLong(long value) throws SQLException
{
try
{
m_bb.putLong(value);
}
catch ( Exception e )
{
throwOrRetry(e, 8, "writeLong");
m_bb.putLong(value);
}
}
@Override
public void writeObject(SQLData value) throws SQLException
{
throw unsupportedOperationException("writeObject");
}
@Override
public void writeRef(Ref value) throws SQLException
{
throw unsupportedOperationException("writeRef");
}
@Override
public void writeShort(short value) throws SQLException
{
try
{
m_bb.putShort(value);
}
catch ( Exception e )
{
throwOrRetry(e, 2, "writeShort");
m_bb.putShort(value);
}
}
@Override
public void writeString(String value) throws SQLException
{
CharBuffer cb = CharBuffer.wrap(value);
try
{
CharsetEncoder enc = UTF_8.newEncoder();
ByteBuffer bb = enc.encode(cb);
int len = bb.limit();
if ( 65535 < len )
throw badRepresentation("writeString");
ensureCapacity(2 + len);
m_bb.putShort((short)len).put(bb);
}
catch ( Exception e )
{
throw badRepresentation(e);
}
}
@Override
public void writeStruct(Struct value) throws SQLException
{
throw unsupportedOperationException("writeStruct");
}
@Override
public void writeTime(Time value) throws SQLException
{
long v = value.getTime();
try
{
m_bb.putLong(v);
}
catch ( Exception e )
{
throwOrRetry(e, 8, "writeTime");
m_bb.putLong(v);
}
}
@Override
public void writeTimestamp(Timestamp value) throws SQLException
{
long v = value.getTime();
try
{
m_bb.putLong(v);
}
catch ( Exception e )
{
throwOrRetry(e, 8, "writeTimestamp");
m_bb.putLong(v);
}
}
@Override
public void writeURL(URL value) throws SQLException
{
this.writeString(value.toString());
}
void close() throws SQLException
{
if ( 0 == m_handle )
return;
ensureCapacity(0); /* propagate final position to native stringinfo */
m_handle = 0;
m_bb = null;
}
private void throwOrRetry(Exception e, int needed, String fn)
throws SQLException
{
if ( e instanceof BufferOverflowException )
{
ensureCapacity(needed);
return;
}
throw badRepresentation(e);
}
private SQLException unsupportedOperationException(String op)
{
return new SQLFeatureNotSupportedException(
this.getClass() + "." + op + " not implemented yet.", "0A000");
}
private SQLException badRepresentation(String fn)
{
return new SQLNonTransientException(
"Limit of 65535 bytes exceeded in " + fn + "for user-defined type",
"54000");
}
private SQLException badRepresentation(Exception e)
{
if ( e instanceof NullPointerException )
return new SQLNonTransientException(
"attempted write via SQLOutput after closing it", "55000", e);
if ( e instanceof BufferOverflowException )
return new SQLNonTransientException(
"Byte limit exceeded for user-defined type",
"54000");
if ( e instanceof UnmappableCharacterException )
/*
* As long as the string encoding is unconditionally UTF-8, this
* shouldn't really be possible, but if the encoding is ever made
* selectable, this could happen.
*/
return new SQLDataException(
"Character not available in destination encoding",
"22P05", e);
if ( e instanceof MalformedInputException )
/*
* This actually CAN happen ... as the input arrives in UTF-16,
* not codepoints.
*/
return new SQLDataException(
"Input that does not encode a valid character",
"22021", e);
return new SQLDataException(
"Could not form binary representation of user-defined type",
"22P03", e);
}
// ************************************************************
// Non-implementation of JDBC 4 methods.
// ************************************************************
@Override
public void writeNClob(NClob x)
throws SQLException
{
throw unsupportedOperationException("writeNClob( NClob )");
}
@Override
public void writeNString(String x)
throws SQLException
{
throw unsupportedOperationException("writeNString( String )");
}
@Override
public void writeRowId(RowId x)
throws SQLException
{
throw unsupportedOperationException("writeRowId( RowId )");
}
@Override
public void writeSQLXML(SQLXML x)
throws SQLException
{
throw unsupportedOperationException("writeSQLXML( SQLXML )");
}
// ************************************************************
// End of non-implementation of JDBC 4 methods.
// ************************************************************
private void ensureCapacity(int c) throws SQLException
{
synchronized(Backend.THREADLOCK)
{
if(m_handle == 0)
throw new SQLException("Stream is closed");
ByteBuffer oldbb = m_bb;
m_bb = _ensureCapacity(m_handle, m_bb, m_bb.position(), c);
if ( m_bb != oldbb )
m_bb.order(oldbb.order());
}
}
private static native ByteBuffer _ensureCapacity(long handle,
ByteBuffer bb, int pos, int needed);
}