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];
   }
}