content: /*
   * Copyright (c) 2015, 2022, Oracle and/or its affiliates.
   *
   * This program is free software; you can redistribute it and/or modify it under
   * the terms of the GNU General Public License, version 2.0, as published by the
   * Free Software Foundation.
   *
   * This program is also distributed with certain software (including but not
   * limited to OpenSSL) that is licensed under separate terms, as designated in a
   * particular file or component or in included license documentation. The
   * authors of MySQL hereby grant you an additional permission to link the
   * program and your derivative works with the separately licensed software that
   * they have included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * This program is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along with
   * this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
   */
  
  package com.mysql.cj.protocol.a;
  
  import java.io.BufferedInputStream;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.Reader;
  import java.lang.management.ManagementFactory;
  import java.lang.management.ThreadInfo;
  import java.lang.management.ThreadMXBean;
  import java.lang.ref.SoftReference;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.net.InetAddress;
  import java.net.MalformedURLException;
  import java.net.Socket;
  import java.net.URISyntaxException;
  import java.net.URL;
  import java.net.UnknownHostException;
  import java.nio.file.InvalidPathException;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.sql.Blob;
  import java.sql.Clob;
  import java.sql.DataTruncation;
  import java.sql.Date;
  import java.sql.SQLWarning;
  import java.sql.Time;
  import java.sql.Timestamp;
  import java.time.Duration;
  import java.time.Instant;
  import java.time.LocalDate;
  import java.time.LocalDateTime;
  import java.time.LocalTime;
  import java.time.OffsetDateTime;
  import java.time.OffsetTime;
  import java.time.ZoneId;
  import java.time.ZoneOffset;
  import java.time.ZonedDateTime;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.TimeZone;
  import java.util.function.Supplier;
  
  import com.mysql.cj.BindValue;
  import com.mysql.cj.CharsetMapping;
  import com.mysql.cj.Constants;
  import com.mysql.cj.MessageBuilder;
  import com.mysql.cj.Messages;
  import com.mysql.cj.MysqlType;
  import com.mysql.cj.NativeCharsetSettings;
  import com.mysql.cj.NativeSession;
  import com.mysql.cj.Query;
  import com.mysql.cj.QueryAttributesBindings;
  import com.mysql.cj.QueryResult;
  import com.mysql.cj.ServerPreparedQuery;
  import com.mysql.cj.ServerVersion;
  import com.mysql.cj.Session;
  import com.mysql.cj.TransactionEventHandler;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.PropertySet;
  import com.mysql.cj.conf.RuntimeProperty;
  import com.mysql.cj.conf.RuntimeProperty.RuntimePropertyListener;
  import com.mysql.cj.exceptions.CJCommunicationsException;
  import com.mysql.cj.exceptions.CJConnectionFeatureNotAvailableException;
  import com.mysql.cj.exceptions.CJException;
  import com.mysql.cj.exceptions.CJOperationNotSupportedException;
  import com.mysql.cj.exceptions.CJPacketTooBigException;
  import com.mysql.cj.exceptions.ClosedOnExpiredPasswordException;
  import com.mysql.cj.exceptions.DataTruncationException;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.FeatureNotAvailableException;
  import com.mysql.cj.exceptions.MysqlErrorNumbers;
  import com.mysql.cj.exceptions.PasswordExpiredException;
  import com.mysql.cj.exceptions.WrongArgumentException;
  import com.mysql.cj.interceptors.QueryInterceptor;
  import com.mysql.cj.jdbc.exceptions.MysqlDataTruncation;
  import com.mysql.cj.log.BaseMetricsHolder;
  import com.mysql.cj.log.Log;
  import com.mysql.cj.log.ProfilerEvent;
  import com.mysql.cj.log.ProfilerEventHandler;
  import com.mysql.cj.protocol.AbstractProtocol;
  import com.mysql.cj.protocol.ColumnDefinition;
  import com.mysql.cj.protocol.ExportControlled;
  import com.mysql.cj.protocol.FullReadInputStream;
  import com.mysql.cj.protocol.Message;
  import com.mysql.cj.protocol.MessageReader;
  import com.mysql.cj.protocol.MessageSender;
  import com.mysql.cj.protocol.PacketReceivedTimeHolder;
  import com.mysql.cj.protocol.PacketSentTimeHolder;
  import com.mysql.cj.protocol.Protocol;
  import com.mysql.cj.protocol.ProtocolEntity;
  import com.mysql.cj.protocol.ProtocolEntityFactory;
  import com.mysql.cj.protocol.ProtocolEntityReader;
  import com.mysql.cj.protocol.ResultBuilder;
  import com.mysql.cj.protocol.Resultset;
  import com.mysql.cj.protocol.Resultset.Concurrency;
  import com.mysql.cj.protocol.Resultset.Type;
  import com.mysql.cj.protocol.ResultsetRow;
  import com.mysql.cj.protocol.ResultsetRows;
  import com.mysql.cj.protocol.ServerSession;
  import com.mysql.cj.protocol.SocketConnection;
  import com.mysql.cj.protocol.ValueEncoder;
  import com.mysql.cj.protocol.a.NativeConstants.IntegerDataType;
  import com.mysql.cj.protocol.a.NativeConstants.StringLengthDataType;
  import com.mysql.cj.protocol.a.NativeConstants.StringSelfDataType;
  import com.mysql.cj.protocol.a.result.OkPacket;
  import com.mysql.cj.result.Field;
  import com.mysql.cj.result.IntegerValueFactory;
  import com.mysql.cj.result.Row;
  import com.mysql.cj.result.StringValueFactory;
  import com.mysql.cj.result.ValueFactory;
  import com.mysql.cj.util.LazyString;
  import com.mysql.cj.util.StringUtils;
  import com.mysql.cj.util.TestUtils;
  import com.mysql.cj.util.TimeUtil;
  import com.mysql.cj.util.Util;
  
  public class NativeProtocol extends AbstractProtocol<NativePacketPayload> implements Protocol<NativePacketPayload>, RuntimePropertyListener {
  
      protected static final int INITIAL_PACKET_SIZE = 1024;
      protected static final int COMP_HEADER_LENGTH = 3;
      protected static final int MAX_QUERY_SIZE_TO_EXPLAIN = 1024 * 1024; // don't explain queries above 1MB
      protected static final int SSL_REQUEST_LENGTH = 32;
      private static final String EXPLAINABLE_STATEMENT = "SELECT";
      private static final String[] EXPLAINABLE_STATEMENT_EXTENSION = new String[] { "INSERT", "UPDATE", "REPLACE", "DELETE" };
  
      protected MessageSender<NativePacketPayload> packetSender;
      protected MessageReader<NativePacketHeader, NativePacketPayload> packetReader;
  
      protected NativeServerSession serverSession;
  
      /** Track this to manually shut down. */
      protected CompressedPacketSender compressedPacketSender;
  
      //private PacketPayload sendPacket = null;
      protected NativePacketPayload sharedSendPacket = null;
      /** Use this when reading in rows to avoid thousands of new() calls, because the byte arrays just get copied out of the packet anyway */
      protected NativePacketPayload reusablePacket = null;
  
      /**
       * Packet used for 'LOAD DATA LOCAL INFILE'
       * 
       * We use a SoftReference, so that we don't penalize intermittent use of this feature
       */
      private SoftReference<NativePacketPayload> loadFileBufRef;
  
      protected byte packetSequence = 0;
      protected boolean useCompression = false;
  
      private RuntimeProperty<Integer> maxAllowedPacket;
      private RuntimeProperty<Boolean> useServerPrepStmts;
  
      private boolean autoGenerateTestcaseScript;
  
      /** Does the server support long column info? */
      private boolean logSlowQueries = false;
      private boolean useAutoSlowLog;
  
      private boolean profileSQL = false;
  
      private long slowQueryThreshold;
  
      private int commandCount = 0;
  
      protected boolean hadWarnings = false;
      private int warningCount = 0;
  
      protected Map<Class<? extends ProtocolEntity>, ProtocolEntityReader<? extends ProtocolEntity, ? extends Message>> PROTOCOL_ENTITY_CLASS_TO_TEXT_READER;
      protected Map<Class<? extends ProtocolEntity>, ProtocolEntityReader<? extends ProtocolEntity, ? extends Message>> PROTOCOL_ENTITY_CLASS_TO_BINARY_READER;
  
      private int statementExecutionDepth = 0;
      private List<QueryInterceptor> queryInterceptors;
  
      private RuntimeProperty<Boolean> maintainTimeStats;
      private RuntimeProperty<Integer> maxQuerySizeToLog;
  
      private InputStream localInfileInputStream;
  
      private BaseMetricsHolder metricsHolder;
  
      static Map<Class<?>, Supplier<ValueEncoder>> DEFAULT_ENCODERS = new HashMap<>();
      static {
          DEFAULT_ENCODERS.put(BigDecimal.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(BigInteger.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(Blob.class, BlobValueEncoder::new);
          DEFAULT_ENCODERS.put(Boolean.class, BooleanValueEncoder::new);
          DEFAULT_ENCODERS.put(Byte.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(byte[].class, ByteArrayValueEncoder::new);
          DEFAULT_ENCODERS.put(Calendar.class, UtilCalendarValueEncoder::new);
          DEFAULT_ENCODERS.put(Clob.class, ClobValueEncoder::new);
          DEFAULT_ENCODERS.put(Date.class, SqlDateValueEncoder::new);
          DEFAULT_ENCODERS.put(java.util.Date.class, UtilDateValueEncoder::new);
          DEFAULT_ENCODERS.put(Double.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(Duration.class, DurationValueEncoder::new);
          DEFAULT_ENCODERS.put(Float.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(InputStream.class, InputStreamValueEncoder::new);
          DEFAULT_ENCODERS.put(Instant.class, InstantValueEncoder::new);
          DEFAULT_ENCODERS.put(Integer.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalDate.class, LocalDateValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalDateTime.class, LocalDateTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalTime.class, LocalTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Long.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(OffsetDateTime.class, OffsetDateTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(OffsetTime.class, OffsetTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Reader.class, ReaderValueEncoder::new);
          DEFAULT_ENCODERS.put(Short.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(String.class, StringValueEncoder::new);
          DEFAULT_ENCODERS.put(Time.class, SqlTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Timestamp.class, SqlTimestampValueEncoder::new);
          DEFAULT_ENCODERS.put(ZonedDateTime.class, ZonedDateTimeValueEncoder::new);
      }
  
      /**
       * The comment (if any) that we'll prepend to all queries
       * sent to the server (to show up in "SHOW PROCESSLIST")
       */
      private String queryComment = null;
  
      private NativeMessageBuilder commandBuilder = null;
  
      public static NativeProtocol getInstance(Session session, SocketConnection socketConnection, PropertySet propertySet, Log log,
              TransactionEventHandler transactionManager) {
          NativeProtocol protocol = new NativeProtocol(log);
          protocol.init(session, socketConnection, propertySet, transactionManager);
          return protocol;
      }
  
      public NativeProtocol(Log logger) {
          this.log = logger;
          this.metricsHolder = new BaseMetricsHolder();
      }
  
      @Override
      public void init(Session sess, SocketConnection phConnection, PropertySet propSet, TransactionEventHandler trManager) {
          super.init(sess, phConnection, propSet, trManager);
  
          this.maintainTimeStats = this.propertySet.getBooleanProperty(PropertyKey.maintainTimeStats);
          this.maxQuerySizeToLog = this.propertySet.getIntegerProperty(PropertyKey.maxQuerySizeToLog);
          this.useAutoSlowLog = this.propertySet.getBooleanProperty(PropertyKey.autoSlowLog).getValue();
          this.logSlowQueries = this.propertySet.getBooleanProperty(PropertyKey.logSlowQueries).getValue();
          this.maxAllowedPacket = this.propertySet.getIntegerProperty(PropertyKey.maxAllowedPacket);
          this.profileSQL = this.propertySet.getBooleanProperty(PropertyKey.profileSQL).getValue();
          this.autoGenerateTestcaseScript = this.propertySet.getBooleanProperty(PropertyKey.autoGenerateTestcaseScript).getValue();
          this.useServerPrepStmts = this.propertySet.getBooleanProperty(PropertyKey.useServerPrepStmts);
  
          this.reusablePacket = new NativePacketPayload(INITIAL_PACKET_SIZE);
  
          try {
              this.packetSender = new SimplePacketSender(this.socketConnection.getMysqlOutput());
              this.packetReader = new SimplePacketReader(this.socketConnection, this.maxAllowedPacket);
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
  
          if (this.propertySet.getBooleanProperty(PropertyKey.logSlowQueries).getValue()) {
              calculateSlowQueryThreshold();
          }
  
          this.authProvider = new NativeAuthenticationProvider();
          this.authProvider.init(this, this.getPropertySet(), this.socketConnection.getExceptionInterceptor());
  
          Map<Class<? extends ProtocolEntity>, ProtocolEntityReader<? extends ProtocolEntity, NativePacketPayload>> protocolEntityClassToTextReader = new HashMap<>();
          protocolEntityClassToTextReader.put(ColumnDefinition.class, new ColumnDefinitionReader(this));
          protocolEntityClassToTextReader.put(ResultsetRow.class, new ResultsetRowReader(this));
          protocolEntityClassToTextReader.put(Resultset.class, new TextResultsetReader(this));
          this.PROTOCOL_ENTITY_CLASS_TO_TEXT_READER = Collections.unmodifiableMap(protocolEntityClassToTextReader);
  
          Map<Class<? extends ProtocolEntity>, ProtocolEntityReader<? extends ProtocolEntity, NativePacketPayload>> protocolEntityClassToBinaryReader = new HashMap<>();
          protocolEntityClassToBinaryReader.put(ColumnDefinition.class, new ColumnDefinitionReader(this));
          protocolEntityClassToBinaryReader.put(Resultset.class, new BinaryResultsetReader(this));
          this.PROTOCOL_ENTITY_CLASS_TO_BINARY_READER = Collections.unmodifiableMap(protocolEntityClassToBinaryReader);
  
      }
  
      @Override
      public MessageBuilder<NativePacketPayload> getMessageBuilder() {
          return getCommandBuilder();
      }
  
      public MessageSender<NativePacketPayload> getPacketSender() {
          return this.packetSender;
      }
  
      public MessageReader<NativePacketHeader, NativePacketPayload> getPacketReader() {
          return this.packetReader;
      }
  
      private NativeMessageBuilder getCommandBuilder() {
          if (this.commandBuilder != null) {
              return this.commandBuilder;
          }
          return this.commandBuilder = new NativeMessageBuilder(this.serverSession.supportsQueryAttributes());
      }
  
      public Supplier<ValueEncoder> getValueEncoderSupplier(Object obj) {
          if (obj == null) {
              return ByteArrayValueEncoder::new;
          }
          Supplier<ValueEncoder> res = DEFAULT_ENCODERS.get(obj.getClass());
          if (res == null) {
              Optional<Supplier<ValueEncoder>> mysqlType = DEFAULT_ENCODERS.entrySet().stream().filter(m -> m.getKey().isAssignableFrom(obj.getClass()))
                      .map(m -> m.getValue()).findFirst();
              if (mysqlType.isPresent()) {
                  res = mysqlType.get();
              }
          }
          return res;
      }
  
      /**
       * Negotiates the SSL communications channel used when connecting
       * to a MySQL server that understands SSL.
       */
      @Override
      public void negotiateSSLConnection() {
          if (!ExportControlled.enabled()) {
              throw new CJConnectionFeatureNotAvailableException(this.getPropertySet(), this.serverSession, this.getPacketSentTimeHolder(), null);
          }
  
          long clientParam = this.serverSession.getClientParam();
  
          NativePacketPayload packet = new NativePacketPayload(SSL_REQUEST_LENGTH);
          packet.writeInteger(IntegerDataType.INT4, clientParam);
          packet.writeInteger(IntegerDataType.INT4, NativeConstants.MAX_PACKET_SIZE);
          packet.writeInteger(IntegerDataType.INT1, this.serverSession.getCharsetSettings().configurePreHandshake(false));
          packet.writeBytes(StringLengthDataType.STRING_FIXED, new byte[23]);  // Set of bytes reserved for future use.
  
          send(packet, packet.getPosition());
  
          try {
              this.socketConnection.performTlsHandshake(this.serverSession, this.log);
  
              // i/o streams were replaced, build new packet sender/reader
              this.packetSender = new SimplePacketSender(this.socketConnection.getMysqlOutput());
              this.packetReader = new SimplePacketReader(this.socketConnection, this.maxAllowedPacket);
  
          } catch (FeatureNotAvailableException nae) {
              throw new CJConnectionFeatureNotAvailableException(this.getPropertySet(), this.serverSession, this.getPacketSentTimeHolder(), nae);
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
      }
  
      public void rejectProtocol(NativePacketPayload msg) {
          try {
              this.socketConnection.getMysqlSocket().close();
          } catch (Exception e) {
              // ignore
          }
  
          int errno = 2000;
  
          NativePacketPayload buf = msg;
          buf.setPosition(1); // skip the packet type
          errno = (int) buf.readInteger(IntegerDataType.INT2);
  
          String serverErrorMessage = "";
          try {
              serverErrorMessage = buf.readString(StringSelfDataType.STRING_TERM, "ASCII");
          } catch (Exception e) {
              //
          }
  
          StringBuilder errorBuf = new StringBuilder(Messages.getString("Protocol.0"));
          errorBuf.append(serverErrorMessage);
          errorBuf.append("\"");
  
          String xOpen = MysqlErrorNumbers.mysqlToSqlState(errno);
  
          throw ExceptionFactory.createException(MysqlErrorNumbers.get(xOpen) + ", " + errorBuf.toString(), xOpen, errno, false, null, getExceptionInterceptor());
      }
  
      @Override
      public void beforeHandshake() {
          // Reset packet sequences
          this.packetReader.resetMessageSequence();
  
          // Create session state
          this.serverSession = new NativeServerSession(this.propertySet);
  
          this.serverSession.setCharsetSettings(new NativeCharsetSettings((NativeSession) this.session));
  
          // Read the first packet
          this.serverSession.setCapabilities(readServerCapabilities());
  
      }
  
      @Override
      public void afterHandshake() {
  
          checkTransactionState();
  
          try {
              //
              // Can't enable compression until after handshake
              //
              if (((this.serverSession.getCapabilities().getCapabilityFlags() & NativeServerSession.CLIENT_COMPRESS) != 0)
                      && this.propertySet.getBooleanProperty(PropertyKey.useCompression).getValue()
                      && !(this.socketConnection.getMysqlInput().getUnderlyingStream() instanceof CompressedInputStream)) {
                  this.useCompression = true;
                  this.socketConnection.setMysqlInput(new FullReadInputStream(new CompressedInputStream(this.socketConnection.getMysqlInput(),
                          this.propertySet.getBooleanProperty(PropertyKey.traceProtocol), this.log)));
                  this.compressedPacketSender = new CompressedPacketSender(this.socketConnection.getMysqlOutput());
                  this.packetSender = this.compressedPacketSender;
              }
  
              applyPacketDecorators(this.packetSender, this.packetReader);
  
              this.socketConnection.getSocketFactory().afterHandshake();
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
  
          // Changing defaults for 8.0.3+ server: PNAME_useInformationSchema=true
          RuntimeProperty<Boolean> useInformationSchema = this.propertySet.<Boolean>getProperty(PropertyKey.useInformationSchema);
          if (versionMeetsMinimum(8, 0, 3) && !useInformationSchema.getValue() && !useInformationSchema.isExplicitlySet()) {
              useInformationSchema.setValue(true);
          }
  
          // listen for properties changes to allow decorators reconfiguration
          this.maintainTimeStats.addListener(this);
          this.propertySet.getBooleanProperty(PropertyKey.traceProtocol).addListener(this);
          this.propertySet.getBooleanProperty(PropertyKey.enablePacketDebug).addListener(this);
      }
  
      @Override
      public void handlePropertyChange(RuntimeProperty<?> prop) {
          switch (prop.getPropertyDefinition().getPropertyKey()) {
              case maintainTimeStats:
              case traceProtocol:
              case enablePacketDebug:
  
                  applyPacketDecorators(this.packetSender.undecorateAll(), this.packetReader.undecorateAll());
  
                  break;
  
              default:
                  break;
          }
      }
  
      /**
       * Apply optional decorators to configured PacketSender and PacketReader.
       * 
       * @param sender
       *            {@link MessageSender}
       * @param messageReader
       *            {@link MessageReader}
       */
      public void applyPacketDecorators(MessageSender<NativePacketPayload> sender, MessageReader<NativePacketHeader, NativePacketPayload> messageReader) {
          TimeTrackingPacketSender ttSender = null;
          TimeTrackingPacketReader ttReader = null;
          LinkedList<StringBuilder> debugRingBuffer = null;
  
          if (this.maintainTimeStats.getValue()) {
              ttSender = new TimeTrackingPacketSender(sender);
              sender = ttSender;
  
              ttReader = new TimeTrackingPacketReader(messageReader);
              messageReader = ttReader;
          }
  
          if (this.propertySet.getBooleanProperty(PropertyKey.traceProtocol).getValue()) {
              sender = new TracingPacketSender(sender, this.log, this.socketConnection.getHost(), getServerSession().getCapabilities().getThreadId());
              messageReader = new TracingPacketReader(messageReader, this.log);
          }
  
          if (this.getPropertySet().getBooleanProperty(PropertyKey.enablePacketDebug).getValue()) {
  
              debugRingBuffer = new LinkedList<>();
  
              sender = new DebugBufferingPacketSender(sender, debugRingBuffer, this.propertySet.getIntegerProperty(PropertyKey.packetDebugBufferSize));
              messageReader = new DebugBufferingPacketReader(messageReader, debugRingBuffer,
                      this.propertySet.getIntegerProperty(PropertyKey.packetDebugBufferSize));
          }
  
          // do it after other decorators to have trace and debug applied to individual packets 
          messageReader = new MultiPacketReader(messageReader);
  
          // atomic replacement of currently used objects
          synchronized (this.packetReader) {
              this.packetReader = messageReader;
              this.packetDebugRingBuffer = debugRingBuffer;
              this.setPacketSentTimeHolder(ttSender != null ? ttSender : new PacketSentTimeHolder() {
              });
          }
          synchronized (this.packetSender) {
              this.packetSender = sender;
              this.setPacketReceivedTimeHolder(ttReader != null ? ttReader : new PacketReceivedTimeHolder() {
              });
          }
      }
  
      public NativeCapabilities readServerCapabilities() {
          // Read the first packet
          NativePacketPayload buf = readMessage(null);
  
          // Server Greeting Error packet instead of Server Greeting
          if (buf.isErrorPacket()) {
              rejectProtocol(buf);
          }
  
          return new NativeCapabilities(buf);
      }
  
      @Override
      public NativeServerSession getServerSession() {
          return this.serverSession;
      }
  
      @Override
      public void changeDatabase(String database) {
          if (database == null || database.length() == 0) {
              return;
          }
  
          try {
              sendCommand(getCommandBuilder().buildComInitDb(getSharedSendPacket(), database), false, 0);
          } catch (CJException ex) {
              if (this.getPropertySet().getBooleanProperty(PropertyKey.createDatabaseIfNotExist).getValue()) {
                  sendCommand(getCommandBuilder().buildComQuery(getSharedSendPacket(),
                          "CREATE DATABASE IF NOT EXISTS " + StringUtils.quoteIdentifier(database, true)), false, 0);
  
                  sendCommand(getCommandBuilder().buildComInitDb(getSharedSendPacket(), database), false, 0);
              } else {
                  throw ExceptionFactory.createCommunicationsException(this.getPropertySet(), this.serverSession, this.getPacketSentTimeHolder(),
                          this.getPacketReceivedTimeHolder(), ex, getExceptionInterceptor());
              }
          }
      }
  
      @Override
      public final NativePacketPayload readMessage(NativePacketPayload reuse) {
          try {
              NativePacketHeader header = this.packetReader.readHeader();
              NativePacketPayload buf = this.packetReader.readMessage(Optional.ofNullable(reuse), header);
              this.packetSequence = header.getMessageSequence();
              return buf;
  
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          } catch (OutOfMemoryError oom) {
              throw ExceptionFactory.createException(oom.getMessage(), MysqlErrorNumbers.SQL_STATE_MEMORY_ALLOCATION_ERROR, 0, false, oom,
                      this.exceptionInterceptor);
          }
      }
  
      public final NativePacketPayload probeMessage(NativePacketPayload reuse) {
          try {
              NativePacketHeader header = this.packetReader.probeHeader();
              NativePacketPayload buf = this.packetReader.probeMessage(Optional.ofNullable(reuse), header);
              this.packetSequence = header.getMessageSequence();
              return buf;
  
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          } catch (OutOfMemoryError oom) {
              throw ExceptionFactory.createException(oom.getMessage(), MysqlErrorNumbers.SQL_STATE_MEMORY_ALLOCATION_ERROR, 0, false, oom,
                      this.exceptionInterceptor);
          }
      }
  
      /**
       * @param packet
       *            {@link Message}
       * @param packetLen
       *            length of header + payload
       */
      @Override
      public final void send(Message packet, int packetLen) {
          try {
              if (this.maxAllowedPacket.getValue() > 0 && packetLen > this.maxAllowedPacket.getValue()) {
                  throw new CJPacketTooBigException(packetLen, this.maxAllowedPacket.getValue());
              }
  
              this.packetSequence++;
              this.packetSender.send(packet.getByteBuffer(), packetLen, this.packetSequence);
  
              //
              // Don't hold on to large packets
              //
              if (packet == this.sharedSendPacket) {
                  reclaimLargeSharedSendPacket();
              }
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.getPropertySet(), this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
      }
  
      @Override
      public final NativePacketPayload sendCommand(Message queryPacket, boolean skipCheck, int timeoutMillis) {
          int command = queryPacket.getByteBuffer()[0];
          this.commandCount++;
  
          if (this.queryInterceptors != null) {
              NativePacketPayload interceptedPacketPayload = (NativePacketPayload) invokeQueryInterceptorsPre(queryPacket, false);
  
              if (interceptedPacketPayload != null) {
                  return interceptedPacketPayload;
              }
          }
  
          this.packetReader.resetMessageSequence();
  
          int oldTimeout = 0;
  
          if (timeoutMillis != 0) {
              try {
                  oldTimeout = this.socketConnection.getMysqlSocket().getSoTimeout();
                  this.socketConnection.getMysqlSocket().setSoTimeout(timeoutMillis);
              } catch (IOException e) {
                  throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                          this.getPacketReceivedTimeHolder(), e, getExceptionInterceptor());
              }
          }
  
          try {
  
              checkForOutstandingStreamingData();
  
              // Clear serverStatus...this value is guarded by an external mutex, as you can only ever be processing one command at a time
              this.serverSession.setStatusFlags(0, true);
              this.hadWarnings = false;
              this.setWarningCount(0);
  
              //
              // Compressed input stream needs cleared at beginning of each command execution...
              //
              if (this.useCompression) {
                  int bytesLeft = this.socketConnection.getMysqlInput().available();
  
                  if (bytesLeft > 0) {
                      this.socketConnection.getMysqlInput().skip(bytesLeft);
                  }
              }
  
              try {
                  clearInputStream();
                  this.packetSequence = -1;
                  send(queryPacket, queryPacket.getPosition());
  
              } catch (CJException ex) {
                  // don't wrap CJExceptions
                  throw ex;
              } catch (Exception ex) {
                  throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                          this.getPacketReceivedTimeHolder(), ex, getExceptionInterceptor());
              }
  
              NativePacketPayload returnPacket = null;
  
              if (!skipCheck) {
                  if ((command == NativeConstants.COM_STMT_EXECUTE) || (command == NativeConstants.COM_STMT_RESET)) {
                      this.packetReader.resetMessageSequence();
                  }
  
                  returnPacket = checkErrorMessage(command);
  
                  if (this.queryInterceptors != null) {
                      returnPacket = (NativePacketPayload) invokeQueryInterceptorsPost(queryPacket, returnPacket, false);
                  }
              }
  
              return returnPacket;
          } catch (IOException ioEx) {
              this.serverSession.preserveOldTransactionState();
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          } catch (CJException e) {
              this.serverSession.preserveOldTransactionState();
              throw e;
  
          } finally {
              if (timeoutMillis != 0) {
                  try {
                      this.socketConnection.getMysqlSocket().setSoTimeout(oldTimeout);
                  } catch (IOException e) {
                      throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                              this.getPacketReceivedTimeHolder(), e, getExceptionInterceptor());
                  }
              }
          }
      }
  
      public void checkTransactionState() {
          int transState = this.serverSession.getTransactionState();
          if (transState == ServerSession.TRANSACTION_COMPLETED) {
              this.transactionManager.transactionCompleted();
          } else if (transState == ServerSession.TRANSACTION_STARTED) {
              this.transactionManager.transactionBegun();
          }
      }
  
      public NativePacketPayload checkErrorMessage() {
          return checkErrorMessage(-1);
      }
  
      /**
       * Checks for errors in the reply packet, and if none, returns the reply
       * packet, ready for reading
       * 
       * @param command
       *            the command being issued (if used)
       * @return NativePacketPayload
       * @throws CJException
       *             if an error packet was received
       * @throws CJCommunicationsException
       *             if a database error occurs
       */
      private NativePacketPayload checkErrorMessage(int command) {
  
          NativePacketPayload resultPacket = null;
          this.serverSession.setStatusFlags(0);
  
          try {
              // Check return value, if we get a java.io.EOFException, the server has gone away. We'll pass it on up the exception chain and let someone higher up
              // decide what to do (barf, reconnect, etc).
              resultPacket = readMessage(this.reusablePacket);
          } catch (CJException ex) {
              // Don't wrap CJExceptions
              throw ex;
          } catch (Exception fallThru) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), fallThru, getExceptionInterceptor());
          }
  
          checkErrorMessage(resultPacket);
  
          return resultPacket;
      }
  
      public void checkErrorMessage(NativePacketPayload resultPacket) {
  
          resultPacket.setPosition(0);
          byte statusCode = (byte) resultPacket.readInteger(IntegerDataType.INT1);
  
          // Error handling
          if (statusCode == (byte) 0xff) {
              String serverErrorMessage;
              int errno = 2000;
  
              errno = (int) resultPacket.readInteger(IntegerDataType.INT2);
  
              String xOpen = null;
  
              serverErrorMessage = resultPacket.readString(StringSelfDataType.STRING_TERM, this.serverSession.getCharsetSettings().getErrorMessageEncoding());
  
              if (serverErrorMessage.charAt(0) == '#') {
  
                  // we have an SQLState
                  if (serverErrorMessage.length() > 6) {
                      xOpen = serverErrorMessage.substring(1, 6);
                      serverErrorMessage = serverErrorMessage.substring(6);
  
                      if (xOpen.equals("HY000")) {
                          xOpen = MysqlErrorNumbers.mysqlToSqlState(errno);
                      }
                  } else {
                      xOpen = MysqlErrorNumbers.mysqlToSqlState(errno);
                  }
              } else {
                  xOpen = MysqlErrorNumbers.mysqlToSqlState(errno);
              }
  
              clearInputStream();
  
              StringBuilder errorBuf = new StringBuilder();
  
              String xOpenErrorMessage = MysqlErrorNumbers.get(xOpen);
  
              boolean useOnlyServerErrorMessages = this.propertySet.getBooleanProperty(PropertyKey.useOnlyServerErrorMessages).getValue();
              if (!useOnlyServerErrorMessages) {
                  if (xOpenErrorMessage != null) {
                      errorBuf.append(xOpenErrorMessage);
                      errorBuf.append(Messages.getString("Protocol.0"));
                  }
              }
  
              errorBuf.append(serverErrorMessage);
  
              if (!useOnlyServerErrorMessages) {
                  if (xOpenErrorMessage != null) {
                      errorBuf.append("\"");
                  }
              }
  
              appendDeadlockStatusInformation(this.session, xOpen, errorBuf);
  
              if (xOpen != null) {
                  if (xOpen.startsWith("22")) {
                      throw new DataTruncationException(errorBuf.toString(), 0, true, false, 0, 0, errno);
                  }
  
                  if (errno == MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD) {
                      throw ExceptionFactory.createException(PasswordExpiredException.class, errorBuf.toString(), getExceptionInterceptor());
  
                  } else if (errno == MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD_LOGIN) {
                      throw ExceptionFactory.createException(ClosedOnExpiredPasswordException.class, errorBuf.toString(), getExceptionInterceptor());
  
                  } else if (errno == MysqlErrorNumbers.ER_CLIENT_INTERACTION_TIMEOUT) {
                      throw ExceptionFactory.createException(CJCommunicationsException.class, errorBuf.toString(), null, getExceptionInterceptor());
                  }
              }
  
              throw ExceptionFactory.createException(errorBuf.toString(), xOpen, errno, false, null, getExceptionInterceptor());
  
          }
      }
  
      private void reclaimLargeSharedSendPacket() {
          if ((this.sharedSendPacket != null) && (this.sharedSendPacket.getCapacity() > 1048576)) {
              this.sharedSendPacket = new NativePacketPayload(INITIAL_PACKET_SIZE);
          }
      }
  
      public void clearInputStream() {
          try {
              int len;
  
              // Due to a bug in some older Linux kernels (fixed after the patch "tcp: fix FIONREAD/SIOCINQ"), our SocketInputStream.available() may return 1 even
              // if there is no data in the Stream, so, we need to check if InputStream.skip() actually skipped anything.
              while ((len = this.socketConnection.getMysqlInput().available()) > 0 && this.socketConnection.getMysqlInput().skip(len) > 0) {
                  continue;
              }
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
      }
  
      /**
       * Don't hold on to overly-large packets
       */
      public void reclaimLargeReusablePacket() {
          if ((this.reusablePacket != null) && (this.reusablePacket.getCapacity() > 1048576)) {
              this.reusablePacket = new NativePacketPayload(INITIAL_PACKET_SIZE);
          }
      }
  
      /**
       * Build a query packet from the given string and send it to the server.
       * 
       * @param <T>
       *            extends {@link Resultset}
       * @param callingQuery
       *            {@link Query}
       * @param query
       *            query string
       * @param characterEncoding
       *            Java encoding name
       * @param maxRows
       *            rows limit
       * @param streamResults
       *            whether a stream result should be created
       * @param cachedMetadata
       *            use this metadata instead of the one provided on wire
       * @param resultSetFactory
       *            {@link ProtocolEntityFactory}
       * @return T instance
       * @throws IOException
       *             if an i/o error occurs
       */
      public final <T extends Resultset> T sendQueryString(Query callingQuery, String query, String characterEncoding, int maxRows, boolean streamResults,
              ColumnDefinition cachedMetadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
          String statementComment = this.queryComment;
  
          if (this.propertySet.getBooleanProperty(PropertyKey.includeThreadNamesAsStatementComment).getValue()) {
              statementComment = (statementComment != null ? statementComment + ", " : "") + "java thread: " + Thread.currentThread().getName();
          }
  
          // We don't know exactly how many bytes we're going to get from the query. Since we're dealing with UTF-8, the max is 4, so pad it
          // (4 * query) + space for headers
          int packLength = 1 /* com_query */ + (query.length() * 4) + 2;
  
          byte[] commentAsBytes = null;
  
          if (statementComment != null) {
              commentAsBytes = StringUtils.getBytes(statementComment, characterEncoding);
  
              packLength += commentAsBytes.length;
              packLength += 6; // for /*[space] [space]*/
          }
  
          boolean supportsQueryAttributes = this.serverSession.supportsQueryAttributes();
          QueryAttributesBindings queryAttributes = null;
  
          if (!supportsQueryAttributes && callingQuery != null && callingQuery.getQueryAttributesBindings().getCount() > 0) {
              this.log.logWarn(Messages.getString("QueryAttributes.SetButNotSupported"));
          }
  
          if (supportsQueryAttributes) {
              if (callingQuery != null) {
                  queryAttributes = callingQuery.getQueryAttributesBindings();
              }
  
              if (queryAttributes != null && queryAttributes.getCount() > 0) {
                  packLength += 9 /* parameter_count */ + 1 /* parameter_set_count */;
                  packLength += (queryAttributes.getCount() + 7) / 8 /* null_bitmap */ + 1 /* new_params_bind_flag */;
                  for (int i = 0; i < queryAttributes.getCount(); i++) {
                      BindValue queryAttribute = queryAttributes.getAttributeValue(i);
                      packLength += 2 /* parameter_type */ + queryAttribute.getName().length() /* parameter_name */ + queryAttribute.getBinaryLength();
                  }
              } else {
                  packLength += 1 /* parameter_count */ + 1 /* parameter_set_count */;
              }
          }
  
          // TODO decide how to safely use the shared this.sendPacket
          NativePacketPayload sendPacket = new NativePacketPayload(packLength);
  
          sendPacket.setPosition(0);
          sendPacket.writeInteger(IntegerDataType.INT1, NativeConstants.COM_QUERY);
  
          if (supportsQueryAttributes) {
              if (queryAttributes != null && queryAttributes.getCount() > 0) {
                  sendPacket.writeInteger(IntegerDataType.INT_LENENC, queryAttributes.getCount());
                  sendPacket.writeInteger(IntegerDataType.INT_LENENC, 1); // parameter_set_count (always 1)
                  byte[] nullBitsBuffer = new byte[(queryAttributes.getCount() + 7) / 8];
                  for (int i = 0; i < queryAttributes.getCount(); i++) {
                      if (queryAttributes.getAttributeValue(i).isNull()) {
                          nullBitsBuffer[i >>> 3] |= 1 << (i & 7);
                      }
                  }
                  sendPacket.writeBytes(StringLengthDataType.STRING_VAR, nullBitsBuffer);
                  sendPacket.writeInteger(IntegerDataType.INT1, 1); // new_params_bind_flag (always 1)
                  queryAttributes.runThroughAll(a -> {
                      sendPacket.writeInteger(IntegerDataType.INT2, a.getFieldType());
                      sendPacket.writeBytes(StringSelfDataType.STRING_LENENC, a.getName().getBytes());
                  });
                  queryAttributes.runThroughAll(a -> {
                      if (!a.isNull()) {
                          a.writeAsQueryAttribute(sendPacket);
                      }
                  });
              } else {
                  sendPacket.writeInteger(IntegerDataType.INT_LENENC, 0);
                  sendPacket.writeInteger(IntegerDataType.INT_LENENC, 1); // parameter_set_count (always 1)
              }
          }
          sendPacket.setTag("QUERY");
  
          if (commentAsBytes != null) {
              sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, Constants.SLASH_STAR_SPACE_AS_BYTES);
              sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, commentAsBytes);
              sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, Constants.SPACE_STAR_SLASH_SPACE_AS_BYTES);
          }
  
          if (!this.session.getServerSession().getCharsetSettings().doesPlatformDbCharsetMatches() && StringUtils.startsWithIgnoreCaseAndWs(query, "LOAD DATA")) {
              sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, StringUtils.getBytes(query));
          } else {
              sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, StringUtils.getBytes(query, characterEncoding));
          }
  
          return sendQueryPacket(callingQuery, sendPacket, maxRows, streamResults, cachedMetadata, resultSetFactory);
      }
  
      /**
       * Send a query stored in a packet to the server.
       * 
       * @param <T>
       *            extends {@link Resultset}
       * @param callingQuery
       *            {@link Query}
       * @param queryPacket
       *            {@link NativePacketPayload} containing query
       * @param maxRows
       *            rows limit
       * @param streamResults
       *            whether a stream result should be created
       * @param cachedMetadata
       *            use this metadata instead of the one provided on wire
       * @param resultSetFactory
       *            {@link ProtocolEntityFactory}
       * @return T instance
       * @throws IOException
       *             if an i/o error occurs
       */
      public final <T extends Resultset> T sendQueryPacket(Query callingQuery, NativePacketPayload queryPacket, int maxRows, boolean streamResults,
              ColumnDefinition cachedMetadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
  
          final long queryStartTime = getCurrentTimeNanosOrMillis();
  
          this.statementExecutionDepth++;
  
          byte[] queryBuf = queryPacket.getByteBuffer();
          int oldPacketPosition = queryPacket.getPosition(); // save the packet position
          int queryPosition = queryPacket.getTag("QUERY");
          LazyString query = new LazyString(queryBuf, queryPosition, (oldPacketPosition - queryPosition));
  
          try {
  
              if (this.queryInterceptors != null) {
                  T interceptedResults = invokeQueryInterceptorsPre(query, callingQuery, false);
                  if (interceptedResults != null) {
                      return interceptedResults;
                  }
              }
  
              if (this.autoGenerateTestcaseScript) {
                  StringBuilder debugBuf = new StringBuilder(query.length() + 32);
                  generateQueryCommentBlock(debugBuf);
                  debugBuf.append(query);
                  debugBuf.append(';');
                  TestUtils.dumpTestcaseQuery(debugBuf.toString());
              }
  
              // Send query command and sql query string
              NativePacketPayload resultPacket = sendCommand(queryPacket, false, 0);
  
              final long queryEndTime = getCurrentTimeNanosOrMillis();
              final long queryDuration = queryEndTime - queryStartTime;
              if (callingQuery != null) {
                  callingQuery.setExecuteTime(queryDuration);
              }
  
              boolean queryWasSlow = this.logSlowQueries && (this.useAutoSlowLog ? this.metricsHolder.checkAbonormallyLongQuery(queryDuration)
                      : queryDuration > this.propertySet.getIntegerProperty(PropertyKey.slowQueryThresholdMillis).getValue());
  
              long fetchBeginTime = this.profileSQL ? getCurrentTimeNanosOrMillis() : 0L;
  
              T rs = readAllResults(maxRows, streamResults, resultPacket, false, cachedMetadata, resultSetFactory);
  
              if (this.profileSQL || queryWasSlow) {
                  long fetchEndTime = this.profileSQL ? getCurrentTimeNanosOrMillis() : 0L;
  
                  // Extract the actual query from the network packet
                  boolean truncated = oldPacketPosition - queryPosition > this.maxQuerySizeToLog.getValue();
                  int extractPosition = truncated ? this.maxQuerySizeToLog.getValue() + queryPosition : oldPacketPosition;
                  String extractedQuery = StringUtils.toString(queryBuf, queryPosition, (extractPosition - queryPosition));
                  if (truncated) {
                      extractedQuery += Messages.getString("Protocol.2");
                  }
  
                  ProfilerEventHandler eventSink = this.session.getProfilerEventHandler();
  
                  if (this.logSlowQueries) {
                      if (queryWasSlow) {
                          eventSink.processEvent(ProfilerEvent.TYPE_SLOW_QUERY, this.session, callingQuery, rs, queryDuration, new Throwable(),
                                  Messages.getString("Protocol.SlowQuery",
                                          new Object[] { this.useAutoSlowLog ? " 95% of all queries " : String.valueOf(this.slowQueryThreshold),
                                                  this.queryTimingUnits, Long.valueOf(queryDuration), extractedQuery }));
  
                          if (this.propertySet.getBooleanProperty(PropertyKey.explainSlowQueries).getValue()) {
                              if (oldPacketPosition - queryPosition < MAX_QUERY_SIZE_TO_EXPLAIN) {
                                  queryPacket.setPosition(queryPosition); // skip until the query is located in the packet 
                                  explainSlowQuery(query.toString(), extractedQuery);
                              } else {
                                  this.log.logWarn(Messages.getString("Protocol.3", new Object[] { MAX_QUERY_SIZE_TO_EXPLAIN }));
                              }
                          }
                      }
  
                      if (this.serverSession.noGoodIndexUsed()) {
                          eventSink.processEvent(ProfilerEvent.TYPE_SLOW_QUERY, this.session, callingQuery, rs, queryDuration, new Throwable(),
                                  Messages.getString("Protocol.4") + extractedQuery);
                      }
                      if (this.serverSession.noIndexUsed()) {
                          eventSink.processEvent(ProfilerEvent.TYPE_SLOW_QUERY, this.session, callingQuery, rs, queryDuration, new Throwable(),
                                  Messages.getString("Protocol.5") + extractedQuery);
                      }
                      if (this.serverSession.queryWasSlow()) {
                          eventSink.processEvent(ProfilerEvent.TYPE_SLOW_QUERY, this.session, callingQuery, rs, queryDuration, new Throwable(),
                                  Messages.getString("Protocol.ServerSlowQuery") + extractedQuery);
                      }
                  }
  
                  if (this.profileSQL) {
                      eventSink.processEvent(ProfilerEvent.TYPE_QUERY, this.session, callingQuery, rs, queryDuration, new Throwable(), extractedQuery);
                      eventSink.processEvent(ProfilerEvent.TYPE_FETCH, this.session, callingQuery, rs, (fetchEndTime - fetchBeginTime), new Throwable(), null);
                  }
              }
  
              if (this.hadWarnings) {
                  scanForAndThrowDataTruncation();
              }
  
              if (this.queryInterceptors != null) {
                  rs = invokeQueryInterceptorsPost(query, callingQuery, rs, false);
              }
  
              return rs;
  
          } catch (CJException sqlEx) {
              if (this.queryInterceptors != null) {
                  // TODO why doing this?
                  invokeQueryInterceptorsPost(query, callingQuery, null, false); // we don't do anything with the result set in this case
              }
  
              if (callingQuery != null) {
                  callingQuery.checkCancelTimeout();
              }
  
              throw sqlEx;
  
          } finally {
              this.statementExecutionDepth--;
          }
      }
  
      public <T extends Resultset> T invokeQueryInterceptorsPre(Supplier<String> sql, Query interceptedQuery, boolean forceExecute) {
          T previousResultSet = null;
  
          for (int i = 0, s = this.queryInterceptors.size(); i < s; i++) {
              QueryInterceptor interceptor = this.queryInterceptors.get(i);
  
              boolean executeTopLevelOnly = interceptor.executeTopLevelOnly();
              boolean shouldExecute = (executeTopLevelOnly && (this.statementExecutionDepth == 1 || forceExecute)) || (!executeTopLevelOnly);
  
              if (shouldExecute) {
                  T interceptedResultSet = interceptor.preProcess(sql, interceptedQuery);
  
                  if (interceptedResultSet != null) {
                      previousResultSet = interceptedResultSet;
                  }
              }
          }
  
          return previousResultSet;
      }
  
      /**
       * 
       * @param <M>
       *            extends {@link Message}
       * @param queryPacket
       *            {@link NativePacketPayload} containing query
       * @param forceExecute
       *            currently ignored
       * @return M instance
       */
      public <M extends Message> M invokeQueryInterceptorsPre(M queryPacket, boolean forceExecute) {
          M previousPacketPayload = null;
  
          for (int i = 0, s = this.queryInterceptors.size(); i < s; i++) {
              QueryInterceptor interceptor = this.queryInterceptors.get(i);
  
              // TODO how to handle executeTopLevelOnly in such case ?
              //            boolean executeTopLevelOnly = interceptor.executeTopLevelOnly();
              //            boolean shouldExecute = (executeTopLevelOnly && (this.statementExecutionDepth == 1 || forceExecute)) || (!executeTopLevelOnly);
              //            if (shouldExecute) {
  
              M interceptedPacketPayload = interceptor.preProcess(queryPacket);
              if (interceptedPacketPayload != null) {
                  previousPacketPayload = interceptedPacketPayload;
              }
              //            }
          }
  
          return previousPacketPayload;
      }
  
      public <T extends Resultset> T invokeQueryInterceptorsPost(Supplier<String> sql, Query interceptedQuery, T originalResultSet, boolean forceExecute) {
  
          for (int i = 0, s = this.queryInterceptors.size(); i < s; i++) {
              QueryInterceptor interceptor = this.queryInterceptors.get(i);
  
              boolean executeTopLevelOnly = interceptor.executeTopLevelOnly();
              boolean shouldExecute = (executeTopLevelOnly && (this.statementExecutionDepth == 1 || forceExecute)) || (!executeTopLevelOnly);
  
              if (shouldExecute) {
                  T interceptedResultSet = interceptor.postProcess(sql, interceptedQuery, originalResultSet, this.serverSession);
  
                  if (interceptedResultSet != null) {
                      originalResultSet = interceptedResultSet;
                  }
              }
          }
  
          return originalResultSet;
      }
  
      /**
       * 
       * @param <M>
       *            extends {@link Message}
       * @param queryPacket
       *            {@link NativePacketPayload} containing query
       * @param originalResponsePacket
       *            {@link NativePacketPayload} containing response
       * @param forceExecute
       *            currently ignored
       * @return T instance
       */
      public <M extends Message> M invokeQueryInterceptorsPost(M queryPacket, M originalResponsePacket, boolean forceExecute) {
  
          for (int i = 0, s = this.queryInterceptors.size(); i < s; i++) {
              QueryInterceptor interceptor = this.queryInterceptors.get(i);
  
              // TODO how to handle executeTopLevelOnly in such case ?
              //            boolean executeTopLevelOnly = interceptor.executeTopLevelOnly();
              //            boolean shouldExecute = (executeTopLevelOnly && (this.statementExecutionDepth == 1 || forceExecute)) || (!executeTopLevelOnly);
              //            if (shouldExecute) {
  
              M interceptedPacketPayload = interceptor.postProcess(queryPacket, originalResponsePacket);
              if (interceptedPacketPayload != null) {
                  originalResponsePacket = interceptedPacketPayload;
              }
              //            }
          }
  
          return originalResponsePacket;
      }
  
      public long getCurrentTimeNanosOrMillis() {
          return this.useNanosForElapsedTime ? TimeUtil.getCurrentTimeNanosOrMillis() : System.currentTimeMillis();
      }
  
      public boolean hadWarnings() {
          return this.hadWarnings;
      }
  
      public void setHadWarnings(boolean hadWarnings) {
          this.hadWarnings = hadWarnings;
      }
  
      /**
       * Runs an 'EXPLAIN' on the given query and dumps the results to the log
       * 
       * @param query
       *            full query string
       * @param truncatedQuery
       *            query string truncated for profiling
       * 
       */
      public void explainSlowQuery(String query, String truncatedQuery) {
          if (StringUtils.startsWithIgnoreCaseAndWs(truncatedQuery, EXPLAINABLE_STATEMENT)
                  || (versionMeetsMinimum(5, 6, 3) && StringUtils.startsWithIgnoreCaseAndWs(truncatedQuery, EXPLAINABLE_STATEMENT_EXTENSION) != -1)) {
  
              try {
                  NativePacketPayload resultPacket = sendCommand(getCommandBuilder().buildComQuery(getSharedSendPacket(), "EXPLAIN " + query), false, 0);
  
                  Resultset rs = readAllResults(-1, false, resultPacket, false, null, new ResultsetFactory(Type.FORWARD_ONLY, null));
  
                  StringBuilder explainResults = new StringBuilder(Messages.getString("Protocol.6"));
                  explainResults.append(truncatedQuery);
                  explainResults.append(Messages.getString("Protocol.7"));
  
                  appendResultSetSlashGStyle(explainResults, rs);
  
                  this.log.logWarn(explainResults.toString());
              } catch (CJException sqlEx) {
                  throw sqlEx;
  
              } catch (Exception ex) {
                  throw ExceptionFactory.createException(ex.getMessage(), ex, getExceptionInterceptor());
              }
          }
      }
  
      /**
       * Reads and discards a single MySQL packet from the input stream.
       * 
       * @throws CJException
       *             if the network fails while skipping the
       *             packet.
       */
      public final void skipPacket() {
          try {
  
              int packetLength = this.packetReader.readHeader().getMessageSize();
  
              this.socketConnection.getMysqlInput().skipFully(packetLength);
  
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
      }
  
      /**
       * Log-off of the MySQL server and close the socket.
       * 
       */
      public final void quit() {
          try {
              try {
                  if (!ExportControlled.isSSLEstablished(this.socketConnection.getMysqlSocket())) { // Fix for Bug#56979 does not apply to secure sockets.
                      if (!this.socketConnection.getMysqlSocket().isClosed()) {
                          try {
                              // The response won't be read, this fixes BUG#56979 [Improper connection closing logic leads to TIME_WAIT sockets on server].
                              this.socketConnection.getMysqlSocket().shutdownInput();
                          } catch (UnsupportedOperationException e) {
                              // Ignore, some sockets do not support this method.
                          }
                      }
                  }
              } catch (IOException e) {
                  // Can't do anything constructive about this.
              }
  
              this.packetSequence = -1;
              NativePacketPayload packet = new NativePacketPayload(1);
              send(getCommandBuilder().buildComQuit(packet), packet.getPosition());
          } finally {
              this.socketConnection.forceClose();
              this.localInfileInputStream = null;
          }
      }
  
      /**
       * Returns the packet used for sending data (used by PreparedStatement) with position set to 0.
       * Guarded by external synchronization on a mutex.
       * 
       * @return A packet to send data with
       */
      public NativePacketPayload getSharedSendPacket() {
          if (this.sharedSendPacket == null) {
              this.sharedSendPacket = new NativePacketPayload(INITIAL_PACKET_SIZE);
          }
          this.sharedSendPacket.setPosition(0);
  
          return this.sharedSendPacket;
      }
  
      private void calculateSlowQueryThreshold() {
          this.slowQueryThreshold = this.propertySet.getIntegerProperty(PropertyKey.slowQueryThresholdMillis).getValue();
  
          if (this.propertySet.getBooleanProperty(PropertyKey.useNanosForElapsedTime).getValue()) {
              long nanosThreshold = this.propertySet.getLongProperty(PropertyKey.slowQueryThresholdNanos).getValue();
  
              if (nanosThreshold != 0) {
                  this.slowQueryThreshold = nanosThreshold;
              } else {
                  this.slowQueryThreshold *= 1000000; // 1 million millis in a nano
              }
          }
      }
  
      /**
       * Re-authenticates as the given user and password
       * 
       * @param user
       *            user name
       * @param password
       *            password
       * @param database
       *            database name
       * 
       */
      public void changeUser(String user, String password, String database) {
          this.packetSequence = -1;
          this.packetSender = this.packetSender.undecorateAll();
          this.packetReader = this.packetReader.undecorateAll();
  
          this.authProvider.changeUser(user, password, database);
      }
  
      protected boolean useNanosForElapsedTime() {
          return this.useNanosForElapsedTime;
      }
  
      public long getSlowQueryThreshold() {
          return this.slowQueryThreshold;
      }
  
      public int getCommandCount() {
          return this.commandCount;
      }
  
      public void setQueryInterceptors(List<QueryInterceptor> queryInterceptors) {
          this.queryInterceptors = queryInterceptors.isEmpty() ? null : queryInterceptors;
      }
  
      public List<QueryInterceptor> getQueryInterceptors() {
          return this.queryInterceptors;
      }
  
      public void setSocketTimeout(int milliseconds) {
          try {
              Socket soc = this.socketConnection.getMysqlSocket();
              if (soc != null) {
                  soc.setSoTimeout(milliseconds);
              }
          } catch (IOException e) {
              throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Protocol.8"), e, getExceptionInterceptor());
          }
      }
  
      public void releaseResources() {
          if (this.compressedPacketSender != null) {
              this.compressedPacketSender.stop();
          }
      }
  
      public void connect(String user, String password, String database) {
          // session creation & initialization happens here
  
          beforeHandshake();
  
          this.authProvider.connect(user, password, database);
      }
  
      protected boolean isDataAvailable() {
          try {
              return this.socketConnection.getMysqlInput().available() > 0;
          } catch (IOException ioEx) {
              throw ExceptionFactory.createCommunicationsException(this.propertySet, this.serverSession, this.getPacketSentTimeHolder(),
                      this.getPacketReceivedTimeHolder(), ioEx, getExceptionInterceptor());
          }
      }
  
      public NativePacketPayload getReusablePacket() {
          return this.reusablePacket;
      }
  
      public int getWarningCount() {
          return this.warningCount;
      }
  
      public void setWarningCount(int warningCount) {
          this.warningCount = warningCount;
      }
  
      public void dumpPacketRingBuffer() {
          // use local variable to allow unsynchronized usage of the buffer
          LinkedList<StringBuilder> localPacketDebugRingBuffer = this.packetDebugRingBuffer;
          if (localPacketDebugRingBuffer != null) {
              StringBuilder dumpBuffer = new StringBuilder();
  
              dumpBuffer.append("Last " + localPacketDebugRingBuffer.size() + " packets received from server, from oldest->newest:\n");
              dumpBuffer.append("\n");
  
              for (Iterator<StringBuilder> ringBufIter = localPacketDebugRingBuffer.iterator(); ringBufIter.hasNext();) {
                  dumpBuffer.append(ringBufIter.next());
                  dumpBuffer.append("\n");
              }
  
              this.log.logTrace(dumpBuffer.toString());
          }
      }
  
      public boolean versionMeetsMinimum(int major, int minor, int subminor) {
          return this.serverSession.getServerVersion().meetsMinimum(new ServerVersion(major, minor, subminor));
      }
  
      public static MysqlType findMysqlType(PropertySet propertySet, int mysqlTypeId, short colFlag, long length, LazyString tableName,
              LazyString originalTableName, int collationIndex, String encoding) {
  
          boolean isUnsigned = ((colFlag & MysqlType.FIELD_FLAG_UNSIGNED) > 0);
          boolean isFromFunction = originalTableName.length() == 0;
          boolean isBinary = ((colFlag & MysqlType.FIELD_FLAG_BINARY) > 0);
          /**
           * Is this field owned by a server-created temporary table?
           */
          boolean isImplicitTemporaryTable = tableName.length() > 0 && tableName.toString().startsWith("#sql_");
  
          boolean isOpaqueBinary = (isBinary && collationIndex == CharsetMapping.MYSQL_COLLATION_INDEX_binary && (mysqlTypeId == MysqlType.FIELD_TYPE_STRING
                  || mysqlTypeId == MysqlType.FIELD_TYPE_VAR_STRING || mysqlTypeId == MysqlType.FIELD_TYPE_VARCHAR)) ?
          // queries resolved by temp tables also have this 'signature', check for that
                          !isImplicitTemporaryTable : "binary".equalsIgnoreCase(encoding);
  
          switch (mysqlTypeId) {
              case MysqlType.FIELD_TYPE_DECIMAL:
              case MysqlType.FIELD_TYPE_NEWDECIMAL:
                  return isUnsigned ? MysqlType.DECIMAL_UNSIGNED : MysqlType.DECIMAL;
  
              case MysqlType.FIELD_TYPE_TINY:
                  // Adjust for pseudo-boolean
                  if (!isUnsigned && length == 1 && propertySet.getBooleanProperty(PropertyKey.tinyInt1isBit).getValue()) {
                      if (propertySet.getBooleanProperty(PropertyKey.transformedBitIsBoolean).getValue()) {
                          return MysqlType.BOOLEAN;
                      }
                      return MysqlType.BIT;
                  }
                  return isUnsigned ? MysqlType.TINYINT_UNSIGNED : MysqlType.TINYINT;
  
              case MysqlType.FIELD_TYPE_SHORT:
                  return isUnsigned ? MysqlType.SMALLINT_UNSIGNED : MysqlType.SMALLINT;
  
              case MysqlType.FIELD_TYPE_LONG:
                  return isUnsigned ? MysqlType.INT_UNSIGNED : MysqlType.INT;
  
              case MysqlType.FIELD_TYPE_FLOAT:
                  return isUnsigned ? MysqlType.FLOAT_UNSIGNED : MysqlType.FLOAT;
  
              case MysqlType.FIELD_TYPE_DOUBLE:
                  return isUnsigned ? MysqlType.DOUBLE_UNSIGNED : MysqlType.DOUBLE;
  
              case MysqlType.FIELD_TYPE_NULL:
                  return MysqlType.NULL;
  
              case MysqlType.FIELD_TYPE_TIMESTAMP:
                  return MysqlType.TIMESTAMP;
  
              case MysqlType.FIELD_TYPE_LONGLONG:
                  return isUnsigned ? MysqlType.BIGINT_UNSIGNED : MysqlType.BIGINT;
  
              case MysqlType.FIELD_TYPE_INT24:
                  return isUnsigned ? MysqlType.MEDIUMINT_UNSIGNED : MysqlType.MEDIUMINT;
  
              case MysqlType.FIELD_TYPE_DATE:
                  return MysqlType.DATE;
  
              case MysqlType.FIELD_TYPE_TIME:
                  return MysqlType.TIME;
  
              case MysqlType.FIELD_TYPE_DATETIME:
                  return MysqlType.DATETIME;
  
              case MysqlType.FIELD_TYPE_YEAR:
                  return MysqlType.YEAR;
  
              case MysqlType.FIELD_TYPE_VARCHAR:
              case MysqlType.FIELD_TYPE_VAR_STRING:
  
                  if (isOpaqueBinary && !(isFromFunction && propertySet.getBooleanProperty(PropertyKey.functionsNeverReturnBlobs).getValue())) {
                      return MysqlType.VARBINARY;
                  }
  
                  return MysqlType.VARCHAR;
  
              case MysqlType.FIELD_TYPE_BIT:
                  //if (length > 1) {
                  // we need to pretend this is a full binary blob
                  //this.colFlag |= MysqlType.FIELD_FLAG_BINARY;
                  //this.colFlag |= MysqlType.FIELD_FLAG_BLOB;
                  //return MysqlType.VARBINARY;
                  //}
                  return MysqlType.BIT;
  
              case MysqlType.FIELD_TYPE_JSON:
                  return MysqlType.JSON;
  
              case MysqlType.FIELD_TYPE_ENUM:
                  return MysqlType.ENUM;
  
              case MysqlType.FIELD_TYPE_SET:
                  return MysqlType.SET;
  
              case MysqlType.FIELD_TYPE_TINY_BLOB:
                  if (!isBinary || collationIndex != CharsetMapping.MYSQL_COLLATION_INDEX_binary
                          || propertySet.getBooleanProperty(PropertyKey.blobsAreStrings).getValue()
                          || isFromFunction && (propertySet.getBooleanProperty(PropertyKey.functionsNeverReturnBlobs).getValue())) {
                      return MysqlType.TINYTEXT;
                  }
                  return MysqlType.TINYBLOB;
  
              case MysqlType.FIELD_TYPE_MEDIUM_BLOB:
                  if (!isBinary || collationIndex != CharsetMapping.MYSQL_COLLATION_INDEX_binary
                          || propertySet.getBooleanProperty(PropertyKey.blobsAreStrings).getValue()
                          || isFromFunction && (propertySet.getBooleanProperty(PropertyKey.functionsNeverReturnBlobs).getValue())) {
                      return MysqlType.MEDIUMTEXT;
                  }
                  return MysqlType.MEDIUMBLOB;
  
              case MysqlType.FIELD_TYPE_LONG_BLOB:
                  if (!isBinary || collationIndex != CharsetMapping.MYSQL_COLLATION_INDEX_binary
                          || propertySet.getBooleanProperty(PropertyKey.blobsAreStrings).getValue()
                          || isFromFunction && (propertySet.getBooleanProperty(PropertyKey.functionsNeverReturnBlobs).getValue())) {
                      return MysqlType.LONGTEXT;
                  }
                  return MysqlType.LONGBLOB;
  
              case MysqlType.FIELD_TYPE_BLOB:
                  // Sometimes MySQL uses this protocol-level type for all possible BLOB variants,
                  // we can divine what the actual type is by the length reported
  
                  int newMysqlTypeId = mysqlTypeId;
  
                  // fixing initial type according to length
                  if (length <= MysqlType.TINYBLOB.getPrecision()) {
                      newMysqlTypeId = MysqlType.FIELD_TYPE_TINY_BLOB;
  
                  } else if (length <= MysqlType.BLOB.getPrecision()) {
                      if (!isBinary || collationIndex != CharsetMapping.MYSQL_COLLATION_INDEX_binary
                              || propertySet.getBooleanProperty(PropertyKey.blobsAreStrings).getValue()
                              || isFromFunction && (propertySet.getBooleanProperty(PropertyKey.functionsNeverReturnBlobs).getValue())) {
                          newMysqlTypeId = MysqlType.FIELD_TYPE_VARCHAR;
                          return MysqlType.TEXT;
                      }
                      return MysqlType.BLOB;
  
                  } else if (length <= MysqlType.MEDIUMBLOB.getPrecision()) {
                      newMysqlTypeId = MysqlType.FIELD_TYPE_MEDIUM_BLOB;
                  } else {
                      newMysqlTypeId = MysqlType.FIELD_TYPE_LONG_BLOB;
                  }
  
                  // call this method again with correct this.mysqlType set
                  return findMysqlType(propertySet, newMysqlTypeId, colFlag, length, tableName, originalTableName, collationIndex, encoding);
  
              case MysqlType.FIELD_TYPE_STRING:
                  if (isOpaqueBinary && !propertySet.getBooleanProperty(PropertyKey.blobsAreStrings).getValue()) {
                      return MysqlType.BINARY;
                  }
                  return MysqlType.CHAR;
  
              case MysqlType.FIELD_TYPE_GEOMETRY:
                  return MysqlType.GEOMETRY;
  
              default:
                  return MysqlType.UNKNOWN;
          }
      }
  
      /*
       * Reading results
       */
  
      @Override
      public <T extends ProtocolEntity> T read(Class<T> requiredClass, ProtocolEntityFactory<T, NativePacketPayload> protocolEntityFactory) throws IOException {
          @SuppressWarnings("unchecked")
          ProtocolEntityReader<T, NativePacketPayload> sr = (ProtocolEntityReader<T, NativePacketPayload>) this.PROTOCOL_ENTITY_CLASS_TO_TEXT_READER
                  .get(requiredClass);
          if (sr == null) {
              throw ExceptionFactory.createException(FeatureNotAvailableException.class, "ProtocolEntityReader isn't available for class " + requiredClass);
          }
          return sr.read(protocolEntityFactory);
      }
  
      @Override
      public <T extends ProtocolEntity> T read(Class<Resultset> requiredClass, int maxRows, boolean streamResults, NativePacketPayload resultPacket,
              boolean isBinaryEncoded, ColumnDefinition metadata, ProtocolEntityFactory<T, NativePacketPayload> protocolEntityFactory) throws IOException {
          @SuppressWarnings("unchecked")
          ProtocolEntityReader<T, NativePacketPayload> sr = isBinaryEncoded
                  ? (ProtocolEntityReader<T, NativePacketPayload>) this.PROTOCOL_ENTITY_CLASS_TO_BINARY_READER.get(requiredClass)
                  : (ProtocolEntityReader<T, NativePacketPayload>) this.PROTOCOL_ENTITY_CLASS_TO_TEXT_READER.get(requiredClass);
          if (sr == null) {
              throw ExceptionFactory.createException(FeatureNotAvailableException.class, "ProtocolEntityReader isn't available for class " + requiredClass);
          }
          return sr.read(maxRows, streamResults, resultPacket, metadata, protocolEntityFactory);
      }
  
      /**
       * Read next result set from multi-result chain.
       * 
       * @param <T>
       *            extends {@link ProtocolEntity}
       * @param currentProtocolEntity
       *            T instance
       * @param maxRows
       *            rows limit
       * @param streamResults
       *            whether a stream result should be created
       * @param isBinaryEncoded
       *            true for binary protocol
       * @param resultSetFactory
       *            {@link ProtocolEntityFactory}
       * @return T instance
       * @throws IOException
       *             if an i/o error occurs
       */
      public <T extends ProtocolEntity> T readNextResultset(T currentProtocolEntity, int maxRows, boolean streamResults, boolean isBinaryEncoded,
              ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
  
          T result = null;
          if (Resultset.class.isAssignableFrom(currentProtocolEntity.getClass()) && this.serverSession.useMultiResults()) {
              if (this.serverSession.hasMoreResults()) {
  
                  T currentResultSet = currentProtocolEntity;
                  T newResultSet;
                  do {
                      NativePacketPayload fieldPacket = checkErrorMessage();
                      fieldPacket.setPosition(0);
                      newResultSet = read(Resultset.class, maxRows, streamResults, fieldPacket, isBinaryEncoded, null, resultSetFactory);
                      ((Resultset) currentResultSet).setNextResultset((Resultset) newResultSet);
                      currentResultSet = newResultSet;
  
                      if (result == null) {
                          // we should return the first result set in chain
                          result = currentResultSet;
                      }
                  } while (streamResults && this.serverSession.hasMoreResults() // we need to consume all result sets which don't contain rows from streamer right now,
                          && !((Resultset) currentResultSet).hasRows()); // because next data portion from streamer is available only via ResultsetRows.next() 
  
              }
          }
          return result;
      }
  
      public <T extends Resultset> T readAllResults(int maxRows, boolean streamResults, NativePacketPayload resultPacket, boolean isBinaryEncoded,
              ColumnDefinition metadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
  
          resultPacket.setPosition(0);
          T topLevelResultSet = read(Resultset.class, maxRows, streamResults, resultPacket, isBinaryEncoded, metadata, resultSetFactory);
  
          if (this.serverSession.hasMoreResults()) {
              T currentResultSet = topLevelResultSet;
              if (streamResults) {
                  currentResultSet = readNextResultset(currentResultSet, maxRows, true, isBinaryEncoded, resultSetFactory);
              } else {
                  while (this.serverSession.hasMoreResults()) {
                      currentResultSet = readNextResultset(currentResultSet, maxRows, false, isBinaryEncoded, resultSetFactory);
                  }
                  clearInputStream();
              }
          }
  
          if (this.hadWarnings) {
              scanForAndThrowDataTruncation();
          }
  
          reclaimLargeReusablePacket();
          return topLevelResultSet;
      }
  
      @SuppressWarnings("unchecked")
      public final <T> T readServerStatusForResultSets(NativePacketPayload rowPacket, boolean saveOldStatus) {
          T result = null;
          if (rowPacket.isEOFPacket()) {
              // read EOF packet
              rowPacket.setPosition(1); // skip the packet signature header
              this.warningCount = (int) rowPacket.readInteger(IntegerDataType.INT2);
              if (this.warningCount > 0) {
                  this.hadWarnings = true; // this is a 'latch', it's reset by sendCommand()
              }
  
              this.serverSession.setStatusFlags((int) rowPacket.readInteger(IntegerDataType.INT2), saveOldStatus);
              checkTransactionState();
          } else {
              // read OK packet
              OkPacket ok = OkPacket.parse(rowPacket, this.serverSession.getCharsetSettings().getErrorMessageEncoding());
              result = (T) ok;
  
              this.serverSession.setStatusFlags(ok.getStatusFlags(), saveOldStatus);
              this.serverSession.getServerSessionStateController().setSessionStateChanges(ok.getSessionStateChanges());
              checkTransactionState();
  
              this.warningCount = ok.getWarningCount();
              if (this.warningCount > 0) {
                  this.hadWarnings = true; // this is a 'latch', it's reset by sendCommand()
              }
          }
          return result;
      }
  
      @Override
      public <T extends QueryResult> T readQueryResult(ResultBuilder<T> resultBuilder) {
          throw ExceptionFactory.createException(CJOperationNotSupportedException.class, "Not supported");
      }
  
      public InputStream getLocalInfileInputStream() {
          return this.localInfileInputStream;
      }
  
      public void setLocalInfileInputStream(InputStream stream) {
          this.localInfileInputStream = stream;
      }
  
      /**
       * Reads and sends a file to the server for LOAD DATA LOCAL INFILE
       * 
       * @param fileName
       *            the file name to send.
       * @return NativePacketPayload
       */
      public final NativePacketPayload sendFileToServer(String fileName) {
          NativePacketPayload filePacket = (this.loadFileBufRef == null) ? null : this.loadFileBufRef.get();
  
          int bigPacketLength = Math.min(this.maxAllowedPacket.getValue() - (NativeConstants.HEADER_LENGTH * 3),
                  alignPacketSize(this.maxAllowedPacket.getValue() - 16, 4096) - (NativeConstants.HEADER_LENGTH * 3));
          int oneMeg = 1024 * 1024;
          int smallerPacketSizeAligned = Math.min(oneMeg - (NativeConstants.HEADER_LENGTH * 3),
                  alignPacketSize(oneMeg - 16, 4096) - (NativeConstants.HEADER_LENGTH * 3));
          int packetLength = Math.min(smallerPacketSizeAligned, bigPacketLength);
  
          if (filePacket == null) {
              try {
                  filePacket = new NativePacketPayload(packetLength);
                  this.loadFileBufRef = new SoftReference<>(filePacket);
              } catch (OutOfMemoryError oom) {
                  throw ExceptionFactory.createException(Messages.getString("MysqlIO.111", new Object[] { packetLength }),
                          MysqlErrorNumbers.SQL_STATE_MEMORY_ALLOCATION_ERROR, 0, false, oom, this.exceptionInterceptor);
              }
          }
  
          filePacket.setPosition(0);
  
          byte[] fileBuf = new byte[packetLength];
          BufferedInputStream fileIn = null;
          try {
              fileIn = getFileStream(fileName);
  
              int bytesRead = 0;
              while ((bytesRead = fileIn.read(fileBuf)) != -1) {
                  filePacket.setPosition(0);
                  filePacket.writeBytes(StringLengthDataType.STRING_FIXED, fileBuf, 0, bytesRead);
                  send(filePacket, filePacket.getPosition());
              }
          } catch (IOException ioEx) {
              boolean isParanoid = this.propertySet.getBooleanProperty(PropertyKey.paranoid).getValue();
  
              StringBuilder messageBuf = new StringBuilder(Messages.getString("MysqlIO.62"));
              if (fileName != null && !isParanoid) {
                  messageBuf.append("'");
                  messageBuf.append(fileName);
                  messageBuf.append("'");
              }
              messageBuf.append(Messages.getString("MysqlIO.63"));
              if (!isParanoid) {
                  messageBuf.append(Messages.getString("MysqlIO.64"));
                  messageBuf.append(Util.stackTraceToString(ioEx));
              }
  
              throw ExceptionFactory.createException(messageBuf.toString(), ioEx, this.exceptionInterceptor);
          } finally {
              if (fileIn != null) {
                  try {
                      fileIn.close();
                  } catch (Exception ex) {
                      throw ExceptionFactory.createException(Messages.getString("MysqlIO.65"), ex, this.exceptionInterceptor);
                  }
  
                  fileIn = null;
              } else {
                  // File open failed, but server needs one packet.
                  filePacket.setPosition(0);
                  send(filePacket, filePacket.getPosition());
                  checkErrorMessage(); // To clear response off of queue.
              }
          }
  
          // send empty packet to mark EOF
          filePacket.setPosition(0);
          send(filePacket, filePacket.getPosition());
  
          return checkErrorMessage();
      }
  
      private BufferedInputStream getFileStream(String fileName) throws IOException {
          RuntimeProperty<Boolean> allowLoadLocalInfile = this.propertySet.getBooleanProperty(PropertyKey.allowLoadLocalInfile);
          RuntimeProperty<String> allowLoadLocaInfileInPath = this.propertySet.getStringProperty(PropertyKey.allowLoadLocalInfileInPath);
          RuntimeProperty<Boolean> allowUrlInLocalInfile = this.propertySet.getBooleanProperty(PropertyKey.allowUrlInLocalInfile);
  
          if (!allowLoadLocalInfile.getValue() && !allowLoadLocaInfileInPath.isExplicitlySet()) {
              throw ExceptionFactory.createException(Messages.getString("MysqlIO.LoadDataLocalNotAllowed"), this.exceptionInterceptor);
          }
  
          if (allowLoadLocalInfile.getValue()) {
              // "LOAD DATA LOCAL INFILE" is enabled without restrictions.
              InputStream hookedStream = getLocalInfileInputStream();
              if (hookedStream != null) {
                  return new BufferedInputStream(hookedStream);
              } else if (allowUrlInLocalInfile.getValue()) {
                  // Look for ':'.
                  if (fileName.indexOf(':') != -1) {
                      try {
                          URL urlFromFileName = new URL(fileName);
                          return new BufferedInputStream(urlFromFileName.openStream());
                      } catch (MalformedURLException e) {
                          // Ignore and fall back to trying this as a file input stream.
                      }
                  }
              }
              return new BufferedInputStream(new FileInputStream(fileName));
          }
  
          // Given the code paths above, allowLoadLocaInfileInPath.isExplicitlySet() must be true and restrictions to "LOAD DATA LOCAL INFILE" apply.
          String safePathValue = allowLoadLocaInfileInPath.getValue();
          Path safePath;
          if (safePathValue.length() == 0) {
              throw ExceptionFactory.createException(
                      Messages.getString("MysqlIO.60", new Object[] { safePathValue, PropertyKey.allowLoadLocalInfileInPath.getKeyName() }),
                      this.exceptionInterceptor);
          }
          try {
              safePath = Paths.get(safePathValue).toRealPath();
          } catch (IOException | InvalidPathException e) {
              throw ExceptionFactory.createException(
                      Messages.getString("MysqlIO.60", new Object[] { safePathValue, PropertyKey.allowLoadLocalInfileInPath.getKeyName() }), e,
                      this.exceptionInterceptor);
          }
  
          if (allowUrlInLocalInfile.getValue()) {
              try {
                  URL urlFromFileName = new URL(fileName);
  
                  if (!urlFromFileName.getProtocol().equalsIgnoreCase("file")) {
                      throw ExceptionFactory.createException(Messages.getString("MysqlIO.66", new Object[] { urlFromFileName.getProtocol() }),
                              this.exceptionInterceptor);
                  }
  
                  try {
                      InetAddress addr = InetAddress.getByName(urlFromFileName.getHost());
                      if (!addr.isLoopbackAddress()) {
                          throw ExceptionFactory.createException(Messages.getString("MysqlIO.67", new Object[] { urlFromFileName.getHost() }),
                                  this.exceptionInterceptor);
                      }
                  } catch (UnknownHostException e) {
                      throw ExceptionFactory.createException(Messages.getString("MysqlIO.68", new Object[] { fileName }), e, this.exceptionInterceptor);
                  }
  
                  Path filePath = null;
                  try {
                      filePath = Paths.get(urlFromFileName.toURI()).toRealPath();
                  } catch (InvalidPathException e) {
                      // Windows paths often can't be extracted, but the URL is still valid.
                      String pathString = urlFromFileName.getPath();
                      if (pathString.indexOf(':') != -1 && (pathString.startsWith("/") || pathString.startsWith("\\"))) {
                          pathString = pathString.replaceFirst("^[/\\\\]*", "");
                      }
                      filePath = Paths.get(pathString).toRealPath();
                  } catch (IllegalArgumentException e) {
                      // Try the path directly.
                      filePath = Paths.get(urlFromFileName.getPath()).toRealPath();
                  }
                  if (!filePath.startsWith(safePath)) {
                      throw ExceptionFactory.createException(Messages.getString("MysqlIO.61", new Object[] { filePath, safePath }), this.exceptionInterceptor);
                  }
  
                  return new BufferedInputStream(urlFromFileName.openStream());
              } catch (MalformedURLException | URISyntaxException e) {
                  // Fall back to trying this as a file input stream.
              }
          }
  
          Path filePath = Paths.get(fileName).toRealPath();
          if (!filePath.startsWith(safePath)) {
              throw ExceptionFactory.createException(Messages.getString("MysqlIO.61", new Object[] { filePath, safePath }), this.exceptionInterceptor);
          }
          return new BufferedInputStream(new FileInputStream(filePath.toFile()));
      }
  
      private int alignPacketSize(int a, int l) {
          return ((((a) + (l)) - 1) & ~((l) - 1));
      }
  
      private ResultsetRows streamingData = null;
  
      public ResultsetRows getStreamingData() {
          return this.streamingData;
      }
  
      public void setStreamingData(ResultsetRows streamingData) {
          this.streamingData = streamingData;
      }
  
      public void checkForOutstandingStreamingData() {
          if (this.streamingData != null) {
              boolean shouldClobber = this.propertySet.getBooleanProperty(PropertyKey.clobberStreamingResults).getValue();
  
              if (!shouldClobber) {
                  throw ExceptionFactory.createException(Messages.getString("MysqlIO.39") + this.streamingData + Messages.getString("MysqlIO.40")
                          + Messages.getString("MysqlIO.41") + Messages.getString("MysqlIO.42"), this.exceptionInterceptor);
              }
  
              // Close the result set
              this.streamingData.getOwner().closeOwner(false);
  
              // clear any pending data....
              clearInputStream();
          }
      }
  
      public void unsetStreamingData(ResultsetRows streamer) {
          if (this.streamingData == null) {
              throw ExceptionFactory.createException(Messages.getString("MysqlIO.17") + streamer + Messages.getString("MysqlIO.18"), this.exceptionInterceptor);
          }
  
          // don't try to discard streamer other than passed in; it's probably already replaced by the next result in a multi-resultset
          if (streamer == this.streamingData) {
              this.streamingData = null;
          }
      }
  
      public void scanForAndThrowDataTruncation() {
          if (this.streamingData == null && this.propertySet.getBooleanProperty(PropertyKey.jdbcCompliantTruncation).getValue() && getWarningCount() > 0) {
              int warningCountOld = getWarningCount();
              convertShowWarningsToSQLWarnings(true);
              setWarningCount(warningCountOld);
          }
      }
  
      public StringBuilder generateQueryCommentBlock(StringBuilder buf) {
          buf.append("/* conn id ");
          buf.append(getServerSession().getCapabilities().getThreadId());
          buf.append(" clock: ");
          buf.append(System.currentTimeMillis());
          buf.append(" */ ");
  
          return buf;
      }
  
      public BaseMetricsHolder getMetricsHolder() {
          return this.metricsHolder;
      }
  
      @Override
      public String getQueryComment() {
          return this.queryComment;
      }
  
      @Override
      public void setQueryComment(String comment) {
          this.queryComment = comment;
      }
  
      private void appendDeadlockStatusInformation(Session sess, String xOpen, StringBuilder errorBuf) {
          if (sess.getPropertySet().getBooleanProperty(PropertyKey.includeInnodbStatusInDeadlockExceptions).getValue() && xOpen != null
                  && (xOpen.startsWith("40") || xOpen.startsWith("41")) && getStreamingData() == null) {
  
              try {
                  NativePacketPayload resultPacket = sendCommand(getCommandBuilder().buildComQuery(getSharedSendPacket(), "SHOW ENGINE INNODB STATUS"), false, 0);
  
                  Resultset rs = readAllResults(-1, false, resultPacket, false, null, new ResultsetFactory(Type.FORWARD_ONLY, null));
  
                  int colIndex = 0;
                  Field f = null;
                  for (int i = 0; i < rs.getColumnDefinition().getFields().length; i++) {
                      f = rs.getColumnDefinition().getFields()[i];
                      if ("Status".equals(f.getName())) {
                          colIndex = i;
                          break;
                      }
                  }
  
                  ValueFactory<String> vf = new StringValueFactory(this.propertySet);
  
                  Row r;
                  if ((r = rs.getRows().next()) != null) {
                      errorBuf.append("\n\n").append(r.getValue(colIndex, vf));
                  } else {
                      errorBuf.append("\n\n").append(Messages.getString("MysqlIO.NoInnoDBStatusFound"));
                  }
              } catch (IOException | CJException ex) {
                  errorBuf.append("\n\n").append(Messages.getString("MysqlIO.InnoDBStatusFailed")).append("\n\n").append(Util.stackTraceToString(ex));
              }
          }
  
          if (sess.getPropertySet().getBooleanProperty(PropertyKey.includeThreadDumpInDeadlockExceptions).getValue()) {
              errorBuf.append("\n\n*** Java threads running at time of deadlock ***\n\n");
  
              ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
              long[] threadIds = threadMBean.getAllThreadIds();
  
              ThreadInfo[] threads = threadMBean.getThreadInfo(threadIds, Integer.MAX_VALUE);
              List<ThreadInfo> activeThreads = new ArrayList<>();
  
              for (ThreadInfo info : threads) {
                  if (info != null) {
                      activeThreads.add(info);
                  }
              }
  
              for (ThreadInfo threadInfo : activeThreads) {
                  // "Thread-60" daemon prio=1 tid=0x093569c0 nid=0x1b99 in Object.wait()
  
                  errorBuf.append('"').append(threadInfo.getThreadName()).append("\" tid=").append(threadInfo.getThreadId()).append(" ")
                          .append(threadInfo.getThreadState());
  
                  if (threadInfo.getLockName() != null) {
                      errorBuf.append(" on lock=").append(threadInfo.getLockName());
                  }
                  if (threadInfo.isSuspended()) {
                      errorBuf.append(" (suspended)");
                  }
                  if (threadInfo.isInNative()) {
                      errorBuf.append(" (running in native)");
                  }
  
                  StackTraceElement[] stackTrace = threadInfo.getStackTrace();
  
                  if (stackTrace.length > 0) {
                      errorBuf.append(" in ");
                      errorBuf.append(stackTrace[0].getClassName()).append(".");
                      errorBuf.append(stackTrace[0].getMethodName()).append("()");
                  }
  
                  errorBuf.append("\n");
  
                  if (threadInfo.getLockOwnerName() != null) {
                      errorBuf.append("\t owned by ").append(threadInfo.getLockOwnerName()).append(" Id=").append(threadInfo.getLockOwnerId()).append("\n");
                  }
  
                  for (int j = 0; j < stackTrace.length; j++) {
                      StackTraceElement ste = stackTrace[j];
                      errorBuf.append("\tat ").append(ste.toString()).append("\n");
                  }
              }
          }
      }
  
      private StringBuilder appendResultSetSlashGStyle(StringBuilder appendTo, Resultset rs) {
          Field[] fields = rs.getColumnDefinition().getFields();
          int maxWidth = 0;
          for (int i = 0; i < fields.length; i++) {
              if (fields[i].getColumnLabel().length() > maxWidth) {
                  maxWidth = fields[i].getColumnLabel().length();
              }
          }
  
          int rowCount = 1;
          Row r;
          while ((r = rs.getRows().next()) != null) {
              appendTo.append("*************************** ");
              appendTo.append(rowCount++);
              appendTo.append(". row ***************************\n");
  
              for (int i = 0; i < fields.length; i++) {
                  int leftPad = maxWidth - fields[i].getColumnLabel().length();
                  for (int j = 0; j < leftPad; j++) {
                      appendTo.append(" ");
                  }
                  appendTo.append(fields[i].getColumnLabel()).append(": ");
                  String stringVal = r.getValue(i, new StringValueFactory(this.propertySet));
                  appendTo.append(stringVal != null ? stringVal : "NULL").append("\n");
              }
              appendTo.append("\n");
          }
          return appendTo;
      }
  
      /**
       * Turns output of 'SHOW WARNINGS' into JDBC SQLWarning instances.
       * 
       * If 'forTruncationOnly' is true, only looks for truncation warnings, and
       * actually throws DataTruncation as an exception.
       * 
       * @param forTruncationOnly
       *            if this method should only scan for data truncation warnings
       * 
       * @return the SQLWarning chain (or null if no warnings)
       */
      public SQLWarning convertShowWarningsToSQLWarnings(boolean forTruncationOnly) {
          if (this.warningCount == 0) {
              return null;
          }
  
          SQLWarning currentWarning = null;
          ResultsetRows rows = null;
  
          try {
              /*
               * +---------+------+---------------------------------------------+
               * | Level ..| Code | Message ....................................|
               * +---------+------+---------------------------------------------+
               * | Warning | 1265 | Data truncated for column 'field1' at row 1 |
               * +---------+------+---------------------------------------------+
               */
              NativePacketPayload resultPacket = sendCommand(getCommandBuilder().buildComQuery(getSharedSendPacket(), "SHOW WARNINGS"), false, 0);
  
              Resultset warnRs = readAllResults(-1, this.warningCount > 99 /* stream large warning counts */, resultPacket, false, null,
                      new ResultsetFactory(Type.FORWARD_ONLY, Concurrency.READ_ONLY));
  
              int codeFieldIndex = warnRs.getColumnDefinition().findColumn("Code", false, 1) - 1;
              int messageFieldIndex = warnRs.getColumnDefinition().findColumn("Message", false, 1) - 1;
  
              ValueFactory<String> svf = new StringValueFactory(this.propertySet);
              ValueFactory<Integer> ivf = new IntegerValueFactory(this.propertySet);
  
              rows = warnRs.getRows();
              Row r;
              while ((r = rows.next()) != null) {
  
                  int code = r.getValue(codeFieldIndex, ivf);
  
                  if (forTruncationOnly) {
                      if (code == MysqlErrorNumbers.ER_WARN_DATA_TRUNCATED || code == MysqlErrorNumbers.ER_WARN_DATA_OUT_OF_RANGE) {
                          DataTruncation newTruncation = new MysqlDataTruncation(r.getValue(messageFieldIndex, svf), 0, false, false, 0, 0, code);
  
                          if (currentWarning == null) {
                              currentWarning = newTruncation;
                          } else {
                              currentWarning.setNextWarning(newTruncation);
                          }
                      }
                  } else {
                      //String level = warnRs.getString("Level"); 
                      String message = r.getValue(messageFieldIndex, svf);
  
                      SQLWarning newWarning = new SQLWarning(message, MysqlErrorNumbers.mysqlToSqlState(code), code);
                      if (currentWarning == null) {
                          currentWarning = newWarning;
                      } else {
                          currentWarning.setNextWarning(newWarning);
                      }
                  }
              }
  
              if (forTruncationOnly && (currentWarning != null)) {
                  throw ExceptionFactory.createException(currentWarning.getMessage(), currentWarning);
              }
  
              return currentWarning;
          } catch (IOException ex) {
              throw ExceptionFactory.createException(ex.getMessage(), ex);
          } finally {
              if (rows != null) {
                  rows.close();
              }
          }
      }
  
      @Override
      public ColumnDefinition readMetadata() {
          throw ExceptionFactory.createException(CJOperationNotSupportedException.class, "Not supported");
      }
  
      @Override
      public void close() throws IOException {
          throw ExceptionFactory.createException(CJOperationNotSupportedException.class, "Not supported");
      }
  
      /**
       * Configures the client's timezone if required.
       * 
       * @throws CJException
       *             if the timezone the server is configured to use can't be
       *             mapped to a Java timezone.
       */
      public void configureTimeZone() {
          String connectionTimeZone = getPropertySet().getStringProperty(PropertyKey.connectionTimeZone).getValue();
  
          TimeZone selectedTz = null;
  
          if (connectionTimeZone == null || StringUtils.isEmptyOrWhitespaceOnly(connectionTimeZone) || "LOCAL".equals(connectionTimeZone)) {
              selectedTz = TimeZone.getDefault();
  
          } else if ("SERVER".equals(connectionTimeZone)) {
              // Session time zone will be detected after the first ServerSession.getSessionTimeZone() call.
              return;
  
          } else {
              selectedTz = TimeZone.getTimeZone(ZoneId.of(connectionTimeZone)); // TODO use ZoneId.of(String zoneId, Map<String, String> aliasMap) for custom abbreviations support
          }
  
          this.serverSession.setSessionTimeZone(selectedTz);
  
          if (getPropertySet().getBooleanProperty(PropertyKey.forceConnectionTimeZoneToSession).getValue()) {
              // TODO don't send 'SET SESSION time_zone' if time_zone is already equal to the selectedTz (but it requires time zone detection)
  
              StringBuilder query = new StringBuilder("SET SESSION time_zone='");
  
              ZoneId zid = selectedTz.toZoneId().normalized();
              if (zid instanceof ZoneOffset) {
                  String offsetStr = ((ZoneOffset) zid).getId().replace("Z", "+00:00");
                  query.append(offsetStr);
                  this.serverSession.getServerVariables().put("time_zone", offsetStr);
              } else {
                  query.append(selectedTz.getID());
                  this.serverSession.getServerVariables().put("time_zone", selectedTz.getID());
              }
  
              query.append("'");
              sendCommand(getCommandBuilder().buildComQuery(null, query.toString()), false, 0);
          }
      }
  
      @Override
      public void initServerSession() {
          configureTimeZone();
  
          if (this.serverSession.getServerVariables().containsKey("max_allowed_packet")) {
              int serverMaxAllowedPacket = this.serverSession.getServerVariable("max_allowed_packet", -1);
  
              // use server value if maxAllowedPacket hasn't been given, or max_allowed_packet is smaller
              if (serverMaxAllowedPacket != -1 && (!this.maxAllowedPacket.isExplicitlySet() || serverMaxAllowedPacket < this.maxAllowedPacket.getValue())) {
                  this.maxAllowedPacket.setValue(serverMaxAllowedPacket);
              }
  
              if (this.useServerPrepStmts.getValue()) {
                  RuntimeProperty<Integer> blobSendChunkSize = this.propertySet.getProperty(PropertyKey.blobSendChunkSize);
                  int preferredBlobSendChunkSize = blobSendChunkSize.getValue();
  
                  // LONG_DATA and MySQLIO packet header size
                  int packetHeaderSize = ServerPreparedQuery.BLOB_STREAM_READ_BUF_SIZE + 11;
                  int allowedBlobSendChunkSize = Math.min(preferredBlobSendChunkSize, this.maxAllowedPacket.getValue()) - packetHeaderSize;
  
                  if (allowedBlobSendChunkSize <= 0) {
                      throw ExceptionFactory.createException(Messages.getString("Connection.15", new Object[] { packetHeaderSize }),
                              MysqlErrorNumbers.SQL_STATE_INVALID_CONNECTION_ATTRIBUTE, 0, false, null, this.exceptionInterceptor);
                  }
  
                  blobSendChunkSize.setValue(allowedBlobSendChunkSize);
              }
          }
  
          this.serverSession.getCharsetSettings().configurePostHandshake(false);
      }
  }
  