# DbTable.java

    /*
     * Copyright (C) 2000 - 2024 Silverpeas
     *
     * 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.
     *
     * As a special exception to the terms and conditions of version 3.0 of
     * the GPL, you may redistribute this Program in connection with Free/Libre
     * Open Source Software ("FLOSS") applications as described in Silverpeas's
     * FLOSS exception.  You should have received a copy of the text describing
     * the FLOSS exception, and it is also available here:
     * "https://www.silverpeas.org/legal/floss_exception.html"
     *
     * 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 should have received a copy of the GNU Affero General Public License
     * along with this program.  If not, see <https://www.gnu.org/licenses/>.
     */

    package org.silverpeas.components.mydb.model;

    import org.silverpeas.components.mydb.model.predicates.AbstractColumnValuePredicate;
    import org.silverpeas.components.mydb.model.predicates.ColumnValuePredicate;
    import org.silverpeas.components.mydb.service.MyDBRuntimeException;
    import org.silverpeas.core.admin.PaginationPage;
    import org.silverpeas.core.util.SilverpeasList;
    import org.silverpeas.kernel.util.StringUtil;

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.Optional;
    import java.util.function.Predicate;
    import java.util.stream.Collectors;

    import static java.util.stream.Collectors.toMap;

    /**
     * Table loaded from the database referred by a {@link MyDBConnectionInfo} instance.
     * @author mmoquillon
     */
    public class DbTable {

      private final String name;
      private final List<DbColumn> columns = new ArrayList<>();
      private JdbcRequester requester = null;

      /**
       * Loads the default table defined in the specified {@link MyDBConnectionInfo} instance.
       * If no default table is defined then nothing is returned. If the default table doesn't
       * exist then a {@link org.silverpeas.components.mydb.service.MyDBRuntimeException} is thrown.
       * @param dsInfo the {@link MyDBConnectionInfo} instance with information to access the
       * database and to get the name of the table load.
       * @return optionally a {@link DbTable} instance or nothing if no default table is set in the
       * specified {@link MyDBConnectionInfo} instance.
       */
      public static Optional<DbTable> defaultTable(final MyDBConnectionInfo dsInfo) {
        DbTable table = null;
        if (dsInfo.isDefaultTableNameDefined()) {
          table = new DbTable(dsInfo.getDefaultTableName(), dsInfo);
        }
        return Optional.ofNullable(table);
      }

      /**
       * Gets the table with the specified name from the database referenced by the given connection
       * information. If no such a table doesn't exist, then nothing is returned.
       * @param tableName the name of a table in the database.
       * @param dsInfo the {@link MyDBConnectionInfo} instance with information to access the
       * database
       * @return optionally a {@link DbTable} instance or nothing if no such a table exist in the
       * database.
       */
      public static Optional<DbTable> table(final String tableName, final MyDBConnectionInfo dsInfo) {
        try {
          return Optional.of(new DbTable(tableName, dsInfo));
        } catch (MyDBRuntimeException e) {
          return Optional.empty();
        }
      }

      /**
       * Gets a listing of the names of all the business tables in the database. The database is
       * identified by the specified {@link MyDBConnectionInfo} instance.
       * (The system and technical tables aren't get.)
       * @param dsInfo the {@link MyDBConnectionInfo} instance with information to access the
       * database.
       * @return a list with the name of all the business tables in the database.
       */
      public static List<String> list(final MyDBConnectionInfo dsInfo) {
        Objects.requireNonNull(dsInfo);
        final JdbcRequester requester = new JdbcRequester(dsInfo);
        return requester.getTableNames();
      }

      /**
       * Constructs a new instance for a table with the specified name and that is defined in the
       * database referenced by the specified {@link MyDBConnectionInfo} object.
       * If the table doesn't exist then a
       * {@link org.silverpeas.components.mydb.service.MyDBRuntimeException} is thrown.
       * @param name the name of the table
       * @param ds the information about the database in which is defined this table.
       */
      private DbTable(final String name, final MyDBConnectionInfo ds) {
        StringUtil.requireDefined(name);
        Objects.requireNonNull(ds);
        this.name = name;
        setJdbcRequester(new JdbcRequester(ds));
      }

      /**
       * Sets a requester to access the database and performs JDBC operations for this table. Once
       * a requester set, the columns of the table are automatically fetched.
       * @param requester a {@link JdbcRequester} instance.
       */
      private void setJdbcRequester(final JdbcRequester requester) {
        this.requester = requester;
        this.requester.perform((r, c) -> {
          this.columns.clear();
          r.loadColumns(c, this.name, d -> this.columns.add(new DbColumn(d)));
          return null;
        });
      }

      /**
       * Gets this table's name.
       * @return the name of this table.
       */
      public String getName() {
        return name;
      }

      /**
       * Gets all the columns that made up this table. If no columns were specified for this table,
       * then an empty list is returned.
       * @return a list of columns.
       */
      public List<DbColumn> getColumns() {
        return this.columns;
      }

      /**
       * Gets the column with the specified name.
       * @param name the name of the column.
       * @return optionally the asked column. If no such column exists then an empty optional is
       * returned.
       */
      public Optional<DbColumn> getColumn(final String name) {
        return this.columns.stream().filter(c -> c.getName().equals(name)).findFirst();
      }

      /**
       * Gets the contents of this table as a list of rows, each of them being a tuple valuing
       * all the columns of this table. If the table is empty, then an empty list is returned.
       * @param filter a predicate to use for filtering the table content.
       * @param orderBy a order by directive already built (without the clause key words).
       * @param pagination a pagination in order to avoid bad performances.
       * @return a list of table rows. If a filter is set, it is then applied when requesting the
       * content of this table. The number of table rows is limited by the
       * {@link MyDBConnectionInfo#getDataMaxNumber()} property.
       */
      public SilverpeasList<TableRow> getRows(final ColumnValuePredicate filter, final String orderBy,
          final PaginationPage pagination) {
        if (!(filter instanceof AbstractColumnValuePredicate)) {
          throw new IllegalArgumentException(
              "DbTable doesn't support predicate other than AbstractColumnValuePredicate objects");
        }
        return requester.perform((r, c) -> {
          final JdbcRequester.DataConverters<TableFieldValue, TableRow> converters =
              new JdbcRequester.DataConverters<>(TableFieldValue::new, TableRow::new);
          return r.request(c, this.name, (AbstractColumnValuePredicate) filter, orderBy, converters,
              pagination);
        });
      }

      /**
       * Deletes the specified row.
       * @param row the row to delete in this database table.
       */
      public long delete(final TableRow row) {
        return requester.perform((r, c) -> {
          final Map<String, Object> criteria = getCriteriaFrom(row);
          return r.delete(c, getName(), criteria);
        });
      }

      /**
       * Updates the specified row with the specified other row.
       * @param actualRow the row currently in this table.
       * @param updatedRow the row that will replace the actual one in this table.
       */
      public long update(final TableRow actualRow, final TableRow updatedRow) {
        return requester.perform((r, c) -> {
          final Map<String, Object> criteria = getCriteriaFrom(actualRow);
          final Map<String, Object> values = tableRowToMap(updatedRow);
          return r.update(c, getName(), values, criteria);
        });
      }

      /**
       * Adds the specified row into this table. The row will be inserted into the database table.
       * @param row the row to add into the database table.
       */
      public void add(final TableRow row) {
        requester.perform((r, c) -> {
          final Map<String, Object> values = tableRowToMap(row);
          r.insert(c, getName(), values);
          return null;
        });
      }

      private Map<String, Object> getCriteriaFrom(final TableRow row) {
        final List<DbColumn> pkColumns =
            columns.stream().filter(DbColumn::isPrimaryKey).collect(Collectors.toList());
        final Map<String, Object> criteria;
        if (!pkColumns.isEmpty()) {
          criteria = pkColumns.stream()
              .collect(toMap(DbColumn::getName,
                  pk -> row.getFieldValue(pk.getName()).toSQLObject()));
        } else {
          criteria = tableRowToMap(row);
        }
        return criteria;
      }

      private Map<String, Object> tableRowToMap(final TableRow tableRow) {
        return tableRow.getFields()
            .entrySet()
            .stream()
            .filter(Predicate.not(e -> e.getKey().equalsIgnoreCase("SP_MAX_ROW_COUNT")))
            .collect(HashMap::new, (h, e) -> h.put(e.getKey(), e.getValue().toSQLObject()), HashMap::putAll);
      }
    }