DmoMetadataManager.java
/*
** Module : DmoMetadataManager.java
** Abstract : Handles the management of DMO structure (metadata), including fields and indexes.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20191015 First revision. Collects DMO information. Expose DmoMeta information to locations that
** need DMO structure.
** 002 OM 20200919 Improved access to dmo metadata.
** ECF 20200920 Map permanent DMO with TableMapper in registerDmo, after DmoMeta object is fully
** initialized.
** OM 20200924 Index components carry multiple information to avoid map lookups for them.
** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** OM 20201012 Moved delegated method to DmoMeta. Deprecated / dropped unused methods.
** IAS 20201224 Added getDmoInfo(String dmoName) method
** CA 20210508 Added isSchemaMatch, which checks if two DMO interfaces have the same schema signature.
** CA 20210626 Fixed getAnnotatedInterface - it must look in the implemented interfaces, too.
** ECF 20211202 Removed unused, deprecated getExistingFields method.
** OM 20220713 Added mapping from POJO types to their pair DmoMeta structure.
** TJD 20221004 In case assembleImplementation fails DMO is now removed from registry.
** ECF 20221207 Method name change.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** ECF 20221207 Method name change.
** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 004 OM 20240314 Added getDmoInterfaces() method.
** 005 OM 20240402 Multiple classes/interfaces of dynamic DMOs can be dropped at once.
** 006 TJD 20240123 Java 17 compatibility updates
** 007 EAB 20240611 Added getDmoIface and changed the parameter types of isSchemaMatch.
** 008 AD 20240313 Changed access modifier of method getAnnotatedInterface(Class) to public.
** AD 20240403 Added callback method when assembling dmo implementation.
** 009 DDF 20250210 Added isSchemaMatch() overload.
** 010 OM 20250310 Added support for R/O (meta) tables.
*/
/*
** 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;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.logging.*;
import java.util.stream.*;
/**
* This manager implements two services:
* <ol>
* <li>Collects and provides meta information on DMO structures. All DMOs encountered during
* execution of a 4GL program is required to register with this manager using
* {@link #registerDmo(Class)}. This method will analyze the class and build an internal
* data structure that will provide metadata information at request, including:
* <ul>
* <li>the attributes of the legacy table;</li>
* <li>the list of properties and attributes of legacy fields they are mapping;</li>
* <li>the list and attributes of legacy indexes;</li>
* <li>the list and attributes of relations between tables, as they were detected at
* conversion time.</li>
* </ul>
* </li>
* <li>Based on the information above, the manager is able to construct and will provide at
* request DDL statements for temp-tables.<br>
* NOTE: A single dialect is expected (usually H2). The DDLs for creation and drop of the
* table along with its indexes are created simultaneously and are cached locally.
* </li>
* </ol>
*/
public class DmoMetadataManager
{
/** Relative directory path for application package root */
private static final String PKGROOT = "legacy-system/pkgroot";
/**
* The DMO registry. The keys are full-name of DMO interfaces and implementation classes. Since these
* include the schema, there will not be any collisions.
*/
private static final Map<String, DmoMeta> registry = new ConcurrentHashMap<>();
/**
* The short-name registry. The keys are the simple name of the DMO implementation class. Since these
* do not reflect their schema, this is needed as a second level map.
*/
private static final Map<String, Map<String, DmoMeta>> simpleRegistry = new ConcurrentHashMap<>();
/** Holds the mapping from POJO types to their pair DmoMeta structure. */
private static final IdentityHashMap<Class<?>, DmoMeta> pojoMap = new IdentityHashMap<>();
/**
* The {@code DmoMeta} ordered by their unique ids. Allocate a minimum amount of space sufficient for
* basic projects. If there are more DMOs the array will be resized as needed (incrementally, by 150%).
*/
private static DmoMeta[] byId = new DmoMeta[20 /* mandatory meta*/ + 100 /* configurable? project-specific */];
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(DmoMetadataManager.class.getName());
/** Cached value of the base package name for all DMOs. Uses lazy initialization. */
private static String pkgroot = null;
/** Set of DMO entity names which correspond to read-only tables */
private static final Set<String> readOnly = Collections.synchronizedSet(new HashSet<>());
/**
* Get the base package name for all DMOs.
*
* @return Base package name.
*/
public static String getDmoBasePackage()
{
if (pkgroot == null)
{
pkgroot = Utils.getDirectoryNodeString(DirectoryService.getInstance(), PKGROOT, null, true) + ".dmo";
}
return pkgroot;
}
/**
* Get the DMO interface associated with the given buffer class.
*
* @param bufferCls
* The buffer class
*
* @return The DMO interface
*/
public static Class<? extends DataModelObject> getDmoIface(Class<?> bufferCls)
{
Class<?>[] superIfaces = bufferCls.getInterfaces();
int len = superIfaces.length;
for (int i = 0; i < len; i++)
{
Class<?> next = superIfaces[i];
if (DataModelObject.class.isAssignableFrom(next))
{
return (Class<? extends DataModelObject>) next;
}
}
return null;
}
/**
* Check if the two DMO interfaces have the same schema. This check is compatible and used for lookup of
* the master buffer/temp-table, when resolving a shared buffer/temp-table.
*
* @param ifc1
* The first DMO interface.
* @param ifc2
* The second DMO interface.
*
* @return <code>true</code> if the {@link DmoSignature#getSchemaSignature() signature} is the same.
*/
public static boolean isSchemaMatch(Class<? extends DataModelObject> ifc1,
Class<? extends DataModelObject> ifc2)
{
return isSchemaMatch(ifc1, ifc2, false);
}
/**
* Check if the two DMO interfaces have the same schema. This check is compatible and used for lookup of
* the master buffer/temp-table, when resolving a shared buffer/temp-table.
*
* @param ifc1
* The first DMO interface.
* @param ifc2
* The second DMO interface.
* @param simpleMatch
* {@code false} if the schema match check should be done on the schema signature or
* {@code true} for the simple schema signature (which does not contain any options).
*
* @return <code>true</code> if the {@link DmoSignature#getSchemaSignature() signature} is the same.
*/
public static boolean isSchemaMatch(Class<? extends DataModelObject> ifc1,
Class<? extends DataModelObject> ifc2,
boolean simpleMatch)
{
DmoMeta meta1 = registerDmo(ifc1);
DmoMeta meta2 = registerDmo(ifc2);
if (meta1 == meta2)
{
return true;
}
DmoSignature metaSig1 = meta1.getSignature();
DmoSignature metaSig2 = meta2.getSignature();
if (!simpleMatch)
{
return metaSig1.getSchemaSignature().equals(metaSig2.getSchemaSignature());
}
else
{
return metaSig1.getSimpleSchemaSignature().equals(metaSig2.getSimpleSchemaSignature());
}
}
/**
* Gather information for a DMO. The {@code aClass} and it super interfaces are analyzed
* and, if DMO annotation are found, they are stored for later use.
*
* @param dmoIface
* The interface to be analyzed.
*
* @return The {code DmoMeta} structure associated with the requested interface. To obtain the super
* interface use the {@link DmoMeta#getAnnotatedInterface()}.
*
* @throws NullPointerException
* if {@code dmoIface} is null.
* @throws IllegalArgumentException
* if {@code dmoIface} is null or neither {@code dmoIface} and none of its super
* interfaces are annotated as DMO or {@code @Table} annotation is not present
*/
public static synchronized DmoMeta registerDmo(Class<? extends DataModelObject> dmoIface)
{
return registerDmo(dmoIface, null);
}
/**
* Gather information for a DMO. The {@code aClass} and it super interfaces are analyzed
* and, if DMO annotation are found, they are stored for later use.
*
* @param dmoIface
* The interface to be analyzed.
* @param assembleCallback
* Callback method to use after assembling implementation.
*
* @return The {code DmoMeta} structure associated with the requested interface. To obtain the super
* interface use the {@link DmoMeta#getAnnotatedInterface()}.
*
* @throws NullPointerException
* if {@code dmoIface} is null.
* @throws IllegalArgumentException
* if {@code dmoIface} is null or neither {@code dmoIface} and none of its super
* interfaces are annotated as DMO or {@code @Table} annotation is not present
*/
public static synchronized DmoMeta registerDmo(Class<? extends DataModelObject> dmoIface, Consumer<DmoClass> assembleCallback)
{
if (dmoIface == null)
{
throw new NullPointerException("Parameter of registerDmo() cannot be null.");
}
DmoMeta test = registry.get(dmoIface.getName());
if (test != null)
{
return test;
}
Class<? extends DataModelObject> dmoImplInterface = dmoIface;
// get the interface that contains the actual DMO annotations
dmoIface = getAnnotatedInterface(dmoIface);
if (dmoIface == null)
{
// no DMO interface detected.
throw new IllegalArgumentException(
"The " + dmoImplInterface + " is not implementing DataModelObject.");
}
Table tableAnn = dmoIface.getAnnotation(Table.class);
if (tableAnn == null)
{
throw new IllegalArgumentException(
"@Table annotation not detected in DataModelObject (" + dmoImplInterface + ").");
}
DmoMeta dmo = new DmoMeta(dmoImplInterface, dmoIface, tableAnn);
int id = dmo.getId();
if (id >= byId.length)
{
// ensure medium/long term capacity
DmoMeta[] newById = new DmoMeta[byId.length * 3 / 2];
System.arraycopy(byId, 0, newById, 0, byId.length);
byId = newById;
}
byId[id] = dmo;
registry.put(dmoImplInterface.getName(), dmo);
if (dmoIface != dmoImplInterface)
{
registry.put(dmoIface.getName(), dmo);
}
Class<? extends Record> implClass;
try
{
implClass = DmoClass.assembleImplementation(dmo, assembleCallback);
}
catch (Exception e)
{
registry.remove(dmoIface.getName());
registry.remove(dmoImplInterface.getName());
throw new IllegalArgumentException(
"Failed to create DMO implementation for (" + dmoImplInterface + ")", e);
}
String schema = getSchema(dmoIface);
Map<String, DmoMeta> schemaMap = simpleRegistry.computeIfAbsent(schema, s -> new HashMap<>());
schemaMap.put(implClass.getSimpleName(), dmo);
registry.put(implClass.getName(), dmo);
// TODO: this condition may change in the future to allow non-meta tables to be declared as R/O
if (dmo.metaTable != null && dmo.metaTable.isReadOnly())
{
readOnly.add(implClass.getName());
}
try
{
dmo.setImplClass(implClass);
}
catch (IllegalAccessException | NoSuchFieldException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Failed to process metadata for " + dmoImplInterface, e);
}
}
if (!dmo.isTempTable())
{
TableMapper.mapPermanentDMO(dmo);
}
if (tableAnn.pojoClass() != Object.class)
{
pojoMap.put(tableAnn.pojoClass(), dmo);
}
return dmo;
}
/**
* Get the DMO interface associated with the given DMO implementation class.
*
* @param dmoClass
* DMO implementation class.
*
* @return DMO interface.
*
* @throws IllegalArgumentException
* if any of its interfaces is recognized as a valid DMO interface.
*
* @deprecated
*/
@Deprecated
public static Class<? extends DataModelObject> getDMOInterface(Class<? extends Record> dmoClass)
{
return getDmoInfo(dmoClass).getAnnotatedInterface();
}
/**
* This removes given a set of dynamic DMO from registry.
*
* @param classNames
* An array of class names to be deregistered.
*/
public static synchronized void deregisterDmos(String... classNames)
{
for (int i = 0; i < classNames.length; i++)
{
registry.remove(classNames[i]);
}
}
/**
* Reverse lookup of a specific {@code DmoMeta} by its POJO class type.
*
* @param pojoClass
* The POJO type to be used as key. If teh specific POJO
*
* @return The {@code DmoMeta} of the DMO which declared the {@code pojoClass} as its POJO type.
*/
public static DmoMeta byPojoClass(Class<?> pojoClass)
{
return pojoMap.get(pojoClass);
}
/**
* Searches for a {@code DmoMeta} for a specific alias in the list of all known aliases.
*
* @param dmoName
* The name of the alias to be located. May be a full-path DMO, DMO implementation, or simple
* name of DMO, DMO implementation in which case the {@code schema} parameter is used for
* disambiguate the entity.
* @param schema
* The schema of the database. This is used to disambiguate short-names entities.
*
* @return The {@code DmoMeta} structure corresponding to {@code dmoName} alias, or
* {@code null} if information on this alias is not available.
*/
public static DmoMeta getDmoInfo(String dmoName, String schema)
{
if (dmoName.indexOf('.') > 0)
{
// full-names DMO interface or implementation class
return registry.get(dmoName);
}
else
{
// simple class name. May be ambiguous, so the database schema is needed
Map<String, DmoMeta> schemaMap = simpleRegistry.get(schema);
DmoMeta dmoMeta = schemaMap.get(dmoName);
if (dmoMeta == null && !dmoName.endsWith(DBUtils.IMPL_SUFFIX))
{
dmoMeta = schemaMap.get(dmoName + DBUtils.IMPL_SUFFIX);
}
return dmoMeta;
}
}
/**
* Searches for a {@code DmoMeta} for a specific alias in the list of all known aliases. The
* method does the best effort to find the right information, regardless the {@code dmo}
* parameter is a DMO interface or an DMO implementation class.
*
* @param dmo
* The DMO class to be located.
*
* @return The {@code DmoMeta} structure corresponding to DMO class
*
* @throws IllegalArgumentException
* if the class passed in is not implementing DMO interface or its interface was not
* registered beforehand.
*/
public static DmoMeta getDmoInfo(Class<?> dmo)
{
return getDmoInfo(dmo.getName());
}
/**
* Searches for a {@code DmoMeta} for a specific alias in the list of all known aliases. The
* method does the best effort to find the right information, regardless the {@code dmo}
* parameter is a DMO interface or an DMO implementation class.
*
* @param dmoName
* The name of the DMO class to be located.
*
* @return The {@code DmoMeta} structure corresponding to DMO class
*
* @throws IllegalArgumentException
* if the class passed in is not implementing DMO interface or its interface was not
* registered beforehand.
*/
public static DmoMeta getDmoInfo(String dmoName)
{
DmoMeta dmoMeta = registry.get(dmoName);
if (dmoMeta == null)
{
throw new IllegalArgumentException("The " + dmoName + " was not registered yet.");
}
return dmoMeta;
}
/**
* Obtain the {@code DmoMeta} of a DMO/table based on its unique id.
*
* @param dmoId
* The unique id of the DMO/table.
*
* @return The {@code DmoMeta} for respective DMO/table, or {@code null} if {@code dmoId} is invalid.
*/
public static DmoMeta getDmoInfo(int dmoId)
{
if (dmoId <= 0 || dmoId >= byId.length)
{
return null;
}
return byId[dmoId];
}
/**
* Filters and returns a set of interface names from a specified package.
*
* @param pathPrefix
* All registered DMOs will to be 'scanned' and the interfaces whose full name start with this
* prefix will be returned. The search will exclude the class implementations by looking at their
* specific suffix.
*
* @return the set of names of the DMO interfaces from the specified package.
*/
public static synchronized Set<String> getDmoInterfaces(String pathPrefix)
{
return registry.keySet()
.stream()
.filter(s -> s.startsWith(pathPrefix) && !s.endsWith(DBUtils.IMPL_SUFFIX))
.collect(Collectors.toSet());
}
/**
* For the given schema and DMO interface, get the map of indexed character column names to
* {@code P2JIndexComponent} instances which describe their use in individual indexes.
* <p>
* They are read directly from the specified table, using a
* {@link IndexMetadataCollector#queryIndexData(Session, String)} call.
*
* @param database
* Database information object.
* @param table
* backing table name.
*
* @return Unmodifiable map.
*
*
* @throws PersistenceException
* if there is an error querying index information from the JDBC driver.
*/
@Deprecated
public static Map<String, P2JIndexComponent> getIndexedCharacterColumns(Database database, String table)
throws PersistenceException
{
List<P2JIndex> indexes = IndexMetadataCollector.queryIndexData(new Session(database), table);
Iterator<P2JIndex> idxIter = indexes.iterator();
Map<String, P2JIndexComponent> indexedCharCols = new HashMap<>();
while (idxIter.hasNext())
{
P2JIndex index = idxIter.next();
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
if (comp.isCharType())
{
indexedCharCols.put(comp.getPropertyName(), comp);
}
}
}
return Collections.unmodifiableMap(indexedCharCols);
}
/**
* Determine whether the given DMO property represents the leading component of any index
* on the table represented by the given buffer.
*
* @param dmo
* The DMO of a table associated with the property.
* @param property
* DMO property to be tested.
*
* @return {@code true} if {@code property} is a leading index component; else {@code false}.
*
* @deprecated use {@link DmoMeta#isLeadingIndexComponent(String)}.
*/
@Deprecated
public static boolean isLeadingIndexComponent(Class<? extends DataModelObject> dmo, String property)
{
return getDmoInfo(dmo).isLeadingIndexComponent(property);
}
/**
* Get an iterator on all foreign relation descriptors for the DMO specified by the given
* schema and interface. The resulting iterator returns {@link RelationInfo} instances, each
* of which describes a foreign key relation <em>from</em> the specified DMO to another.
*
* @param iface
* DMO interface.
*
* @return Iterator on foreign relation descriptors.
*
* @throws IllegalArgumentException
* if either the {@code schema} or {@code iface} parameters are not recognized.
* @throws IllegalStateException
* if P2J_HOME is not defined; if there is any error parsing the DMO index document.
*
* @deprecated use {@link DmoMeta#relations()}.
*/
@Deprecated
public static Iterator<RelationInfo> relations(Class<? extends DataModelObject> iface)
{
return getDmoInfo(iface).relations();
}
/**
* Get an iterator on all inverse foreign relation descriptors for the DMO specified by the
* given schema and interface. The resulting iterator returns {@link RelationInfo} instances,
* each of which describes a foreign key relation <em>to</em> the specified DMO from another.
*
* @param iface
* DMO interface.
*
* @return Iterator on inverse foreign relation descriptors.
*
* @throws IllegalArgumentException
* if either the {@code schema} or {@code iface} parameters are not recognized.
* @throws IllegalStateException
* if P2J_HOME is not defined; if there is any error parsing the DMO index document.
*/
@Deprecated
public static Iterator<RelationInfo> inverseRelations(Class<? extends DataModelObject> iface)
{
return getDmoInfo(iface).inverseRelations();
}
/**
* Retrieve an object which describes the foreign relation between two tables.
*
* @param localIface
* DMO interface for the local table.
* @param foreignIface
* DMO interface for the foreign table.
*
* @return Object which describes the relation, or {@code null} if no such relation exists.
* Note that a {@code null} return does not necessarily indicate that the inverse
* relation does not exist. This can be determined by invoking this method with the
* {@code localIface} and {@code foreignIface} parameters inverted.
*/
@Deprecated
public static RelationInfo getRelation(Class<? extends DataModelObject> localIface,
Class<? extends DataModelObject> foreignIface)
{
return getDmoInfo(localIface).getRelation(foreignIface);
}
/**
* Add an object which describes a foreign relation from one table to another at server runtime
* (as opposed to relations detected at conversion stage).
*
* @param info
* Relation descriptor.
*/
public static void putDynamicRelation(RelationInfo info)
{
getDmoInfo(info.getLocalInterface()).putRelation(info.getForeignInterface(), info);
getDmoInfo(info.getForeignInterface()).putInverseRelation(info.getLocalInterface(), info);
}
/**
* Searches and returns the super-interface {@code DataModelObject} that has the {@code Table} annotation.
*
* @param dmoIface
* The interface to be analysed.
*
* @return the super-interface {@code DataModelObject} that has the {@code Table} annotation
* or {@code null} if no such interface can be found.
*/
public static Class<? extends DataModelObject> getAnnotatedInterface(
Class<? extends DataModelObject> dmoIface)
{
if (dmoIface.getAnnotation(Table.class) != null)
{
return dmoIface;
}
Class<?>[] interfaces = dmoIface.getInterfaces();
for (Class<?> iface : interfaces)
{
if (DataModelObject.class.isAssignableFrom(iface))
{
if (iface.getAnnotation(Table.class) != null)
{
return (Class<? extends DataModelObject>) iface;
}
Class<? extends DataModelObject> res = getAnnotatedInterface((Class<? extends DataModelObject>) iface);
if (res != null)
{
return res;
}
}
}
// all superinterfaces extend [DataModelObject] but the [Table] annotation is missing
return null;
}
/**
* Checks if the DMO class corresponds to the read-only table. Records in a read-only table cannot be
* changed, created, or deleted from 4GL code, only the administrative tools are allowed to process them.
*
* @param entity
* DMO entity name.
*
* @return {@code true} if the DMO entity corresponds to a read-only table.
*/
public static boolean isReadOnly(String entity)
{
return !readOnly.isEmpty() && readOnly.contains(entity);
}
/**
* Extracts the schema from a DMO class name. It is computed as a very last package path part from the
* class/interface full name.
*
* @param aClass
* the DMO class or interface.
*
* @return the schema name.
*/
private static String getSchema(Class<?> aClass)
{
String[] tokens = aClass.getName().split("\\.");
if (tokens.length < 2)
{
return null; // invalid schema
}
return tokens[tokens.length - 2];
}
}