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