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)
   }
}