UastHintsWorker.java

/*
** Module   : UastHintsWorker.java
** Abstract : provides a pattern engine service to lookup generic UAST hints
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20050314   @20326 Created initial version that provides
**                           support for reading string, long, double
**                           and boolean values using a file-unique key.
** 002 GES 20050316   @20398 Converted this worker to use the visitAst()
**                           interface in PatternWorker instead of using
**                           a rule driven approach.  
** 003 ECF 20050526   @21290 Changes to support new expression engine
**                           implementation. Changed library registration
**                           mechanism based on superclass' modifications
**                           to support single library per pattern worker
**                           limit.
** 004 ECF 20050713   @21700 Added getTableHints user function. Retrieves
**                           hints, if any, for a specified table.
** 005 GES 20051129   @23548 Fixes to remove null pointer exceptions.
** 006 ECF 20061126   @31401 Added getExternalTableHints() method to
**                           HintsReader. This method is used to read a
**                           set of hints for an explicit, external AST.
**                           Specifically, it allows temp/work-tables to
**                           read table hints for permanent tables.
** 007 GES 20070718   @34595 Fix to remove null pointer exception.
** 008 GES 20090429   @42067 Match package and class name changes.
** 009 GES 20090514   @42234 Removed dead code.
** 010 ECF 20091006   @44186 Allow a .dict AST to load UAST hints associated
**                           with the 4GL program file. This allows us to
**                           maintain a single hints file per program, but to
** 011 LMR 20110106          Fixes to reflect renaming of load() method in Configuration class to
**                           getSchemaConfig() and  removal of its 'key' parameter.
** 012 VMN 20140423          Added enhance schema name conversion support for hint "escape" attribute.
** 013 ECF 20160516          Do not try to load hints in runtime mode.
** 014 IAS 20160619          Added support for the "read-only" hint 
** 015 OM  20200108          Added support for "dirty-read" hint/attribute.
** 016 IAS 20210219          Use UastHints factory
**     CA  20211214          The AST manager plugin must be context-local, for runtime conversion, as the
**                           InMemoryRegistryPlugin is not thread-safe.
**     OM  20220330          Moved database conversion artifacts to ${cvtpath} folder.
**     OM  20220407          Looking for hints in the original folder, not in the volatile $cvtpath folder.
**     OM  20220415          Improved previous update. Support for reading schema hints from sbName.df.hints.
** 017 VVT 20230318          Unit test configuration support. See #6237.
** 018 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 019 SVL 20230516          Support for dirty-intra-read annotation.
*/

/*
** 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.uast;

import java.io.*;
import java.util.*;
import java.util.logging.*;

import com.goldencode.ast.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.schema.*;

/**
 * Provides a pattern engine service to read string, long, double and boolean values using a file-unique key.
 */
public class UastHintsWorker
extends AbstractPatternWorker
{
   /** Logger. */
   private static final ConversionStatus LOG = ConversionStatus.get(UastHintsWorker.class);
   
   /** Stores the current AST's hints object. */
   private UastHints hints = null;
   
   /**
    * Default constructor which calls the super-class constructor, registers its libraries and initializes its
    * instance members.
    * 
    * @throws  ConfigurationException
    *          If a configuration access error is encountered.
    * @throws  AstException
    *          If an error occurs while doing AST operations.
    */
   public UastHintsWorker()
   throws ConfigurationException,
          AstException
   {
      super();
      setLibrary(new HintsReader());
   }
      
   /**
    * Resets the current hints instance to <code>null</code> each time a new
    * AST is visited.  This enables the lazy loading of hints files only when
    * needed.
    *
    * @param   ast
    *          The root node of the source AST about to be processed by the
    *          pattern engine.
    */
   @Override
   public void visitAst(Aast ast)
   {
      hints = null;
   }
   
   /**
    * Provides a service for reading generic named hints associated with the
    * current AST.  This class is a <code>PatternEngine</code> worker.
    */
   public class HintsReader
   {
      /**
       * Retrieve the table hints, if any, for the specified table name.
       *
       * @param   table
       *          Name of the table for which hint information should be
       *          retrieved.
       *
       * @return  Table hints instance for the specified table, or
       *          <code>null</code> if none exists.
       */
      public TableHints getTableHints(String table)
      {
         if (!loadHints())
         {
            return null;
         }
         
         Map<String, TableHints> map = hints.getTableHints();
         if (map == null)
         {
            return null;
         }
         
         return map.get(table);
      }
      
      /**
       * Retrieve the table hints, if any, for the specified table name from a hints file not directly
       * associated with the currently loaded AST. Instead, the hints file is assumed to be a database-related
       * hints file identified by the specified, fully qualified table name.
       * <p>
       * Notes:  (1) unlike the other service methods of this worker, the hints loaded by this method are not
       *             cached, but are read each time this method is invoked.
       *         (2) this method only works for tables from permanent database. If the hints of a table from
       *             {@code @temp_db} database is requested, {@code null} is returned.
       *
       * @param   table
       *          Name of the table for which hint information should be retrieved.  This name is a schema
       *          name which must be qualified by the target database name for which to load hints.
       *
       * @return  Table hints instance for the specified table, or {@code null} if none exists.
       */
      public TableHints getExternalTableHints(String table)
      {
         // Load the external hints file, if any.
         EntityName ename = new EntityName(EntityName.TABLE, table);
         UastHints extHints = null;
         
         try
         {
            String database = ename.getDatabase();
            SchemaConfig config = Configuration.getSchemaConfig();
            File importFile = config.getImportFile(database);
            if (importFile == null)
            {
               return null;
            }
            extHints = getUastHints(importFile.getAbsolutePath());
         }
         catch (Exception exc)
         {
            LOG.log(Level.SEVERE, "", exc);
            return null;
         }
         
         Map<String, TableHints> map = extHints.getTableHints();
         if (map == null)
         {
            return null;
         }
         
         return map.get(ename.getTable());
      }
      
      /**
       * Get a UAST hint using a uniquely named key, accessing a string or
       * <code>null</code> if no such hint exists.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   The named string or <code>null</code>.
       */
      public String getUastString(String key)
      {
         if (!loadHints())
         {
            return null;
         }
         
         String result = null;
         
         String cls = hints.getUastType(key);
         
         if (cls != null && cls.equals("java.lang.String"))
         {
            result = hints.getUastString(key);
         }
         
         return result;
      }
      
      /**
       * Get a UAST hint using a uniquely named key, accessing a string array
       * or <code>null</code> if no such hint exists.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       * @param    index
       *           The 0-based index of the entry to access.
       *
       * @return   The indexed element in the named string array or 
       *           <code>null</code> if the array or element doesn't exist.
       */
      public String getUastStringArray(String key, int index)
      {
         if (!loadHints())
         {
            return null;
         }
         
         String[] array  = null;
         String   result = null;
         String   cls    = hints.getUastType(key);
         
         if (cls != null && cls.equals("[Ljava.lang.String;"))
         {
            array = hints.getUastStringArray(key);
         }
         
         try
         {
            if (array != null)
            {
               result = array[index];
            }
         }
         
         catch (IndexOutOfBoundsException ioe)
         {
            // nothing to do, just return null
         }
         
         return result;
      }
      
      /**
       * Get a UAST hint using a uniquely named key, accessing a long or
       * <code>null</code> if no such hint exists.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   The named long or <code>null</code>.
       */
      public Long getUastLong(String key)
      {
         if (!loadHints())
         {
            return null;
         }
         
         Long   result = null;
         String cls    = hints.getUastType(key);
         
         if (cls != null && cls.equals("java.lang.Long"))
         {
            result = hints.getUastLong(key);
         }
         
         return result;
      }
      
      /**
       * Get a UAST hint using a uniquely named key, accessing a double or
       * <code>null</code> if no such hint exists.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   The named double or <code>null</code>.
       */
      public Double getUastDouble(String key)
      {
         if (!loadHints())
         {
            return null;
         }
         
         Double result = null;
         String cls    = hints.getUastType(key);
         
         if (cls != null && cls.equals("java.lang.Double"))
         {
            result = hints.getUastDouble(key);
         }
         
         return result;
      }
      
      /**
       * Get a UAST hint using a uniquely named key, accessing a boolean or
       * <code>null</code> if no such hint exists.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   The named boolean or <code>null</code>.
       */
      public Boolean getUastBoolean(String key)
      {
         if (!loadHints())
         {
            return null;
         }
         
         Boolean result = null;
         String  cls    = hints.getUastType(key);
         
         if (cls != null && cls.equals("java.lang.Boolean"))
         {
            result = hints.getUastBoolean(key);
         }
         
         return result;
      }
      
      /**
       * Accesses the class name (data type) of a UAST hint using a uniquely
       * named key.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   The class name of the hint if it exists or 
       *           <code>null</code> if the hint does not exist.
       */
      public String getUastHintType(String key)
      {
         if (!loadHints())
         {
            return null;
         }
         
         return hints.getUastType(key);
      }
      
      /**
       * Checks the existence of a UAST hint using a uniquely named key.
       *
       * @param    key
       *           Unique name for the hint, which is used as a key.
       *
       * @return   <code>true</code> if any hint exists by that key for this
       *           file, <code>false</code> if no such hint exists.
       */
      public boolean isUastHint(String key)
      {
         if (!loadHints())
         {
            return false;
         }
         
         return hints.isUastHint(key);
      }
      
      /**
       * Checks if hint is set for schema.
       *
       * @return   <code>true</code> if "escape" hint is set for schema and equals
       *           <code>true</code>, otherwise <code>false</code>.
       */
      public boolean isSchemaEscaped()
      {
         if (!loadHints())
         {
            return false;
         }
         
         return hints.isSchemaEscaped();
      }
      
      /**
       * Checks if field should be escaped.
       *
       * @param    table
       *           The legacy table name.
       * @param    field
       *           The legacy field name.
       *
       * @return   value of "escape" hint for field if hint is set, otherwise "escape" hint for
       *           table or schema.
       */
      public boolean isFieldEscaped(String table, String field)
      {
         if (!loadHints())
         {
            return false;
         }
         
         return hints.isFieldEscaped(table, field);
      }
      
      /**
       * Checks if table should be escaped.
       *
       * @param    table
       *           The legacy table name.
       *
       * @return   value of "escape" hint for table if hint is set, otherwise "escape" hint for
       *           schema.
       */
      public boolean isTableEscaped(String table)
      {
         if (!loadHints())
         {
            return false;
         }
         
         return hints.isTableEscaped(table);
      }
      
      /**
       * Checks if table is read-only.
       *
       * @param    table
       *           The legacy table name.
       *
       * @return   value of "read-only" hint for table if hint is set, otherwise {@code false}
       */
      public boolean isTableReadOnly(String table)
      {
         if (!loadHints())
         {
            return false;
         }
         return hints.isTableReadOnly(table);
      }
      
      /**
       * Get unit test legacy type. Valid values are "OEUnit" and "ABLUnit", no default.
       * 
       * @return see above
       */
      public String getUnitTestType()
      {
         if (!loadHints())
         {
            return null;
         }

         return hints.getUnitTestType();
      }
      
      /**
       * Checks if table is dirty-read  (that is, it needs to be tracked by the dirty share
       * manager across sessions).
       *
       * @param   table
       *          The legacy table name.
       *
       * @return  value of "dirty-read" hint for table if hint is set, otherwise {@code false}.
       */
      public boolean isTableDirtyRead(String table)
      {
         if (!loadHints())
         {
            return false;
         }
         
         return hints.isTableDirtyRead(table);
      }

      /**
       * Checks if table is dirty-read  (that is, it needs to be tracked by the dirty share
       * manager within the same session).
       *
       * @param   table
       *          The legacy table name.
       *
       * @return  value of "dirty-intra-read" hint for table if hint is set, otherwise {@code false}.
       */
      public boolean isTableDirtyIntraRead(String table)
      {
         if (!loadHints())
         {
            return false;
         }

         return hints.isTableDirtyIntraRead(table);
      }
      
      /**
       * Loads the hints file for an AST if it exists and hasn't already been loaded.
       *
       * @return   {@code true} if there are hints for this file and the hints are loaded, {@code false} if
       *           no hints are available (server runtime mode).
       */
      private boolean loadHints()
      {
         if (Configuration.isRuntimeConfig())
         {
            return false;
         }
         
         if (hints == null)
         {
            try
            {
               Aast src  = getResolver().getSourceAst();
               // locate the original file (.df, .p, .w, etc)
               String filename = (String) src.getAncestor(-1).getAnnotation("srcfile");
               if (filename == null)
               {
                  // if that fails (expected annotation not present), locate the intermediary/artifact file
                  filename = src.getFilename();
               }
               
               hints = getUastHints(Configuration.toFile(filename).getAbsolutePath());
            }
            catch (Exception exc)
            {
               // any failure drops us out
               return false;
            }
         }
         
         return true;
      }
      
      /**
       * Look for the hints file of a specific 4GL source file. Supported cases:
       * <ul>
       *    <li>.df, .p, .w code source file. The hints are looked in same directory, with the .hints suffix
       *          appended to the file's name</li>
       *    <li>namespace.schema.hints (supported but deprecated, use the above, instead)</li>
       * </ul>
       * 
       * @param   sourcePath
       *          The path of the source file (source or data definition)
       *
       * @return  If a .hints file is detected for respective parameter file a {@code UastHints} is returned.
       *          Otherwise {@code null} is returned.
       */
      private UastHints getUastHints(String sourcePath)
      {
         if (new File(sourcePath + UastHints.HINTS_SUFFIX).exists())
         {
            // the preferred way: the .hints suffix is appended to source filename
            return new UastHints(sourcePath);
         }
         
         String renPath = sourcePath;
         if (sourcePath.endsWith(".df"))
         {
            // the deprecated way: replace [.df] with [.schema.hints] suffix 
            renPath = sourcePath.substring(0, sourcePath.length() - 3) + SchemaWorker.SCHEMA_POSTFIX;
         }
         else if (sourcePath.endsWith(AstGenerator.DICT_POSTFIX))
         {
            // the deprecated way: replace [.dict] with [.schema.hints] suffix 
            int len = AstGenerator.DICT_POSTFIX.length();
            renPath = sourcePath.substring(0, sourcePath.length() - len) + SchemaWorker.SCHEMA_POSTFIX;
         }
         
         return new UastHints(renPath);
      }
   }
}