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