com.continuent.tungsten.common.mysql.MySQLPacket.java Source code

Java tutorial

Introduction

Here is the source code for com.continuent.tungsten.common.mysql.MySQLPacket.java

Source

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