org.postgresql.core.PGStream Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of postgresql-holo Show documentation
Show all versions of postgresql-holo Show documentation
PostgreSQL JDBC Driver Postgresql
The newest version!
/*
* Copyright (c) 2017, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.core;
import org.postgresql.gss.GSSInputStream;
import org.postgresql.gss.GSSOutputStream;
import org.postgresql.util.ByteStreamWriter;
import org.postgresql.util.GT;
import org.postgresql.util.HostSpec;
import org.postgresql.util.PGPropertyMaxResultBufferParser;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.MessageProp;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.FilterOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.net.SocketFactory;
/**
* Wrapper around the raw connection to the server that implements some basic primitives
* (reading/writing formatted data, doing string encoding, etc).
*
* In general, instances of PGStream are not threadsafe; the caller must ensure that only one thread
* at a time is accessing a particular PGStream instance.
*/
public class PGStream implements Closeable, Flushable {
private static final Logger LOGGER = Logger.getLogger(PGStream.class.getName());
private final SocketFactory socketFactory;
private final HostSpec hostSpec;
private final byte[] int4Buf;
private final byte[] int2Buf;
private Socket connection;
private VisibleBufferedInputStream pgInput;
private OutputStream pgOutput;
private byte @Nullable [] streamBuffer;
public boolean isGssEncrypted() {
return gssEncrypted;
}
boolean gssEncrypted = false;
public void setSecContext(GSSContext secContext) {
MessageProp messageProp = new MessageProp(0, true);
pgInput = new VisibleBufferedInputStream(new GSSInputStream(pgInput.getWrapped(), secContext, messageProp ), 8192);
pgOutput = new GSSOutputStream(pgOutput, secContext, messageProp, 16384);
gssEncrypted = true;
}
private long nextStreamAvailableCheckTime;
// This is a workaround for SSL sockets: sslInputStream.available() might return 0
// so we perform "1ms reads" once in a while
private int minStreamAvailableCheckDelay = 1000;
private Encoding encoding;
private Writer encodingWriter;
private long maxResultBuffer = -1;
private long resultBufferByteCount = 0;
/**
* Constructor: Connect to the PostgreSQL back end and return a stream connection.
*
* @param socketFactory socket factory to use when creating sockets
* @param hostSpec the host and port to connect to
* @param timeout timeout in milliseconds, or 0 if no timeout set
* @throws IOException if an IOException occurs below it.
*/
@SuppressWarnings({"method.invocation.invalid", "initialization.fields.uninitialized"})
public PGStream(SocketFactory socketFactory, HostSpec hostSpec, int timeout) throws IOException {
this.socketFactory = socketFactory;
this.hostSpec = hostSpec;
Socket socket = createSocket(timeout);
changeSocket(socket);
setEncoding(Encoding.getJVMEncoding("UTF-8"));
int2Buf = new byte[2];
int4Buf = new byte[4];
}
@SuppressWarnings({"method.invocation.invalid", "initialization.fields.uninitialized"})
public PGStream(PGStream pgStream, int timeout ) throws IOException {
/*
Some defaults
*/
int sendBufferSize = 1024;
int receiveBufferSize = 1024;
int soTimeout = 0;
boolean keepAlive = false;
/*
Get the existing values before closing the stream
*/
try {
sendBufferSize = pgStream.getSocket().getSendBufferSize();
receiveBufferSize = pgStream.getSocket().getReceiveBufferSize();
soTimeout = pgStream.getSocket().getSoTimeout();
keepAlive = pgStream.getSocket().getKeepAlive();
} catch ( SocketException ex ) {
// ignore it
}
//close the existing stream
pgStream.close();
this.socketFactory = pgStream.socketFactory;
this.hostSpec = pgStream.hostSpec;
Socket socket = createSocket(timeout);
changeSocket(socket);
setEncoding(Encoding.getJVMEncoding("UTF-8"));
// set the buffer sizes and timeout
socket.setReceiveBufferSize(receiveBufferSize);
socket.setSendBufferSize(sendBufferSize);
setNetworkTimeout(soTimeout);
socket.setKeepAlive(keepAlive);
int2Buf = new byte[2];
int4Buf = new byte[4];
}
/**
* Constructor: Connect to the PostgreSQL back end and return a stream connection.
*
* @param socketFactory socket factory
* @param hostSpec the host and port to connect to
* @throws IOException if an IOException occurs below it.
* @deprecated use {@link #PGStream(SocketFactory, org.postgresql.util.HostSpec, int)}
*/
@Deprecated
public PGStream(SocketFactory socketFactory, HostSpec hostSpec) throws IOException {
this(socketFactory, hostSpec, 0);
}
public HostSpec getHostSpec() {
return hostSpec;
}
public Socket getSocket() {
return connection;
}
public SocketFactory getSocketFactory() {
return socketFactory;
}
/**
* Check for pending backend messages without blocking. Might return false when there actually are
* messages waiting, depending on the characteristics of the underlying socket. This is used to
* detect asynchronous notifies from the backend, when available.
*
* @return true if there is a pending backend message
* @throws IOException if something wrong happens
*/
public boolean hasMessagePending() throws IOException {
boolean available = false;
// In certain cases, available returns 0, yet there are bytes
if (pgInput.available() > 0) {
return true;
}
long now = System.nanoTime() / 1000000;
if (now < nextStreamAvailableCheckTime && minStreamAvailableCheckDelay != 0) {
// Do not use ".peek" too often
return false;
}
int soTimeout = getNetworkTimeout();
connection.setSoTimeout(1);
try {
if (!pgInput.ensureBytes(1, false)) {
return false;
}
available = (pgInput.peek() != -1);
} catch (SocketTimeoutException e) {
return false;
} finally {
connection.setSoTimeout(soTimeout);
}
/*
If none available then set the next check time
In the event that there more async bytes available we will continue to get them all
see issue 1547 https://github.com/pgjdbc/pgjdbc/issues/1547
*/
if (!available) {
nextStreamAvailableCheckTime = now + minStreamAvailableCheckDelay;
}
return available;
}
public void setMinStreamAvailableCheckDelay(int delay) {
this.minStreamAvailableCheckDelay = delay;
}
private Socket createSocket(int timeout) throws IOException {
IOException exception = new IOException("create socket to " + hostSpec + " failed");;
List ipAddressList = getShuffledAddressList(hostSpec.getHost());
for (String ip : ipAddressList) {
try {
Socket socket = socketFactory.createSocket();
if (!socket.isConnected()) {
// When using a SOCKS proxy, the host might not be resolvable locally,
// thus we defer resolution until the traffic reaches the proxy. If there
// is no proxy, we must resolve the host to an IP to connect the socket.
InetSocketAddress address = hostSpec.shouldResolve()
? new InetSocketAddress(ip, hostSpec.getPort())
: InetSocketAddress.createUnresolved(ip, hostSpec.getPort());
socket.connect(address, timeout);
}
return socket;
} catch (IOException e) {
exception = e;
LOGGER.log(Level.WARNING, "create socket to {0}[{1}] failed, try next address.",
new Object[]{hostSpec, ip + ":" + hostSpec.getPort()});
}
}
throw exception;
}
private List getShuffledAddressList(String host) throws IOException {
try {
List inetAddresses = Arrays.asList(InetAddress.getAllByName(host));
if (inetAddresses.isEmpty()) {
// The address resolution failed and it regressed to a connection to the original address.
return Collections.singletonList(host);
}
Collections.shuffle(inetAddresses);
return inetAddresses.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
} catch (UnknownHostException e) {
throw new IOException("Failed to resolve host " + host, e);
}
}
/**
* Switch this stream to using a new socket. Any existing socket is not closed; it's
* assumed that we are changing to a new socket that delegates to the original socket (e.g. SSL).
*
* @param socket the new socket to change to
* @throws IOException if something goes wrong
*/
public void changeSocket(Socket socket) throws IOException {
assert connection != socket : "changeSocket is called with the current socket as argument."
+ " This is a no-op, however, it re-allocates buffered streams, so refrain from"
+ " excessive changeSocket calls";
this.connection = socket;
// Submitted by Jason Venner . Disable Nagle
// as we are selective about flushing output only when we
// really need to.
connection.setTcpNoDelay(true);
// Buffer sizes submitted by Sverre H Huseby
pgInput = new VisibleBufferedInputStream(connection.getInputStream(), 8192);
pgOutput = new BufferedOutputStream(connection.getOutputStream(), 8192);
if (encoding != null) {
setEncoding(encoding);
}
}
public Encoding getEncoding() {
return encoding;
}
/**
* Change the encoding used by this connection.
*
* @param encoding the new encoding to use
* @throws IOException if something goes wrong
*/
public void setEncoding(Encoding encoding) throws IOException {
if (this.encoding != null && this.encoding.name().equals(encoding.name())) {
return;
}
// Close down any old writer.
if (encodingWriter != null) {
encodingWriter.close();
}
this.encoding = encoding;
// Intercept flush() downcalls from the writer; our caller
// will call PGStream.flush() as needed.
OutputStream interceptor = new FilterOutputStream(pgOutput) {
public void flush() throws IOException {
}
public void close() throws IOException {
super.flush();
}
};
encodingWriter = encoding.getEncodingWriter(interceptor);
}
/**
* Get a Writer instance that encodes directly onto the underlying stream.
*
* The returned Writer should not be closed, as it's a shared object. Writer.flush needs to be
* called when switching between use of the Writer and use of the PGStream write methods, but it
* won't actually flush output all the way out -- call {@link #flush} to actually ensure all
* output has been pushed to the server.
*
* @return the shared Writer instance
* @throws IOException if something goes wrong.
*/
public Writer getEncodingWriter() throws IOException {
if (encodingWriter == null) {
throw new IOException("No encoding has been set on this connection");
}
return encodingWriter;
}
/**
* Sends a single character to the back end.
*
* @param val the character to be sent
* @throws IOException if an I/O error occurs
*/
public void sendChar(int val) throws IOException {
pgOutput.write(val);
}
/**
* Sends a 4-byte integer to the back end.
*
* @param val the integer to be sent
* @throws IOException if an I/O error occurs
*/
public void sendInteger4(int val) throws IOException {
int4Buf[0] = (byte) (val >>> 24);
int4Buf[1] = (byte) (val >>> 16);
int4Buf[2] = (byte) (val >>> 8);
int4Buf[3] = (byte) (val);
pgOutput.write(int4Buf);
}
/**
* Sends a 2-byte integer (short) to the back end.
*
* @param val the integer to be sent
* @throws IOException if an I/O error occurs or {@code val} cannot be encoded in 2 bytes
*/
public void sendInteger2(int val) throws IOException {
if (val < Short.MIN_VALUE || val > Short.MAX_VALUE) {
throw new IOException("Tried to send an out-of-range integer as a 2-byte value: " + val);
}
int2Buf[0] = (byte) (val >>> 8);
int2Buf[1] = (byte) val;
pgOutput.write(int2Buf);
}
/**
* Send an array of bytes to the backend.
*
* @param buf The array of bytes to be sent
* @throws IOException if an I/O error occurs
*/
public void send(byte[] buf) throws IOException {
pgOutput.write(buf);
}
/**
* Send a fixed-size array of bytes to the backend. If {@code buf.length < siz}, pad with zeros.
* If {@code buf.lengh > siz}, truncate the array.
*
* @param buf the array of bytes to be sent
* @param siz the number of bytes to be sent
* @throws IOException if an I/O error occurs
*/
public void send(byte[] buf, int siz) throws IOException {
send(buf, 0, siz);
}
/**
* Send a fixed-size array of bytes to the backend. If {@code length < siz}, pad with zeros. If
* {@code length > siz}, truncate the array.
*
* @param buf the array of bytes to be sent
* @param off offset in the array to start sending from
* @param siz the number of bytes to be sent
* @throws IOException if an I/O error occurs
*/
public void send(byte[] buf, int off, int siz) throws IOException {
int bufamt = buf.length - off;
pgOutput.write(buf, off, bufamt < siz ? bufamt : siz);
for (int i = bufamt; i < siz; ++i) {
pgOutput.write(0);
}
}
/**
* Send a fixed-size array of bytes to the backend. If {@code length < siz}, pad with zeros. If
* {@code length > siz}, truncate the array.
*
* @param writer the stream writer to invoke to send the bytes
* @throws IOException if an I/O error occurs
*/
public void send(ByteStreamWriter writer) throws IOException {
final FixedLengthOutputStream fixedLengthStream = new FixedLengthOutputStream(writer.getLength(), pgOutput);
try {
writer.writeTo(new ByteStreamWriter.ByteStreamTarget() {
@Override
public OutputStream getOutputStream() {
return fixedLengthStream;
}
});
} catch (IOException ioe) {
throw ioe;
} catch (Exception re) {
throw new IOException("Error writing bytes to stream", re);
}
for (int i = fixedLengthStream.remaining(); i > 0; i--) {
pgOutput.write(0);
}
}
/**
* Receives a single character from the backend, without advancing the current protocol stream
* position.
*
* @return the character received
* @throws IOException if an I/O Error occurs
*/
public int peekChar() throws IOException {
int c = pgInput.peek();
if (c < 0) {
throw new EOFException();
}
return c;
}
/**
* Receives a single character from the backend.
*
* @return the character received
* @throws IOException if an I/O Error occurs
*/
public int receiveChar() throws IOException {
int c = pgInput.read();
if (c < 0) {
throw new EOFException();
}
return c;
}
/**
* Receives a four byte integer from the backend.
*
* @return the integer received from the backend
* @throws IOException if an I/O error occurs
*/
public int receiveInteger4() throws IOException {
if (pgInput.read(int4Buf) != 4) {
throw new EOFException();
}
return (int4Buf[0] & 0xFF) << 24 | (int4Buf[1] & 0xFF) << 16 | (int4Buf[2] & 0xFF) << 8
| int4Buf[3] & 0xFF;
}
/**
* Receives a two byte integer from the backend.
*
* @return the integer received from the backend
* @throws IOException if an I/O error occurs
*/
public int receiveInteger2() throws IOException {
if (pgInput.read(int2Buf) != 2) {
throw new EOFException();
}
return (int2Buf[0] & 0xFF) << 8 | int2Buf[1] & 0xFF;
}
/**
* Receives a fixed-size string from the backend.
*
* @param len the length of the string to receive, in bytes.
* @return the decoded string
* @throws IOException if something wrong happens
*/
public String receiveString(int len) throws IOException {
if (!pgInput.ensureBytes(len)) {
throw new EOFException();
}
String res = encoding.decode(pgInput.getBuffer(), pgInput.getIndex(), len);
pgInput.skip(len);
return res;
}
/**
* Receives a fixed-size string from the backend, and tries to avoid "UTF-8 decode failed"
* errors.
*
* @param len the length of the string to receive, in bytes.
* @return the decoded string
* @throws IOException if something wrong happens
*/
public EncodingPredictor.DecodeResult receiveErrorString(int len) throws IOException {
if (!pgInput.ensureBytes(len)) {
throw new EOFException();
}
EncodingPredictor.DecodeResult res;
try {
String value = encoding.decode(pgInput.getBuffer(), pgInput.getIndex(), len);
// no autodetect warning as the message was converted on its own
res = new EncodingPredictor.DecodeResult(value, null);
} catch (IOException e) {
res = EncodingPredictor.decode(pgInput.getBuffer(), pgInput.getIndex(), len);
if (res == null) {
Encoding enc = Encoding.defaultEncoding();
String value = enc.decode(pgInput.getBuffer(), pgInput.getIndex(), len);
res = new EncodingPredictor.DecodeResult(value, enc.name());
}
}
pgInput.skip(len);
return res;
}
/**
* Receives a null-terminated string from the backend. If we don't see a null, then we assume
* something has gone wrong.
*
* @return string from back end
* @throws IOException if an I/O error occurs, or end of file
*/
public String receiveString() throws IOException {
int len = pgInput.scanCStringLength();
String res = encoding.decode(pgInput.getBuffer(), pgInput.getIndex(), len - 1);
pgInput.skip(len);
return res;
}
/**
* Read a tuple from the back end. A tuple is a two dimensional array of bytes. This variant reads
* the V3 protocol's tuple representation.
*
* @return tuple from the back end
* @throws IOException if a data I/O error occurs
* @throws SQLException if read more bytes than set maxResultBuffer
*/
public Tuple receiveTupleV3() throws IOException, OutOfMemoryError, SQLException {
int messageSize = receiveInteger4(); // MESSAGE SIZE
int nf = receiveInteger2();
//size = messageSize - 4 bytes of message size - 2 bytes of field count - 4 bytes for each column length
int dataToReadSize = messageSize - 4 - 2 - 4 * nf;
byte[][] answer = new byte[nf][];
increaseByteCounter(dataToReadSize);
OutOfMemoryError oom = null;
for (int i = 0; i < nf; ++i) {
int size = receiveInteger4();
if (size != -1) {
try {
answer[i] = new byte[size];
receive(answer[i], 0, size);
} catch (OutOfMemoryError oome) {
oom = oome;
skip(size);
}
}
}
if (oom != null) {
throw oom;
}
return new Tuple(answer);
}
/**
* Reads in a given number of bytes from the backend.
*
* @param siz number of bytes to read
* @return array of bytes received
* @throws IOException if a data I/O error occurs
*/
public byte[] receive(int siz) throws IOException {
try {
byte[] answer = new byte[siz];
receive(answer, 0, siz);
return answer;
} catch (SocketTimeoutException e) {
throw new IOException(e);
}
}
/**
* Reads in a given number of bytes from the backend.
*
* @param buf buffer to store result
* @param off offset in buffer
* @param siz number of bytes to read
* @throws IOException if a data I/O error occurs
*/
public void receive(byte[] buf, int off, int siz) throws IOException {
int s = 0;
while (s < siz) {
int w = pgInput.read(buf, off + s, siz - s);
if (w < 0) {
throw new EOFException();
}
s += w;
}
}
public void skip(int size) throws IOException {
long s = 0;
while (s < size) {
s += pgInput.skip(size - s);
}
}
/**
* Copy data from an input stream to the connection.
*
* @param inStream the stream to read data from
* @param remaining the number of bytes to copy
* @throws IOException if a data I/O error occurs
*/
public void sendStream(InputStream inStream, int remaining) throws IOException {
int expectedLength = remaining;
if (streamBuffer == null) {
streamBuffer = new byte[8192];
}
while (remaining > 0) {
int count = (remaining > streamBuffer.length ? streamBuffer.length : remaining);
int readCount;
try {
readCount = inStream.read(streamBuffer, 0, count);
if (readCount < 0) {
throw new EOFException(
GT.tr("Premature end of input stream, expected {0} bytes, but only read {1}.",
expectedLength, expectedLength - remaining));
}
} catch (IOException ioe) {
while (remaining > 0) {
send(streamBuffer, count);
remaining -= count;
count = (remaining > streamBuffer.length ? streamBuffer.length : remaining);
}
throw new PGBindException(ioe);
}
send(streamBuffer, readCount);
remaining -= readCount;
}
}
/**
* Flush any pending output to the backend.
*
* @throws IOException if an I/O error occurs
*/
@Override
public void flush() throws IOException {
if (encodingWriter != null) {
encodingWriter.flush();
}
pgOutput.flush();
}
/**
* Consume an expected EOF from the backend.
*
* @throws IOException if an I/O error occurs
* @throws SQLException if we get something other than an EOF
*/
public void receiveEOF() throws SQLException, IOException {
int c = pgInput.read();
if (c < 0) {
return;
}
throw new PSQLException(GT.tr("Expected an EOF from server, got: {0}", c),
PSQLState.COMMUNICATION_ERROR);
}
/**
* Closes the connection.
*
* @throws IOException if an I/O Error occurs
*/
@Override
public void close() throws IOException {
if (encodingWriter != null) {
encodingWriter.close();
}
pgOutput.close();
pgInput.close();
connection.close();
}
public void setNetworkTimeout(int milliseconds) throws IOException {
connection.setSoTimeout(milliseconds);
pgInput.setTimeoutRequested(milliseconds != 0);
}
public int getNetworkTimeout() throws IOException {
return connection.getSoTimeout();
}
/**
* Method to set MaxResultBuffer inside PGStream.
*
* @param value value of new max result buffer as string (cause we can expect % or chars to use
* multiplier)
* @throws PSQLException exception returned when occurred parsing problem.
*/
public void setMaxResultBuffer(@Nullable String value) throws PSQLException {
maxResultBuffer = PGPropertyMaxResultBufferParser.parseProperty(value);
}
/**
* Method to clear count of byte buffer.
*/
public void clearResultBufferCount() {
resultBufferByteCount = 0;
}
/**
* Method to increase actual count of buffer. If buffer count is bigger than max result buffer
* limit, then gonna return an exception.
*
* @param value size of bytes to add to byte buffer.
* @throws SQLException exception returned when result buffer count is bigger than max result
* buffer.
*/
private void increaseByteCounter(long value) throws SQLException {
if (maxResultBuffer != -1) {
resultBufferByteCount += value;
if (resultBufferByteCount > maxResultBuffer) {
throw new PSQLException(GT.tr(
"Result set exceeded maxResultBuffer limit. Received: {0}; Current limit: {1}",
String.valueOf(resultBufferByteCount), String.valueOf(maxResultBuffer)),PSQLState.COMMUNICATION_ERROR);
}
}
}
public boolean isClosed() {
return connection.isClosed();
}
}