# DatetimetzType.java

    /*
    ** Module   : DatetimetzType.java
    ** Abstract : Handles conversion from/to SQL/FWD datetimetz type.
    **
    ** Copyright (c) 2019-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 OM  20191101 Created initial version.
    ** 002 CA  20200910 Fixed the datetime(-tz) field's initial value - it uses ISO8601 format or
    **                  NOW function.
    **     ECF 20200919 Performance: removed use of ResultSet.wasNull. ResultSet.getTimestamp already returns null
    **                  in the case of SQL NULL.
    **     CA  20210304 Fixed datetimetz literal parsing.
    **     OM  20220218 Fixed timezone when the dtz is rehydrated.
    **     ECF 20221005 Added support for dynamic initial values.
    **     CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
    **                  property type.
    **     OM  20221117 Some properties (datetime-tz) may take a different number of positional parameters when
    **                  used in insert/update or where predicate.
    **     CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
    **                  property type.
    ** 003 CA  20240318 INITIAL value for a DATETIME(-TZ) field will always use 'mdy' as date format.
    ** 004 OM  20240508 Runtime parsing of date literal takes into account the current date-format.
    **                  Avoid wrapping String literals, when possible.
    **         20240510 Added auto detection for date format, depending on the context and a quick heuristic
    **                  analysis of the text.
    ** 005 OM  20240524 Improved detection of date-related formats when literals are parsed.
    ** 006 TJD 20240123 Java 17 compatibility updates
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist.orm.types;

    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;
    import java.sql.*;
    import java.time.*;
    import java.util.function.*;

    /** This object handles conversion from/to SQL/FWD {@code datetimetz} type. */
    public class DatetimetzType
    implements DataHandler
    {
       /** Helper handler for computing getFieldSizeInIndex(). */
       private final Int64Type int64Type;

       /** Helper handler for computing getFieldSizeInIndex(). */
       private final DateType dateType;

       /**
        * The single constructor.
        *
        * @param   dateType
        *          Helper handler for computing {@link #getFieldSizeInIndex}.
        * @param   int64Type
        *          Helper handler for computing {@link #getFieldSizeInIndex}.
        */
       public DatetimetzType(DateType dateType, Int64Type int64Type)
       {
          this.dateType = dateType;
          this.int64Type = int64Type;
       }

       /**
        * Creates and returns a new {@link datetimetz} from a SQL counterpart.
        *
        * @param   sqlVal
        *          A value obtained from a SQL query, which needs to be converted to a {@code BaseDataType}.
        *
        * @return  A FWD {@code datetimetz} object representing the same value as {@code sqlVal}.
        */
       @Override
       public datetimetz convert(Object sqlVal)
       {
          return new datetimetz((OffsetDateTime) sqlVal);
       }

       /**
        * Use the data from a {@link datetimetz} field stored in {@code BaseRecord.data} and use
        * it to initialize a parameter of type OffsetDateTime in a SQL prepared query.
        * <p>
        * <strong>Note:</strong><br>
        * This implementation of {@code DataHandler} is the only one that fills in two parameters in a
        * prepared statement.
        *
        * @param   stmt
        *          The prepared statement whose parameter is to be set.
        * @param   index
        *          The index/position of the parameters.
        * @param   val
        *          The value of the parameter. Must be a {@code datetimetz}.
        * @param   forUpdate
        *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
        *          it is used as part of the where predicate.
        *
        * @return  2 (the number of position used by a datetimetz value).
        *
        * @throws  SQLException
        *          when an error is encountered while setting the value.
        */
       @Override
       public int setParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
       throws SQLException
       {
          return TypeManager.setOffsetDateTimeParameter(stmt, index, val, forUpdate);
       }

       /**
        * Reads data from one or more positions from the {@code ResultSet}, eventually converting it
        * to a proper type and storing the result directly to the {@code Record}'s data structure.
        *
        * @param   rs
        *          The {@code ResultSet} to read from.
        * @param   rsOffset
        *          The index/position in current row of {@code rs} to start reading.
        * @param   data
        *          The destination. I.e. the record's {@code data} field member.
        * @param   propIndex
        *          The destination offset. I.e. the property index.
        *
        * @return  always 2.
        *
        * @throws  SQLException
        *          when an error is encountered while setting the value.
        */
       @Override
       public int readProperty(ResultSet rs, int rsOffset, Object[] data, int propIndex)
       throws SQLException
       {
          Timestamp tt = rs.getTimestamp(rsOffset);
          if (tt == null)
          {
             data[propIndex] = null;
          }
          else
          {
             int tz = rs.getInt(rsOffset + 1); // DTZ_OFFSET
             data[propIndex] = OffsetDateTime.ofInstant(tt.toInstant(),
                                                        ZoneOffset.ofHoursMinutes(tz / 60, tz % 60));
          }
          return 2;
       }

       /**
        * Get the number of columns read from the result-set.
        *
        * @return   always 2.
        */
       @Override
       public int propertySize()
       {
          return 2;
       }

       /**
        * Instantiate a low-level initial value suitable for storage in a {@link BaseRecord}'s data
        * array, parsed from a string representation.
        *
        * @param   text
        *          String representation of the initial value.
        * @param   propMeta
        *          Property metadata.
        *
        * @return  If the initial value is static, an {@code OffsetDateTimeValue} representing the initial value;
        *          or
        *          if the initial value is dynamic (i.e., "now"), a {@code Supplier<Object>)} lambda expression,
        *          the evaluation of which must be deferred until the moment the value is needed; or
        *          {@code null} if the initial value is missing or is the unknown value.
        */
       @Override
       public Object initialValue(String text, PropertyMeta propMeta)
       {
          if (text == null || text.isEmpty() || "?".equals(text))
          {
             // avoid creation of some new short-lived objects, including a date object (it's rather
             // costly as it involves parsing and a couple of context accesses)
             return null;
          }

          // special cases where the input is not a dtz literal
          if ("now".equalsIgnoreCase(text))
          {
             // evaluation of the dynamic initializer "now" must be deferred until the moment it is needed
             return (Supplier<Object>) () -> datetimetz.now().offsetDateTimeValue();
          }

          datetimetz initDTZ = datetimetz.parseInitial(text);
          if (initDTZ.isUnknown())
          {
             return null;
          }

          return initDTZ.offsetDateTimeValue();
       }

       /**
        * Compute the size in bytes taken by a specified value for the data type handled by this
        * class. The returned value is used to compute the index size.
        *
        * @param   val
        *          The value to be computed the size.
        *
        * @return  the number of bytes the {@code val} object occupies.
        */
       @Override
       public int getFieldSizeInIndex(Object val)
       {
          if (val == null)
          {
             return 3;
          }

          datetimetz asBDT = convert(val);
          date datePart = date.plusDays(asBDT, 0); // fast hack to extract date from datetime
          int time = asBDT.getTime();
          int offset = asBDT.getOffset();
          return dateType.getFieldSizeInIndex(datePart.dateValue()) +
                 int64Type.getFieldSizeInIndex(time) +
                 int64Type.getFieldSizeInIndex(offset) - 2;
       }

       /**
        * Sets a value of a property in a {@code Record}. The specified value is converted from
        * {@code BaseDataType} FWD standard to plain Java, as the {@code data} array expects before
        * assigning the value to right offset.
        *
        * @param   data
        *          The {@code data} array of the record to be altered.
        * @param   val
        *          The {@code BaseDataType} value to be set. Assumed to be a {@code datetimetz}.
        * @param   offset
        *          The offset of the property, ie, the entry in {@code data} array. This includes both
        *          base property offset and the eventual extent index.
        * @param   meta
        *          Property metadata. Used to fine-tune stored data.
        */
       @Override
       public void setField(Object[] data, BaseDataType val, int offset, PropertyMeta meta)
       {
          if (val == null || val.isUnknown())
          {
             data[offset] = null;
             return;
          }

          datetimetz asBDT = (datetimetz) val;
          data[offset] = asBDT.offsetDateTimeValue();
       }

       /**
        * Obtain the value of the property of a {@code Record} located at a specified offset in {@code data}
        * array. It assumes the data at specified offset is compatible with this data handler.
        *
        * @param   r
        *          The {@code Record}.
        * @param   offset
        *          The offset.
        *
        * @return  The value requested wrapped as a {@code BaseDataType}.
        */
       @Override
       public BaseDataType getField(Record r, int offset)
       {
          return r._getDatetimetz(offset);
       }
    }