/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed 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.
*
* Initial developer(s): Csaba Simon
* Contributor(s): Gilles Rayrat, Robert Hodges
*/
package com.continuent.tungsten.common.mysql;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.SocketTimeoutException;
import java.sql.Date;
import java.sql.Time;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.continuent.tungsten.common.io.WrappedInputStream;
/**
* A MySQL packet with helper functions to ease the reading and writting of
* bytes, integers, long integers, strings...
*
* @author <a href="mailto:csaba.simon@continuent.com">Csaba Simon</a>
* @author <a href="gilles.rayrat@continuent.com">Gilles Rayrat</a>
* @version $Id: $
*/
public class MySQLPacket
{
/** Length of the packet header */
public static final int HEADER_LENGTH = 4;
/** Maximum packet length (16M) */
public static final int MAX_LENGTH = 0x00ffffff;
/** Packet type is a single byte at this offset */
public static final int PACKET_TYPE_OFFSET = HEADER_LENGTH;
/** EOF warning count appears as a short starting at this offset */
public static final int EOF_WARNING_COUNT_OFFSET = PACKET_TYPE_OFFSET + 1;
/** EOF packet server status appears as a short at this offset */
public static final int EOF_SERVER_STATUS_OFFSET = EOF_WARNING_COUNT_OFFSET + 2;
/** MySQL Errorno appears as a short at this offset */
public static final int ERROR_NUMBER_OFFSET = PACKET_TYPE_OFFSET + 1;
/** ODBC/JDBC SQL state appears as a byte at this offset */
public static final int ERROR_SQL_STATE_OFFSET = ERROR_NUMBER_OFFSET + 2;
/** ODBC/JDBC SQL state appears as a byte at this offset */
public static final int ERROR_MESSAGE_OFFSET = ERROR_SQL_STATE_OFFSET + 1;
private static final long NULL_LENGTH = -1;
private static final Logger logger = Logger.getLogger(MySQLPacket.class);
/** Header + data buffer */
private byte[] byteBuffer;
/** Cursor position */
private int position;
/** Data size */
private int dataLength = 0;
/** The input stream used to read this packet */
InputStream inputStream = null;
/**
* Creates a new <code>MySQLPacket</code> object
*
* @param buffer the buffer
* @param packetNumber the packet number
*/
public MySQLPacket(int dataLength, byte[] buffer, byte packetNumber)
{
this.byteBuffer = buffer;
this.byteBuffer[3] = packetNumber;
this.dataLength = dataLength;
this.position = HEADER_LENGTH;
}
/**
* Creates a new <code>MySQLPacket</code> object
*
* @param size the size of the buffer
* @param packetNumber the packet number
*/
public MySQLPacket(int size, byte packetNumber)
{
if (size < HEADER_LENGTH)
{
this.byteBuffer = new byte[HEADER_LENGTH + size];
}
else
{
this.byteBuffer = new byte[size];
}
this.byteBuffer[3] = packetNumber;
this.position = HEADER_LENGTH;
}
/**
* Reads a MySQL packet from the input stream.
*
* @param in the data input stream from where we read the MySQL packet
* @param dropLargePackets whether or not to return null when a packet of
* 16Mb is read
* @return a MySQLPacket object or null if the MySQL packet cannot be read
*/
public static MySQLPacket readPacket(InputStream in,
boolean dropLargePackets)
{
try
{
return mysqlReadPacket(in, dropLargePackets);
}
catch (SocketTimeoutException e)
{
logger.warn("Socket timeout expired, closing connection");
}
catch (IOException e)
{
logger.error("I/O error while reading from socket");
}
return null;
}
public static MySQLPacket mysqlReadPacket(InputStream in,
boolean dropLargePackets) throws IOException, EOFException
{
int mask = 0xff;
int packetLen1 = in.read();
int packetLen2 = in.read();
int packetLen3 = in.read();
int packetLen = (packetLen1 & mask) | (packetLen2 & mask) << 8
| (packetLen3 & mask) << 16;
int packetNumber = in.read();
// This is ok, no more packet on the line
if (packetLen1 == -1)
{
logger.debug("Reached end of input stream while reading packet");
return null;
}
// This is bad, client went away
if (packetLen2 == -1 || packetLen3 == -1 || packetNumber == -1)
{
throw new EOFException("Reached end of input stream.");
}
if (dropLargePackets && (packetLen == MAX_LENGTH || packetLen == 0))
{
logger.error("Received packet of size " + packetLen
+ ", but packets bigger than 16 MB are not supported yet!");
return null;
}
// read the body of the packet
byte[] packetData = new byte[packetLen + HEADER_LENGTH];
// copy header
packetData[0] = (byte) packetLen1;
packetData[1] = (byte) packetLen2;
packetData[2] = (byte) packetLen3;
packetData[3] = (byte) packetNumber;
// read() returns the number of actual bytes read, which might be
// less that the desired length this loop ensures that the whole
// packet is read
int n = 0;
while (n < packetLen)
{
int count = in.read(packetData, HEADER_LENGTH + n, packetLen - n);
if (count < 0)
{
throw new EOFException("Reached end of input stream.");
}
n += count;
}
MySQLPacket p = new MySQLPacket(packetLen, packetData,
(byte) packetNumber);
p.setInputStream(in);
return p;
}
/**
* Reads a MySQL packet from the input stream.
*
* @param in the data input stream from where we read the MySQL packet
* @param timeoutMillis Number of milliseconds we will pause while waiting
* for data from the the network during a packet.
* @return a MySQLPacket object or null if the MySQL packet cannot be read
*/
public static MySQLPacket readPacket(InputStream in, long timeoutMillis)
{
try
{
int mask = 0xff;
int packetLen1 = in.read();
int packetLen2 = in.read();
int packetLen3 = in.read();
int packetLen = (packetLen1 & mask) | (packetLen2 & mask) << 8
| (packetLen3 & mask) << 16;
int packetNumber = in.read();
// This is ok, no more packet on the line
if (packetLen1 == -1)
{
logger.debug("Reached end of input stream while reading packet");
return null;
}
// This is bad, client went away
if (packetLen2 == -1 || packetLen3 == -1 || packetNumber == -1)
{
throw new EOFException("Reached end of input stream.");
}
// read the body of the packet
byte[] packetData = new byte[packetLen + HEADER_LENGTH];
// copy header
packetData[0] = (byte) packetLen1;
packetData[1] = (byte) packetLen2;
packetData[2] = (byte) packetLen3;
packetData[3] = (byte) packetNumber;
// See if we can trust the availability from this stream.
boolean deterministicAvailability = true;
if (in instanceof WrappedInputStream)
{
deterministicAvailability = ((WrappedInputStream) in)
.isDeterministic();
}
// read() returns the number of actual bytes read, which might be
// less that the desired length this loop ensures that the whole
// packet is read
int n = 0;
while (n < packetLen)
{
// Issue 281. Wait until at least one byte is available to avoid
// a possible out of data condition when reading from the
// network.
if (deterministicAvailability && in.available() == 0)
{
long readStartTime = System.currentTimeMillis();
long delay = -1;
// Sleep for up to timeout.
while (in.available() == 0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
return null;
}
delay = System.currentTimeMillis() - readStartTime;
if (delay > timeoutMillis)
{
break;
}
}
// Note the delay if longer than 10% of the timeout. This
// is helpful for diagnosing failures.
if (delay > (timeoutMillis / 10))
{
logger.info("Paused to allow packet data to appear on the network: delay="
+ (delay / 1000.0)
+ " timeout="
+ (timeoutMillis / 1000.0)
+ " packetNumber="
+ packetNumber
+ " packetlen="
+ packetLen
+ " bytesRead=" + n);
}
}
// Now read data.
int count = in.read(packetData, HEADER_LENGTH + n, packetLen
- n);
if (count < 0)
{
throw new EOFException(
"Reached end of input stream: packetNumber="
+ packetNumber + " packetlen=" + packetLen
+ " bytesRead=" + n);
}
n += count;
}
MySQLPacket p = new MySQLPacket(packetLen, packetData,
(byte) packetNumber);
p.setInputStream(in);
return p;
}
catch (SocketTimeoutException e)
{
logger.warn("Socket timeout expired, closing connection");
}
catch (IOException e)
{
logger.error("I/O error while reading from client socket", e);
}
return null;
}
/**
* Reads a MySQL packet from the input stream using a default partial read
* timeout of 5 seconds.
*
* @param in the data input stream from where we read the MySQL packet
* @return a MySQLPacket object or null if the MySQL packet cannot be read
*/
public static MySQLPacket readPacket(InputStream in)
{
return readPacket(in, 10000);
}
/**
* Returns the raw byte buffer.
*/
public byte[] getByteBuffer()
{
return byteBuffer;
}
public void setByteBuffer(byte[] newByteBuffer)
{
this.byteBuffer = newByteBuffer;
this.dataLength = newByteBuffer.length - HEADER_LENGTH;
}
/**
* Returns the packet number.
*
* @return the packet number
*/
public byte getPacketNumber()
{
return this.byteBuffer[3];
}
/**
* Gets the current length of the byteBuffer
*
* @return the byte buffer length
*/
public int getDataLength()
{
return this.dataLength;
}
/**
* Whether or not this packet is a large packet, thus part of a large query
* or result
*/
public boolean isLargePacket()
{
return getDataLength() == MySQLPacket.MAX_LENGTH;
}
/**
* Empty packets have a zero size data
*
* @return true if the data length is zero
*/
public boolean isEmpty()
{
return getDataLength() == 0;
}
/**
* Retrieves the current byte buffer cursor position
*
* @return the position field
*/
public int getPacketPosition()
{
return this.position;
}
/**
* Changes the byte buffer cursor position
*
* @param pos the new position
*/
public void setPacketPosition(int pos)
{
this.position = pos;
}
/**
* Returns the MySQL error number from the MySQL "ERROR" packet without
* modifying the packet
*
* @return int as value of error
*/
public int peekErrorErrno()
{
return (byteBuffer[ERROR_NUMBER_OFFSET] & 0xff)
| ((byteBuffer[ERROR_NUMBER_OFFSET + 1] & 0xff) << 8);
}
/**
* Returns the MySQL error number from the MySQL "ERROR" packet without
* modifying the packet
*
* @return int as value of error
*/
public int peekErrorSQLState()
{
return byteBuffer[ERROR_SQL_STATE_OFFSET];
}
/**
* Returns the MySQL error message from the MySQL "ERROR" packet without
* modifying the packet
*
* @return String with error message
*/
public String peekErrorErrorMessage()
{
return peekStringAtOffset(ERROR_MESSAGE_OFFSET);
}
/**
* Returns the bit mask for the server status from the MySQL "EOF" packet
*
* @return int as value of EOF server status
*/
public int peekEOFServerStatus()
{
if (isEOF() && getDataLength() >= 5)
{
return (byteBuffer[EOF_SERVER_STATUS_OFFSET] & 0xff)
| ((byteBuffer[EOF_SERVER_STATUS_OFFSET + 1] & 0xff) << 8);
}
return -1;
}
/**
* Returns the warning count from the MySQL "EOF" packet
*
* @return int as value of EOF warning count
*/
public int peekEOFWarningCount()
{
if (isEOF() && getDataLength() >= 3)
{
return (byteBuffer[EOF_WARNING_COUNT_OFFSET] & 0xff)
| ((byteBuffer[EOF_WARNING_COUNT_OFFSET + 1] & 0xff) << 8);
}
return -1;
}
/**
* When streaming a packet, some data has possibly been already read in it.
* In order to send it, we need to scroll the cursor to the last data byte
* position
*/
public void preparePacketForStreaming()
{
setPacketPosition(getDataLength() + HEADER_LENGTH);
}
/**
* Whether or not this packet is a MySQL "OK" packet
*
* @return true if this packet is a OK packet
*/
public boolean isOK()
{
return (this.byteBuffer[4] == (byte) 0x00) && getDataLength() > 3;
}
/**
* Whether or not this packet is a MySQL "ERROR" packet
*
* @return true if this packet is a ERROR packet
*/
public boolean isError()
{
return this.byteBuffer[4] == (byte) 0xFF;
}
/**
* Whether or not this packet is a MySQL "EOF" packet
*
* @return true if this packet is a EOF packet
*/
public boolean isEOF()
{
// An EOF packet is composed of:
// 1byte 0xFE - the EOF header
// plus, if protocol >= 4.1:
// 2bytes - warningCount
// 2bytes - status flags
// So the data is never larger than 5 bytes
return this.byteBuffer[4] == (byte) 0xFE && this.getDataLength() <= 5;
}
/**
* Whether or not this packet has the SERVER_STATUS_IN_TRANS flag. See:
* http://dev.mysql.com/doc/internals/en/status-flags.html
*
* @return true if it has. flase otherwise
*/
public boolean isSERVER_STATUS_IN_TRANS()
{
return this.isServerFlagSet(MySQLConstants.SERVER_STATUS_IN_TRANS);
}
/**
* Whether or not this packet has the SERVER_STATUS_AUTOCOMMIT flag. See:
* http://dev.mysql.com/doc/internals/en/status-flags.html
*
* @return true if it has. flase otherwise
*/
public boolean isSERVER_STATUS_AUTOCOMMIT()
{
return this.isServerFlagSet(MySQLConstants.SERVER_STATUS_AUTOCOMMIT);
}
/**
* Tests "server status" bytes in a OK or EOF packet to tell whether or not
* the given flag is set. A warning will be printed if the packet is neither
* an EOF nor a OK, and false will be returned
*
* @return true if and only if the packet is OK or EOF packet and if the
* given flag is set in the server status bytes.
*/
public boolean isServerFlagSet(short flag)
{
boolean flagSet = false;
int originalPos = position;
// we need to position the cursor (position right before the server
// flag. This is different between a OK and a EOF
if (isOK())
{
reset();
getByte(); // always 0, that's a OK packet
getFieldLength(); // affectedRows
getFieldLength(); // insertId
}
else if (isEOF())
{
// here, the HEADER_LENGTH + 3 stands for:
// 4 bytes header (HEADER_LENGTH)
// 1 byte EOF marker (HEADER_LENGTH + 1)
// 2 bytes warningCount (HEADER_LENGTH + 3)
position = HEADER_LENGTH + 3;
}
else
{
logger.warn("Probable bug here: testing server status on a packet that's neither EOF nor OK. "
+ this.toString());
}
// next 2 bytes are the serverStatus
flagSet = (getShort() & flag) != 0;
position = originalPos;
return flagSet;
}
/**
* Tells whether or not more results exist on the server.
*
* @return true if and only if the packet is a OK or EOF packet and that
* SERVER_MORE_RESULTS_EXISTS is set in the server status bytes.
*/
public boolean hasMoreResults()
{
return isServerFlagSet(MySQLConstants.SERVER_MORE_RESULTS_EXISTS);
}
/**
* Tells whether or not a cursor exists.
*
* @return true if and only if the packet is a OK or EOF packet and that
* SERVER_STATUS_CURSOR_EXISTS is set in the server status bytes.
*/
public boolean hasCursor()
{
return isServerFlagSet(MySQLConstants.SERVER_STATUS_CURSOR_EXISTS);
}
/**
* Tells whether the end of a result set has been reached upon
* COM_STMT_FETCH command
*
* @return true if and only if the packet is a OK or EOF packet and that
* SERVER_STATUS_LAST_ROW_SENT is set in the server status bytes.
*/
public boolean hasLastRowSent()
{
return isServerFlagSet(MySQLConstants.SERVER_STATUS_LAST_ROW_SENT);
}
public long peekFieldCount()
{
int originalPosition = position;
reset();
// Get the column count so we know where the column list finishes
long fieldCount = getFieldLength();
position = originalPosition;
return fieldCount;
}
/**
* Returns the number of bytes remaining in the buffer
*
* @return remaining bytes to read
*/
public int getRemainingBytes()
{
return this.byteBuffer.length - this.position;
}
/**
* Returns one byte from the buffer.
*
* @return one byte from the buffer
*/
public byte getByte()
{
return this.byteBuffer[this.position++];
}
/**
* Reads one byte from the buffer considering it as an unsigned value and
* returns a corresponding short
*
* @return one byte from the buffer
*/
public short getUnsignedByte()
{
int firstByte = this.byteBuffer[this.position++] & 0xff;
short ret = (short) firstByte;
return ret;
}
/**
* Returns len bytes from the buffer.
*
* @param len the number of bytes to return
* @return len bytes from the buffer
*/
public byte[] getBytes(int len)
{
byte[] b = new byte[len];
System.arraycopy(this.byteBuffer, this.position, b, 0, len);
this.position += len;
return b;
}
/**
* Aka. getUnsignedInt16(), returns two consecutive bytes from the buffer
* considering them as an unsigned value
*
* @return an integer corresponding to 2 buffer bytes treated as an unsigned
* value
*/
public int getUnsignedShort()
{
byte[] b = this.byteBuffer;
return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8);
}
/**
* Aka. getInt16(), returns two consecutive bytes from the buffer
* considering them as signed value<br>
*
* @return an integer corresponding to the 2 bytes treated as a signed value
*/
public short getShort()
{
byte[] b = this.byteBuffer;
byte firstbyte = b[this.position++];
byte secondbyte = b[this.position++];
return (short) ((secondbyte << 8) | firstbyte & 0xff);
}
/**
* Returns a len encoded byte array from the buffer.
*
* @return a len encoded byte array from the buffer
*/
public byte[] getLenEncodedBytes()
{
long len = this.readFieldLength();
if (len == NULL_LENGTH)
{
return null;
}
if (len == 0)
{
return new byte[0];
}
return getBytes((int) len);
}
/**
* Returns four consecutive bytes as an integer (MySQL long) from the
* buffer.
*
* @return four consecutive bytes as an integer (MySQL long) from the buffer
*/
public int getInt32()
{
byte[] b = this.byteBuffer;
return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8)
| ((b[this.position++] & 0xff) << 16)
| ((b[this.position++] & 0xff) << 24);
}
/**
* Returns four consecutive bytes as an integer (MySQL long) from the
* buffer.
*
* @return an integer (MySQL long) based from the buffer
*/
public long getUnsignedInt32()
{
byte[] b = this.byteBuffer;
long firstbyte = b[this.position++] & 0xFF;
long secondbyte = b[this.position++] & 0xFF;
long thirdbyte = b[this.position++] & 0xFF;
long forthbyte = b[this.position++] & 0xFF;
return (long) (forthbyte << 24 | thirdbyte << 16 | secondbyte << 8 | firstbyte) & 0xFFFFFFFFL;
}
/**
* Returns three consecutive bytes as an integer (MySQL longint) from the
* buffer considering it as an unsigned.
*
* @return an integer (MySQL longint) from the buffer treated as an unsigned
*/
public int getUnsignedInt24()
{
byte[] b = this.byteBuffer;
return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8)
| ((b[this.position++] & 0xff) << 16);
}
/**
* Returns three consecutive bytes as an integer (MySQL longint) from the
* buffer.
*
* @return three consecutive bytes as an integer (MySQL longint) from the
* buffer.
*/
public int getInt24()
{
byte[] b = this.byteBuffer;
return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8)
| ((b[this.position++]) << 16);
}
/**
* Returns eight consecutive bytes as a long (MySQL longlong) from the
* buffer.
*
* @return eight consecutive bytes as a long (MySQL longlong) from the
* buffer
*/
public long getLong()
{
byte[] b = this.byteBuffer;
return (b[this.position++] & 0xff)
| ((long) (b[this.position++] & 0xff) << 8)
| ((long) (b[this.position++] & 0xff) << 16)
| ((long) (b[this.position++] & 0xff) << 24)
| ((long) (b[this.position++] & 0xff) << 32)
| ((long) (b[this.position++] & 0xff) << 40)
| ((long) (b[this.position++] & 0xff) << 48)
| ((long) (b[this.position++] & 0xff) << 56);
}
/**
* Returns eight consecutive bytes from the buffer (MySQL longlong) treated
* as unsigned value into a big integer .
*
* @return the corresponding bigInteger
*/
public BigInteger getUnsignedLong()
{
// create a byte array that BigInteger can read
byte[] byteArray = new byte[8];
int sign = 0; // will become 1 if the number is non-zero
// invert endianess and check if number is zero
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
{
// put most significant by first
byteArray[byteIndex] = (byte) (this.byteBuffer[this.position + 7
- byteIndex] & 0xff);
// make positive if one of the bits is not zero (done only once)
if (sign == 0 && byteArray[byteIndex] != 0)
sign = 1;
}
this.position += 8;
BigInteger ret = new BigInteger(sign, byteArray);
return ret;
}
/**
* Returns four bytes from the buffer as a float
*/
public float getFloat()
{
return Float.intBitsToFloat(getInt32());
}
/**
* Returns eight bytes from the buffer as a float
*/
public double getDouble()
{
return Double.longBitsToDouble(getLong());
}
/**
* Returns a string from the buffer.
*
* @return a string from the buffer
*/
public String getString()
{
int i = this.position;
int len = 0;
int maxLen = this.byteBuffer.length;
while ((i < maxLen) && (this.byteBuffer[i] != 0))
{
len++;
i++;
}
return getString(len);
}
/**
* Returns a string from the buffer.
*
* @return a string from the buffer
*/
public String peekStringAtOffset(int startPosition)
{
int i = startPosition;
int len = 0;
int maxLen = this.byteBuffer.length;
while ((i < maxLen) && (this.byteBuffer[i] != 0))
{
len++;
i++;
}
return peekString(startPosition, len);
}
/**
* Returns a len length string from the buffer.
*
* @param len the length of the string
* @return a len length string from the buffer
*/
public String peekString(int offset, int len)
{
int maxLen = this.byteBuffer.length - this.position;
String s = new String(this.byteBuffer, offset, len < maxLen
? len
: maxLen);
return s;
}
/**
* Returns a len length string from the buffer.
*
* @param len the length of the string
* @return a len length string from the buffer
*/
public String getString(int len)
{
int maxLen = this.byteBuffer.length - this.position;
String s = new String(this.byteBuffer, this.position, len < maxLen
? len
: maxLen);
this.position += (len + 1);
return s;
}
/**
* Returns a string from the buffer, where the encoded size is right before
* the actual string
*
* @param stopAtNullChar whether or not to consider the string ends with the
* null character. When false, the string length will always be
* the decoded length
* @return string with decoded length
*/
public String getLenEncodedString(boolean stopAtNullChar)
{
int leftBytes = getRemainingBytes();
if (leftBytes < 1)
return "";
long announcedLength = readFieldLength();
if (announcedLength > Integer.MAX_VALUE)
logger.warn("String length bug here!");
// make sure client did not give a buggy length
if (announcedLength > leftBytes)
announcedLength = leftBytes;
int actualLength = (int) announcedLength;
if (stopAtNullChar)
{
int i = this.position;
int untilNullCharLen = 0;
while ((untilNullCharLen < announcedLength)
&& (this.byteBuffer[i] != 0))
{
untilNullCharLen++;
i++;
}
actualLength = untilNullCharLen;
}
String s = new String(this.byteBuffer, this.position, actualLength);
// even if we constructed a smaller string, we have to discard what's
// after the null char
this.position += announcedLength;
return s;
}
/**
* Reads a time from stream given its length and returns the corresponding
* {@link Time}
*
* @param length size of the time data
* @return jdbc Time decoded from stream
*/
public Time getTime(int length)
{
long millis = 0;
boolean neg = false;
if (length >= 8)
{
neg = (getByte() != 0);
if (logger.isTraceEnabled())
logger.trace("neg=" + neg);
int day = getInt32();
if (logger.isTraceEnabled())
logger.trace("day=" + day);
millis = getHourMinSec();
if (length > 8)
{
int mil = getInt32();
if (logger.isTraceEnabled())
logger.trace("millis =" + mil % 1000);
// MySQL ignores millis > 1000 (don't add the corresponding
// seconds) => %1000
millis += mil % 1000;
}
}
return new Time(neg ? -millis : millis);
}
/**
* Reads hours, minutes and seconds from stream and returns the
* corresponding number of milliseconds
*
* @return milliseconds decoded from stream
*/
public long getHourMinSec()
{
int hour = getByte();
if (logger.isTraceEnabled())
logger.trace("hour=" + hour);
int min = getByte();
if (logger.isTraceEnabled())
logger.trace("min=" + min);
int sec = getByte();
if (logger.isTraceEnabled())
logger.trace("sec=" + sec);
return sec * 1000 + min * 60 * 1000 + hour * 60 * 60 * 1000;
}
/**
* Reads a date from stream given its length and returns the corresponding
* {@link Date}
*
* @param length size of the date data
* @return jdbc Date decoded from stream
*/
public Date getDate(int length)
{
int year = 0;
int month = 0;
int day = 0;
if (length >= 4)
{
year = getUnsignedShort();
if (logger.isTraceEnabled())
logger.trace("year=" + year);
month = getByte();
if (logger.isTraceEnabled())
logger.trace("month=" + month);
day = getByte();
if (logger.isTraceEnabled())
logger.trace("day=" + day);
}
// This is the easier way, probably not the fastest
String date = "" + year + "-" + month + "-" + day;
return Date.valueOf(date);
}
/**
* Put a byte in the buffer.
*
* @param b the byte to put in the buffer
*/
public void putByte(byte b)
{
ensureCapacity(1);
this.byteBuffer[this.position++] = b;
}
/**
* Put an array of bytes in the buffer.
*
* @param bytes the byte array
*/
public void putBytes(byte[] bytes)
{
ensureCapacity(bytes.length);
System.arraycopy(bytes, 0, this.byteBuffer, this.position, bytes.length);
this.position += bytes.length;
}
/**
* Put a long as a len encoded value in the buffer.
*
* @param length the value to put in the buffer
*/
public void putFieldLength(long length)
{
if (length < 251)
{
putByte((byte) length);
}
else if (length < 65536L)
{
ensureCapacity(3);
putByte((byte) 252);
putInt16((int) length);
}
else if (length < 16777216L)
{
ensureCapacity(4);
putByte((byte) 253);
putInt24((int) length);
}
else
{
ensureCapacity(9);
putByte((byte) 254);
putLong(length);
}
}
public long getFieldLength()
{
if (this.position > this.byteBuffer.length)
{
return 0;
}
byte encoding = getByte();
if ((encoding & 0xff) < 251)
{
return (long) encoding & 0xff;
}
if ((encoding & 0xff) == 251)
{
return -1;
}
if ((encoding & 0xff) == 252)
{
return (long) getShort() & 0xffff;
}
if ((encoding & 0xff) == 253)
{
return (long) getInt24() & 0xffffff;
}
if ((encoding & 0xff) == 254)
{
return getLong();
}
return 0;
}
/**
* Put an integer in the buffer.
*
* @param i the integer to put in the buffer
*/
public void putInt16(int i)
{
ensureCapacity(2);
byte[] b = this.byteBuffer;
b[this.position++] = (byte) (i & 0xff);
b[this.position++] = (byte) (i >>> 8);
}
/**
* Put a byte array as a len encoded bytes in the buffer.
*
* @param bytes the byte array to put in the buffer
*/
public void putLenBytes(byte[] bytes)
{
ensureCapacity(9 + bytes.length);
putFieldLength(bytes.length);
System.arraycopy(bytes, 0, this.byteBuffer, this.position, bytes.length);
this.position += bytes.length;
}
/**
* Put a string as a len encoded bytes in the buffer.
*
* @param s the string to put in the buffer
*/
public void putLenString(String s)
{
if (s == null)
{
putFieldLength(0);
}
else
{
byte[] b = s.getBytes();
ensureCapacity(9 + b.length);
putFieldLength(b.length);
System.arraycopy(b, 0, this.byteBuffer, this.position, b.length);
this.position += b.length;
}
}
/**
* Put an integer (MySQL long) in the buffer.
*
* @param i the value to put in the buffer
*/
public void putInt32(int i)
{
ensureCapacity(4);
byte[] b = this.byteBuffer;
b[this.position++] = (byte) (i & 0xff);
b[this.position++] = (byte) (i >>> 8);
b[this.position++] = (byte) (i >>> 16);
b[this.position++] = (byte) (i >>> 24);
}
/**
* Put an unsigned integer (MySQL long) in the buffer.
*
* @param i the value to put in the buffer
*/
public void putUnsignedInt32(long i)
{
ensureCapacity(4);
byte[] b = this.byteBuffer;
b[this.position++] = (byte) (i & 0xff);
b[this.position++] = (byte) (i >>> 8);
b[this.position++] = (byte) (i >>> 16);
b[this.position++] = (byte) (i >>> 24);
}
/**
* Put an integer (MySQL longint) in the buffer
*
* @param i the value to put in the buffer
*/
public void putInt24(int i)
{
ensureCapacity(3);
byte[] b = this.byteBuffer;
b[this.position++] = (byte) (i & 0xff);
b[this.position++] = (byte) (i >>> 8);
b[this.position++] = (byte) (i >>> 16);
}
/**
* Put a long (MySQL longlong) in the buffer.
*
* @param i the value to put in the buffer
*/
public void putLong(long i)
{
ensureCapacity(8);
byte[] b = this.byteBuffer;
b[this.position++] = (byte) (i & 0xff);
b[this.position++] = (byte) (i >>> 8);
b[this.position++] = (byte) (i >>> 16);
b[this.position++] = (byte) (i >>> 24);
b[this.position++] = (byte) (i >>> 32);
b[this.position++] = (byte) (i >>> 40);
b[this.position++] = (byte) (i >>> 48);
b[this.position++] = (byte) (i >>> 56);
}
/**
* Puts a float in the buffer
*/
public void putFloat(float f)
{
putInt32(Float.floatToIntBits(f));
}
/**
* Puts a double in the buffer
*/
public void putDouble(double d)
{
putLong(Double.doubleToLongBits(d));
}
/**
* Put a string in the buffer.
*
* @param s the value to put in the buffer
*/
public void putString(String s)
{
ensureCapacity((s.length() * 2) + 1);
System.arraycopy(s.getBytes(), 0, this.byteBuffer, this.position,
s.length());
this.position += s.length();
this.byteBuffer[this.position++] = 0;
}
/**
* Put a string in the buffer. No null terminated.
*
* @param s the value to put in the buffer
*/
public void putStringNoNull(String s)
{
ensureCapacity(s.length() * 2);
System.arraycopy(s.getBytes(), 0, this.byteBuffer, this.position,
s.length());
this.position += s.length();
}
/**
* Converts a time in millis to hours, minutes and seconds and put it in the
* buffer
*
* @param millis milliseconds since EPOCH
* @return the actual number of millis written (without the millis info)
*/
public long putHourMinSec(long millis)
{
long hours = 0, minutes = 0, seconds = 0;
hours = millis / (1000 * 60 * 60);
minutes = (millis - (hours * 1000 * 60 * 60)) / (1000 * 60);
seconds = (millis - (hours * 1000 * 60 * 60) - (minutes * 1000 * 60)) / 1000;
putByte((byte) hours);
putByte((byte) minutes);
putByte((byte) seconds);
return millis - millis % 1000;
}
/**
* Puts a jdbc {@link java.sql.Time} in the buffer as:<br>
* Send sign (0 for > EPOCH, 1 for < EPOCH)<br>
* Then day (always zero for a time)<br>
* Finally Hour Minutes and Seconds using
* {@link MySQLPacket#putHourMinSec(long)}<br>
* Note that MySQL jdbc driver doesn't care about days and millis, so it is
* most probably unnecessary to send them...
*
* @param t the Time object to write
*/
public void putTime(Time t)
{
// guess sign and normalize millis
long millis = t.getTime();
if (millis < 0)
{
putByte((byte) 1);
millis = -millis;
}
else
putByte((byte) 0);
// Send a fake day
putInt32(0);
// Send the 3 bytes HHMMSS
putHourMinSec(millis);
// Don't send millis
}
/**
* Puts a {@link Date} to the buffer as:<br>
* two bytes year one byte month one byte day
*
* @param d jdbc date to write
* @return the rounded date as milliseconds actually put (without hh mm ss
* ms information)
*/
public long putDate(Date d)
{
Calendar fullDate = new GregorianCalendar();
fullDate.clear();
fullDate.setTimeInMillis(d.getTime());
// MYO-100: We need to return the remaining millis for the date only,
// not for its HHMMSSMS
Calendar yymmddOnly = new GregorianCalendar(
fullDate.get(Calendar.YEAR), fullDate.get(Calendar.MONTH),
fullDate.get(Calendar.DAY_OF_MONTH));
// Note: Supported range for MySQL date is '1000-01-01' to '9999-12-31'.
// So we don't care about era field (BC/AD)
putInt16(yymmddOnly.get(Calendar.YEAR));
// 1-based (ie. 1 for January)
putByte((byte) (1 + yymmddOnly.get(Calendar.MONTH)));
// 1-based (ie. 1 for 1st of the month)
putByte((byte) yymmddOnly.get(Calendar.DAY_OF_MONTH));
return yymmddOnly.getTimeInMillis();
}
/**
* Rewinds data buffer pointer to the first data byte.
*/
public void reset()
{
this.position = HEADER_LENGTH;
}
/**
* Write out the content of the buffer to the output stream.
*
* @param out the output stream
* @throws IOException if an error happens when writting to the output
* stream
*/
public void write(OutputStream out) throws IOException
{
int len = this.position - HEADER_LENGTH;
// handle packets bigger than 16 MB
// for now only we return an error message
if (len >= 256 * 256 * 256)
{
String message = "Trying to send packet of size " + len
+ ", packets bigger than 16 MB are not supported yet!";
logger.error(message);
throw new IOException(message);
}
byte[] b = this.byteBuffer;
b[0] = (byte) (len & 0xff);
b[1] = (byte) (len >>> 8);
b[2] = (byte) (len >>> 16);
out.write(b, 0, this.position);
}
/**
* Ensure that there are additionalData bytes available in the buffer. If
* not realocate the buffer.
*
* @param additionalData the number of bytes what need to be available
*/
private void ensureCapacity(int additionalData)
{
if ((this.position + additionalData) > this.byteBuffer.length)
{
int newLength = (int) (this.byteBuffer.length * 1.25);
if (newLength < (this.byteBuffer.length + additionalData))
{
newLength = this.byteBuffer.length
+ (int) (additionalData * 1.25);
}
if (newLength < this.byteBuffer.length)
{
newLength = this.byteBuffer.length + additionalData;
}
byte[] newBytes = new byte[newLength];
System.arraycopy(this.byteBuffer, 0, newBytes, 0,
this.byteBuffer.length);
this.byteBuffer = newBytes;
}
}
/**
* Returns the len encoded value as a long from buffer.
*
* @return the len encoded value as a long from buffer
*/
public long readFieldLength()
{
int sw = this.byteBuffer[this.position++] & 0xff;
switch (sw)
{
case 251 :
return NULL_LENGTH;
case 252 :
return getUnsignedShort();
case 253 :
return getUnsignedInt24();
case 254 :
return getLong();
default :
return sw;
}
}
/**
* Retrieves the input stream that created this packet
*
* @return the input stream used to read the current packet
*/
public InputStream getInputStream()
{
return inputStream;
}
/**
* Retain creation input stream for further re-reads
*
* @param inputStream the input stream that created this packet
*/
public void setInputStream(InputStream inputStream)
{
this.inputStream = inputStream;
}
/**
* Provided this packet is a large packet > 16M, reads the remaining packets
* and and adds their data to this packet data. The result will consist in
* one large packet with all data in. Note that the new packet's header will
* be invalid
*/
public void readRemainingPackets()
{
if (getDataLength() != MAX_LENGTH)
{
logger.error("readRemainingPackets() is only relevant for large packets!");
return;
}
// Read all packets into an array list
ArrayList<MySQLPacket> nextPackets = new ArrayList<MySQLPacket>();
MySQLPacket nextPacket = readPacket(getInputStream());
while (nextPacket.getDataLength() == MAX_LENGTH)
{
nextPackets.add(nextPacket);
nextPacket = readPacket(getInputStream());
}
nextPackets.add(nextPacket);
// get the new size
int newSize = getDataLength();
for (MySQLPacket packet : nextPackets)
{
newSize += packet.getDataLength();
}
// create a new large buffer
byte[] newBytes = new byte[newSize + HEADER_LENGTH];
// copy this packet data into the new buffer
System.arraycopy(byteBuffer, 0, newBytes, 0, getDataLength()
+ HEADER_LENGTH);
int cursor = getDataLength() + HEADER_LENGTH;
// copy next packets data into at the end of the new buffer
for (MySQLPacket packet : nextPackets)
{
System.arraycopy(packet.getByteBuffer(), HEADER_LENGTH, newBytes,
cursor, packet.getDataLength());
cursor += packet.getDataLength();
}
byteBuffer = newBytes;
dataLength = newSize;
}
/**
* Close inputstream if we have one.
*
* @throws IOException
*/
public void close() throws IOException
{
if (inputStream != null)
{
try
{
inputStream.close();
}
finally
{
inputStream = null;
}
}
}
/**
* Print debug information on status received from the server
*/
public void printServerStatus()
{
String statusMessageString = "";
if (this.isServerFlagSet(MySQLConstants.SERVER_STATUS_IN_TRANS))
statusMessageString = statusMessageString
+ "SERVER_STATUS_IN_TRANS | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_STATUS_AUTOCOMMIT))
statusMessageString = statusMessageString
+ "SERVER_STATUS_AUTOCOMMIT | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_MORE_RESULTS_EXISTS))
statusMessageString = statusMessageString
+ "SERVER_MORE_RESULTS_EXISTS | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_QUERY_NO_GOOD_INDEX_USED))
statusMessageString = statusMessageString
+ "SERVER_QUERY_NO_GOOD_INDEX_USED | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_QUERY_NO_INDEX_USED))
statusMessageString = statusMessageString
+ "SERVER_QUERY_NO_INDEX_USED | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_STATUS_CURSOR_EXISTS))
statusMessageString = statusMessageString
+ "SERVER_STATUS_CURSOR_EXISTS | ";
if (this.isServerFlagSet(MySQLConstants.SERVER_STATUS_LAST_ROW_SENT))
statusMessageString = statusMessageString
+ "SERVER_STATUS_LAST_ROW_SENT | ";
statusMessageString = StringUtils.removeEnd(statusMessageString, "| ");
logger.debug(MessageFormat.format("Server Status= {0}",
statusMessageString));
}
@Override
public String toString()
{
StringBuffer sb = new StringBuffer("Packet #").append(this
.getPacketNumber());
sb.append(" size=").append(this.getDataLength());
sb.append(" pos=").append(this.getPacketPosition());
sb.append(" data=");
if (getDataLength() < 1024)
{
sb.append(Utils.byteArrayToHexString(byteBuffer));
sb.append(" text data=");
for (int i = 0; i < this.byteBuffer.length; i++)
{
if (this.byteBuffer[i] != 0)
sb.append((char) this.byteBuffer[i]);
else
sb.append(' ');
sb.append(" ");
}
}
else
{
for (int i = 0; i < 1024; i++)
{
sb.append(Utils.byteToHexString(this.byteBuffer[i]));
sb.append(' '); // separate bytes with a space
}
sb.append(" ... - text data=");
for (int i = 0; i < 1024; i++)
{
if (Character.isValidCodePoint(this.byteBuffer[i]))
{
sb.append((char) this.byteBuffer[i]);
sb.append(' ');
}
}
sb.append("... [data above 1Kb not displayed]");
}
return sb.toString();
}
}