content: /*
   * Copyright (c) 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.ByteArrayOutputStream;
  import java.math.BigDecimal;
  import java.sql.Time;
  import java.sql.Timestamp;
  import java.time.Duration;
  import java.time.LocalDateTime;
  import java.time.LocalTime;
  import java.util.concurrent.TimeUnit;
  
  import com.mysql.cj.BindValue;
  import com.mysql.cj.Messages;
  import com.mysql.cj.MysqlType;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.PropertySet;
  import com.mysql.cj.conf.RuntimeProperty;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.ExceptionInterceptor;
  import com.mysql.cj.exceptions.WrongArgumentException;
  import com.mysql.cj.protocol.InternalDate;
  import com.mysql.cj.protocol.InternalTime;
  import com.mysql.cj.protocol.InternalTimestamp;
  import com.mysql.cj.protocol.Message;
  import com.mysql.cj.protocol.ServerSession;
  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.result.Field;
  import com.mysql.cj.util.StringUtils;
  import com.mysql.cj.util.TimeUtil;
  
  public abstract class AbstractValueEncoder implements ValueEncoder {
  
      protected PropertySet propertySet;
      protected ServerSession serverSession;
      protected ExceptionInterceptor exceptionInterceptor;
      protected RuntimeProperty<String> charEncoding = null;
      protected RuntimeProperty<Boolean> sendFractionalSeconds;
      protected RuntimeProperty<Boolean> sendFractionalSecondsForTime;
      protected RuntimeProperty<Boolean> preserveInstants;
  
      @Override
      public void init(PropertySet pset, ServerSession serverSess, ExceptionInterceptor excInterceptor) {
          this.propertySet = pset;
          this.serverSession = serverSess;
          this.exceptionInterceptor = excInterceptor;
          this.charEncoding = pset.getStringProperty(PropertyKey.characterEncoding);
          this.sendFractionalSeconds = pset.getBooleanProperty(PropertyKey.sendFractionalSeconds);
          this.sendFractionalSecondsForTime = pset.getBooleanProperty(PropertyKey.sendFractionalSecondsForTime);
          this.preserveInstants = pset.getBooleanProperty(PropertyKey.preserveInstants);
      }
  
      @Override
      public byte[] getBytes(BindValue binding) {
          return StringUtils.getBytes(getString(binding), this.charEncoding.getValue());
      }
  
      @Override
      public void encodeAsText(Message msg, BindValue binding) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          intoPacket.writeBytes(StringLengthDataType.STRING_FIXED, getBytes(binding));
      }
  
      @Override
      public void encodeAsQueryAttribute(Message msg, BindValue binding) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          String x = binding.getValue().toString();
          intoPacket.writeBytes(StringSelfDataType.STRING_LENENC, StringUtils.getBytes(x, this.charEncoding.getValue()));
      }
  
      protected BigDecimal getScaled(BigDecimal x, long scaleOrLength) {
          BigDecimal scaledBigDecimal;
          if (scaleOrLength < 0) {
              return x.setScale(x.scale());
          }
          try {
              scaledBigDecimal = x.setScale((int) scaleOrLength);
          } catch (ArithmeticException ex) {
              try {
                  scaledBigDecimal = x.setScale((int) scaleOrLength, BigDecimal.ROUND_HALF_UP);
              } catch (ArithmeticException arEx) {
                  throw ExceptionFactory.createException(WrongArgumentException.class,
                          Messages.getString("PreparedStatement.65", new Object[] { scaleOrLength, x.toPlainString() }), this.exceptionInterceptor);
              }
          }
          return scaledBigDecimal;
      }
  
      protected LocalTime adjustLocalTime(LocalTime x, Field f) {
          if (!this.serverSession.getCapabilities().serverSupportsFracSecs() || !this.sendFractionalSeconds.getValue()) {
              if (x.getNano() > 0) {
                  x = x.withNano(0); // truncate nanoseconds
              }
              return x;
          }
          return TimeUtil.adjustNanosPrecision(x, f == null ? 6 : f.getDecimals(), !this.serverSession.isServerTruncatesFracSecs());
      }
  
      protected LocalDateTime adjustLocalDateTime(LocalDateTime x, Field f) {
          if (!this.serverSession.getCapabilities().serverSupportsFracSecs() || !this.sendFractionalSeconds.getValue()) {
              if (x.getNano() > 0) {
                  x = x.withNano(0); // truncate nanoseconds
              }
              return x;
          }
          return TimeUtil.adjustNanosPrecision(x, f == null ? 6 : f.getDecimals(), !this.serverSession.isServerTruncatesFracSecs());
      }
  
      protected Duration adjustDuration(Duration x, Field f) {
          if (!this.serverSession.getCapabilities().serverSupportsFracSecs() || !this.sendFractionalSeconds.getValue()) {
              if (x.getNano() > 0) {
                  x = x.isNegative() ? x.plusSeconds(1).withNanos(0) : x.withNanos(0); // truncate nanoseconds
              }
              return x;
          }
          return TimeUtil.adjustNanosPrecision(x, f == null ? 6 : f.getDecimals(), !this.serverSession.isServerTruncatesFracSecs());
      }
  
      protected Timestamp adjustTimestamp(Timestamp x, Field f, boolean keepOrigNanos) {
          if (keepOrigNanos) {
              return x; // if the value is set as a key for UpdatableResultSet updater, it should keep original milliseconds
          }
          if (!this.serverSession.getCapabilities().serverSupportsFracSecs() || !this.sendFractionalSeconds.getValue()) {
              return TimeUtil.truncateFractionalSeconds(x);
          }
          return TimeUtil.adjustNanosPrecision(x, f == null ? 6 : f.getDecimals(), !this.serverSession.isServerTruncatesFracSecs());
      }
  
      protected Time adjustTime(Time x) {
          if (!this.serverSession.getCapabilities().serverSupportsFracSecs() || !this.sendFractionalSeconds.getValue()
                  || !this.sendFractionalSecondsForTime.getValue()) {
              return TimeUtil.truncateFractionalSeconds(x);
          }
          return x;
      }
  
      protected void writeDate(Message msg, InternalDate d) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          intoPacket.ensureCapacity(NativeConstants.BIN_LEN_DATE + 1);
          intoPacket.writeInteger(IntegerDataType.INT1, NativeConstants.BIN_LEN_DATE);
          intoPacket.writeInteger(IntegerDataType.INT2, d.getYear());
          intoPacket.writeInteger(IntegerDataType.INT1, d.getMonth());
          intoPacket.writeInteger(IntegerDataType.INT1, d.getDay());
      }
  
      protected void writeTime(Message msg, InternalTime time) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          boolean hasFractionalSeconds = time.getNanos() > 0;
          intoPacket.ensureCapacity((hasFractionalSeconds ? NativeConstants.BIN_LEN_TIME_WITH_MICROS : NativeConstants.BIN_LEN_TIME_NO_FRAC) + 1);
          intoPacket.writeInteger(IntegerDataType.INT1, hasFractionalSeconds ? NativeConstants.BIN_LEN_TIME_WITH_MICROS : NativeConstants.BIN_LEN_TIME_NO_FRAC);
          intoPacket.writeInteger(IntegerDataType.INT1, time.isNegative() ? 1 : 0);
          intoPacket.writeInteger(IntegerDataType.INT4, time.getHours() / 24);
          intoPacket.writeInteger(IntegerDataType.INT1, time.getHours() % 24);
          intoPacket.writeInteger(IntegerDataType.INT1, time.getMinutes());
          intoPacket.writeInteger(IntegerDataType.INT1, time.getSeconds());
          if (hasFractionalSeconds) {
              intoPacket.writeInteger(IntegerDataType.INT4, TimeUnit.NANOSECONDS.toMicros(time.getNanos()));
          }
      }
  
      protected void writeDateTime(Message msg, InternalTimestamp ts) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          long microseconds = TimeUnit.NANOSECONDS.toMicros(ts.getNanos());
          intoPacket.ensureCapacity((microseconds > 0 ? NativeConstants.BIN_LEN_TIMESTAMP_WITH_MICROS : NativeConstants.BIN_LEN_TIMESTAMP_NO_FRAC) + 1);
          intoPacket.writeInteger(IntegerDataType.INT1,
                  microseconds > 0 ? NativeConstants.BIN_LEN_TIMESTAMP_WITH_MICROS : NativeConstants.BIN_LEN_TIMESTAMP_NO_FRAC);
          intoPacket.writeInteger(IntegerDataType.INT2, ts.getYear());
          intoPacket.writeInteger(IntegerDataType.INT1, ts.getMonth());
          intoPacket.writeInteger(IntegerDataType.INT1, ts.getDay());
          intoPacket.writeInteger(IntegerDataType.INT1, ts.getHours());
          intoPacket.writeInteger(IntegerDataType.INT1, ts.getMinutes());
          intoPacket.writeInteger(IntegerDataType.INT1, ts.getSeconds());
          if (microseconds > 0) {
              intoPacket.writeInteger(IntegerDataType.INT4, microseconds);
          }
      }
  
      public void writeDateTimeWithOffset(Message msg, InternalTimestamp timestamp) {
          NativePacketPayload intoPacket = (NativePacketPayload) msg;
          intoPacket.ensureCapacity(NativeConstants.BIN_LEN_TIMESTAMP_WITH_TZ + 1);
          intoPacket.writeInteger(IntegerDataType.INT1, NativeConstants.BIN_LEN_TIMESTAMP_WITH_TZ);
          intoPacket.writeInteger(IntegerDataType.INT2, timestamp.getYear());
          intoPacket.writeInteger(IntegerDataType.INT1, timestamp.getMonth());
          intoPacket.writeInteger(IntegerDataType.INT1, timestamp.getDay());
          intoPacket.writeInteger(IntegerDataType.INT1, timestamp.getHours());
          intoPacket.writeInteger(IntegerDataType.INT1, timestamp.getMinutes());
          intoPacket.writeInteger(IntegerDataType.INT1, timestamp.getSeconds());
          intoPacket.writeInteger(IntegerDataType.INT4, TimeUnit.NANOSECONDS.toMicros(timestamp.getNanos()));
          intoPacket.writeInteger(IntegerDataType.INT2, timestamp.getOffset());
      }
  
      protected byte[] escapeBytesIfNeeded(byte[] x) {
          if (this.serverSession.isNoBackslashEscapesSet() || this.serverSession.getCharsetSettings().isMultibyteCharset(this.charEncoding.getValue())) {
  
              // Send as hex
              ByteArrayOutputStream bOut = new ByteArrayOutputStream((x.length * 2) + 3);
              bOut.write('x');
              bOut.write('\'');
  
              StringUtils.hexEscapeBlock(x, x.length, (lowBits, highBits) -> {
                  bOut.write(lowBits);
                  bOut.write(highBits);
              });
              bOut.write('\'');
              return bOut.toByteArray();
          }
  
          // escape them
          ByteArrayOutputStream bOut = new ByteArrayOutputStream(x.length + 9);
          bOut.write('_');
          bOut.write('b');
          bOut.write('i');
          bOut.write('n');
          bOut.write('a');
          bOut.write('r');
          bOut.write('y');
          bOut.write('\'');
          StringUtils.escapeBytes(bOut, x);
          bOut.write('\'');
          return bOut.toByteArray();
      }
  
      @Override
      public long getTextLength(BindValue binding) {
          if (binding.isNull()) {
              return 4 /* for NULL literal in SQL */;
          }
          return binding.isStream() && binding.getScaleOrLength() != -1 ? binding.getScaleOrLength() * 2 /* for safety in escaping */
                  : binding.getByteValue().length;
      }
  
      @Override
      public long getBinaryLength(BindValue binding) {
          if (binding.isNull()) {
              return 0;
          }
  
          if (binding.isStream() && binding.getScaleOrLength() != -1) {
              return binding.getScaleOrLength();
          }
  
          int bufferType = binding.getFieldType();
          switch (bufferType) {
              case MysqlType.FIELD_TYPE_TINY:
                  return NativeConstants.BIN_LEN_INT1;
              case MysqlType.FIELD_TYPE_SHORT:
                  return NativeConstants.BIN_LEN_INT2;
              case MysqlType.FIELD_TYPE_LONG:
                  return NativeConstants.BIN_LEN_INT4;
              case MysqlType.FIELD_TYPE_LONGLONG:
                  return NativeConstants.BIN_LEN_INT8;
              case MysqlType.FIELD_TYPE_FLOAT:
                  return NativeConstants.BIN_LEN_FLOAT;
              case MysqlType.FIELD_TYPE_DOUBLE:
                  return NativeConstants.BIN_LEN_DOUBLE;
              case MysqlType.FIELD_TYPE_DATE:
                  return NativeConstants.BIN_LEN_DATE + 1;
              case MysqlType.FIELD_TYPE_TIME:
                  return NativeConstants.BIN_LEN_TIME_WITH_MICROS + 1;
              case MysqlType.FIELD_TYPE_DATETIME:
                  return NativeConstants.BIN_LEN_TIMESTAMP_WITH_MICROS + 1;
              case MysqlType.FIELD_TYPE_TIMESTAMP:
                  return NativeConstants.BIN_LEN_TIMESTAMP_WITH_TZ + 1;
              case MysqlType.FIELD_TYPE_VAR_STRING:
                  return binding.getValue().toString().length() + 9;
              case MysqlType.FIELD_TYPE_STRING:
              case MysqlType.FIELD_TYPE_VARCHAR:
              case MysqlType.FIELD_TYPE_DECIMAL:
              case MysqlType.FIELD_TYPE_NEWDECIMAL:
                  if (binding.getValue() instanceof byte[]) {
                      return ((byte[]) binding.getValue()).length;
                  } else if (binding.getValue() instanceof BigDecimal) {
                      return ((BigDecimal) binding.getValue()).toPlainString().length();
                  }
                  return ((String) binding.getValue()).length();
              default:
                  return 0;
          }
      }
  }
  