# Database.java

    /*
    ** Module   : Database.java
    ** Abstract : Information required to connect to a database
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20070911   @35296 Created initial version. Information required to connect to a database.
    ** 002 ECF 20071002   @35329 Exposed all methods as public.
    ** 003 ECF 20071009   @35598 Reworked to better support remote databases. This support is not yet complete.
    ** 004 ECF 20071119   @35906 Completed rework to support remote databases.
    ** 005 CA  20080425   @38111 Added dirty flag to track the dirty databases.
    ** 006 ECF 20080510   @38481 Minor changes. Intern database name string
    **                           and additional toString() debug output.
    ** 007 ECF 20131002          Added metadata support.
    ** 008 ECF 20141204          Added unique ID based on physical database name.
    ** 009 ECF 20150801          Added id to Type enum to support database statistics.
    ** 010 OM  20200113          Added pseudo-type PRIMARY_NON_DIRTY.
    ** 011 IAS 20200914          Re-work (de)serialization
    **     CA  20200930          Cache the hashcode value.
    **     IAS 20210315          Added getSchema() method.
    **     ECF 20210420          Removed getSchema method, which cannot assume that physical database name and
    **                           schema name are the same.
    ** 012 CA  20240809          Cache and intern the lowercase database name, to help in compare operations.
    ** 013 OM  20240822          Database objects are tenant aware.
    ** 014 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    */

    /*
    ** 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;

    import static com.goldencode.util.NativeTypeSerializer.*;
    import java.io.*;
    import java.net.*;

    /**
     * A collection of information required to connect to a database, including the database's
     * physical name, purpose, and the host address (including port) of the P2J server instance which
     * is the authoritative controller of this database. Note that this host name might not be the
     * same as that on which the database server itself actually resides, and the port, if any, will
     * very likely be different than that on which the database server is listening. For the current
     * (i.e., non-remote) server instance, host address will be {@code null}.
     * <p>
     * Instances of this class are used extensively in the persistence framework as tokens to access
     * additional information that is database specific. It is used heavily as a hash map key and as such,
     * it should not be modified to contain additional information not needed for that purpose.
     * <p>
     * The tenant database objects are aware of their default (parent) database.
     *
     * @author ECF
     */
    public final class Database
    implements Externalizable
    {
       /** Type/purpose of database */
       public static enum Type
       {
          PRIMARY(0),
          DIRTY(1),
          META(2),
          PRIMARY_NON_DIRTY(10);

          /** Unique identifier associated with enum value */
          private final int id;

          /**
           * Constructor.
           *
           * @param   id
           *          Unique identifier associated with enum value */
          private Type(int id)
          {
             this.id = id;
          }

          /**
           * Get the unique identifier associated with the enum value.
           *
           * @return  Identifier.
           */
          public int getId()
          {
             return id;
          }
       }

       /** Physical name of database */
       private String name;

       /** Lowercase database {@link #name}. */
       private String namelc;

       /** Unique ID of database, based on physical name */
       private String id;

       /** Database type/purpose */
       private Type type;

       /** Socket address of P2J server which is authoritative for database */
       private InetSocketAddress socketAddress = null;

       /** Is this database managed by the local P2J server instance? */
       private Boolean local = null;

       /** The cached hash code value. */
       private Integer hashCode = null;

       /**
        * Reference to 'parent' database, aka the default database for a tenant database, if the case. If this is
        * the default or in case of non tenant-enabled databases this is a reference to itself.
        * Particular case: if the information is unknown, the value is {@code null}.
        */
       private Database parent = null;

       /** {@code true} only if this represents the temporary database. */
       private boolean temp;

       /** Default constructor */
       public Database()
       {
          super();
       }

       /**
        * Constructor which accepts the physical database name and assumes a type of primary.
        *
        * @param   name
        *          Physical name of database.  May not be <code>null</code>.
        */
       public Database(String name)
       {
          this(name, Type.PRIMARY);
       }

       /**
        * Constructor which accepts the physical database name and a type.
        *
        * @param   name
        *          Physical name of database.  May not be <code>null</code>.
        * @param   type
        *          Type/purpose of the database.
        */
       public Database(String name, Type type)
       {
          this(name, type, null, -1);
       }

       /**
        * Convenience constructor which copies an existing instance, but applies a new type.
        *
        * @param   database
        *          Database to be copied.  May not be <code>null</code>.
        * @param   type
        *          Type/purpose of the copy database.
        */
       public Database(Database database, Type type)
       {
          this(database.getName(), type, database.isLocal());

          this.socketAddress = database.getSocketAddress();
       }

       /**
        * Constructor which accepts the physical database name, type, host name/address, and port
        * number.
        *
        * @param   name
        *          Physical name of database.  May not be <code>null</code>.
        * @param   type
        *          Type/purpose of the database.
        * @param   host
        *          Host name or address of P2J server which is authoritative for
        *          database.  <code>null</code> indicates host is not relevant.
        * @param   port
        *          Port of P2J server which is authoritative for database.
        *          -1 indicates port is unknown.  May not be less than -1 or
        *          greater than 65535.
        */
       public Database(String name, Type type, String host, int port)
       {
          if (name == null || name.trim().isEmpty())
          {
             throw new NullPointerException("Database name may not be null");
          }

          if (port < -1 || port > 65535)
          {
             throw new IllegalArgumentException("Invalid port number:  " + port);
          }

          if (host != null && port >= 0)
          {
             this.socketAddress = new InetSocketAddress(host, port);
          }

          name = name.toLowerCase();
          if (name.endsWith(".db"))
          {
             name = name.substring(0, name.length() - 3);
          }
          this.name = name.intern();

          this.namelc = name.toLowerCase().intern();
          this.id = name.replace(File.separatorChar, '.').intern();

          this.type = type;
          if (type == Type.PRIMARY_NON_DIRTY)
          {
             this.local = Boolean.TRUE;
          }

          temp = type == Type.PRIMARY && name == DatabaseManager.TEMP_TABLE_SCHEMA; // == is OK (interned)
       }

       /**
        * Package-private constructor which accepts the physical database name
        * and an indication as to whether this database is locally managed.
        * <p>
        * This constructor is intended for use by other framework classes which
        * must ensure that the determination of whether this class is locally
        * managed is not deferred until lazily initialized by the {@link
        * #isLocal()} method.
        *
        * @param   name
        *          Physical name of database.  May not be {@code null}.
        * @param   type
        *           database type
        * @param   local
        *          {@code true} to indicate database is locally managed, else {@code false}.
        */
       public Database(String name, Type type, boolean local)
       {
          this(name, type, null, -1);

          this.local = (local ? Boolean.TRUE : Boolean.FALSE);
       }

       /**
        * Returns the reference to default tenant-database, if this database object is a tenant database. If this
        * is the default database or a not a tenant-enabled database, {@code this} is returned.
        *
        * @return  the default (parent) database of this tenant-database or else {@code this}.
        *          This method never returns {@code null}.
        */
       public Database getDefault()
       {
          return parent != null ? parent : this;
       }

       /**
        * Factory method for creating a tenant database with a specific name derived from this (a default tenant
        * database). Objects returned by this method are aware of their 'parent' database.
        *
        * @param   tenantName
        *          The name of the new tenant database.
        *
        * @return  a new tenant database with same properties as its parent.
        */
       public Database createTenant(String tenantName)
       {
          if (isTenant())
          {
             throw new RuntimeException("A tenant database can be created only from a default database!");
          }

          Database ret = new Database(tenantName, type);
          ret.local = local;
          ret.socketAddress = socketAddress;
          ret.parent = this;
          return ret;
       }

       /**
        * Create a 'sibling' database with a specific type {@code t}. The returned database preserved information
        * about its default
        *
        * @param   t
        *          The type of the database to be returned.
        *
        * @return  a database with same name as the current but with the specific type. If this database is a
        *          tenant, the returned database will preserve that information.
        */
       public Database toType(Type t)
       {
          if (type == t)
          {
             return this;
          }

          if (parent == null)
          {
             // default-tenant database
             Database ret = new Database(name, t, local);
             ret.socketAddress = socketAddress;
             return ret;
          }

          // derived tenant databases will keep the reference to their tenant-default database
          Database defaultDb = new Database(getDefault().getName(), t, local);
          defaultDb.socketAddress = socketAddress;
          if (defaultDb.isMeta()) //ie  t == Type.META
          {
             return defaultDb; // the meta database is shared by all physical databases of same logical database
          }

          return defaultDb.createTenant(name);
       }

       /**
        * Calculate a hash code for this object.
        *
        * @return  Hash code.
        */
       @Override
       public int hashCode()
       {
          if (hashCode == null)
          {
             int result = 17;
             result = 37 * result + namelc.hashCode();
             if (socketAddress != null)
             {
                result = 37 * result + socketAddress.hashCode();
             }

             result = 37 * result + type.hashCode();

             hashCode = result;
          }

          return hashCode;
       }

       /**
        * Test this object for equality (equivalence) with the given object.
        *
        * @param   o
        *          Object to test.
        *
        * @return  <code>true</code> if this object is the same instance as the
        *          given object, or it is considered equivalent to the given
        *          object.  <code>Database</code> instances are equivalent if
        *          they contain the same database name and P2J server address.
        */
       @Override
       public boolean equals(Object o)
       {
          if (this == o)
          {
             // Same instance.
             return true;
          }

          if (!(o instanceof Database))
          {
             // Impossible to be equivalent.
             return false;
          }

          Database that = (Database) o;

          if (this.hashCode() != that.hashCode())
          {
             return false;
          }

          // This comparison assumes that physical database names have been
          // canonicalized prior to instantiation of these Database instances.
          if (this.namelc != that.namelc)
          {
             return false;
          }

          if (!this.type.equals(that.type))
          {
             return false;
          }

          // This comparison will fail if different hostnames actually would
          // resolve to the same address.
          InetSocketAddress addr1 = this.socketAddress;
          InetSocketAddress addr2 = that.socketAddress;
          boolean sameAddr = (addr1 == null || addr2 == null)
                             ? addr1 == addr2        // both null
                             : addr1.equals(addr2);  // equivalent

          return sameAddr;
       }

       /**
        * Produce a string representation of the internal state of this object.
        *
        * @return  String representation of this object.
        */
       @Override
       public String toString()
       {
          StringBuilder buf = new StringBuilder();
          if (local == null)
          {
             buf.append("?");
          }
          else if (local)
          {
             buf.append("local");
          }
          else
          {
             buf.append(socketAddress);
          }
          buf.append("/");
          buf.append(name);
          buf.append("/");
          buf.append(type.toString().toLowerCase());

          if (isTenant())
          {
             buf.append(" (").append(parent.name).append(")");
          }

          return buf.toString();
       }

       /**
        * Get the physical database name.
        *
        * @return  Physical database name.
        */
       public String getName()
       {
          return name;
       }

       /**
        * Get the unique id of this database.
        *
        * @return  Unique id.
        */
       public String getId()
       {
          return id;
       }

       /**
        * Get the host address (including port number) of the P2J server instance
        * which is authoritative for the database instance represented by this
        * object.
        *
        * @return  P2J server address.
        */
       public InetSocketAddress getSocketAddress()
       {
          return socketAddress;
       }

       /**
        * Indicate whether the current P2J server instance is authoritative for
        * the database instance represented by this object.
        *
        * @return  <code>true</code> if the current P2J server instance is
        *          authoritative;  <code>false</code> if this database is
        *          controlled by a remote P2J server instance.  Note that "remote"
        *          and "local" are logical concepts and have nothing to do with
        *          the physical location of the database or P2J server instance.
        */
       public boolean isLocal()
       {
          if (local == null)
          {
             local = (DatabaseManager.isAuthoritative(this) ? Boolean.TRUE : Boolean.FALSE);
          }

          return local;
       }

       /**
        * Get the type/purpose of this database.
        *
        * @return  Type/purpose of this database.
        */
       public Type getType()
       {
          return type;
       }

       /**
        * Get the primary status of this database.
        *
        * @return  True if this represents a primary database.
        */
       public boolean isPrimary()
       {
          return type == Type.PRIMARY;
       }

       /**
        * Get the dirty status of this database.
        *
        * @return  True if this represents a dirty database.
        */
       public boolean isDirty()
       {
          return type == Type.DIRTY;
       }

       /**
        * Get the dirty status of this database.
        *
        * @return  True if this represents a metadata database.
        */
       public boolean isMeta()
       {
          return type == Type.META;
       }

       /**
        * Test whether this object represents the temporary database (it's local, {@code Type.PRIMARY}, and its
        * name is {@link DatabaseManager#TEMP_TABLE_SCHEMA}).
        *
        * @return  {@code true} only if this is the temporary database.
        */
       public boolean isTemporary()
       {
          return temp;
       }

       /**
        * Test whether this object represents an instance of a tenant database (it has a reference to its parent,
        * default tenant-database).
        *
        * @return  {@code true} only if this is a tenant database. Otherwise, this is not, or the
        *          <strong>information is unknown<strong/> at this moment.
        */
       public boolean isTenant()
       {
          return parent != null;
       }

       /**
        * Replacement for the default object writing method.
        *
        * @param    out
        *           The output destination to which fields will be saved.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       @Override
       public void writeExternal(ObjectOutput out)
       throws IOException
       {
          writeString(out, name);
          writeString(out, id);
          writeEnum(out, type);
          out.writeObject(socketAddress);
          writeBoolean(out, local);
          out.writeObject(parent);
       }

       /**
        * Replacement for the default object reading method.
        *
        * @param    in
        *           Input source from which fields will be restored.
        *
        * @throws   IOException
        *           In case of I/O errors.
        * @throws   ClassNotFoundException
        *           If payload can't be instantiated.
        */
       @Override
       public void readExternal(ObjectInput in)
       throws IOException, ClassNotFoundException
       {
          name = readString(in);
          id = readString(in);
          type = readEnum(in, Type.values());
          socketAddress = (InetSocketAddress) in.readObject();
          local = readBoolean(in);
          parent = (Database) in.readObject();

          namelc = name.toLowerCase().intern();
          name = name.intern();
          id = id.intern();
          temp = type == Type.PRIMARY && name == DatabaseManager.TEMP_TABLE_SCHEMA; // == is OK (interned)
       }
    }