ProError.java

/*
** Module   : ProError.java
** Abstract : Implementation of the Progress.Lang.ProError builtin class.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description--------------------------------
** 001 OM  20181218 First version.
** 002 CA  20190219 Runtime implementation.
** 003 CA  20190527 Added annotations for property accessors and class event methods.
** 005 CA  20190720 Emit the 'this' reference for the BlockManager APIs.
** 006 CA  20191024 Added method support levels and updated the class support level.
** 007 CA  20200324 Error message access is 0-based, not 1-based.
**         20200330 Some refactoring to allow a generic way to build errors with messages.
** 008 ME  20200417 Default severity is zero, on add message unknown text is set to empty 
**         and unknown num to zero.
** 009 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
**                  signature.
**     CA  20220208 Implemented the callstack property.
**     VVT 20221221 Serialization support added (see #4658).
** 010 ME  20230503 Make severity private (#6410). 
** 011 CA  20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
**                  dropped if is a no-op.
** 012 VVT 20240213 populateCallStack(): legacy class methods are now included in stack trace. See #8130.
**     VVT 20240213 populateCallStack(): Exclude two top stack trace entries. See #8130-7.
** 013 VVT 20240326 populateCallStack(): add missing stack entries to the 4gl stack stace.
**                  Optimise using StringBuilder. See #8130.
** 014 VVT 20240706 Regression in ProError:populateCallStack introduced in the 20240213
**                  change fixed. SourceNameMapper.convertJavaProg() renamed. See #8613.
** 015 AP  20250121 The call stack populated at the moment of object creation. The contents
**                  of populateCallStack method optimized using a StringBuilder. See discussion
**                  around #3827-527. Excluded the first 2 call stack entries and other irrelevant methods.
** 016 GBB 20250218 Implementing custom methods _isDisplayed and _setDisplayed.
 */

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

import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.BlockManager.*;

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

import com.goldencode.p2j.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.InternalEntry.*;

/**
 * Implementation of the Progress.Lang.ProError builtin class.
 */
@LegacyResource(resource = "Progress.Lang.ProError")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_BASIC)
@LegacySerializable
public class ProError
extends BaseObject
implements LegacyError,
           Externalizable
{
   /** The severity property. */
   private integer severity = new integer(0);
   
   /** The recorded errors. */
   protected List<NumberedException> errors = new ArrayList<>();
   
   /** The call stack at the time the error was thrown. */
   private String callStack = null;

   /** A custom flag to indicate if the error has already been displayed. */
   private boolean isDisplayed;
   
   /**
    * Create a new instance with the specified error.
    *  
    * @param   error
    *          The legacy error.
    */
   public ProError(NumberedException error)
   {
      errors.add(error);
      populateCallStack();
   }
   
   /**
    * Create a new instance with the specified errors.
    *  
    * @param   errors
    *          The legacy errors.
    */
   public ProError(NumberedException[] errors)
   {
      this.errors.addAll(Arrays.asList(errors));
      populateCallStack();
   }
   
   /**
    * Implicit constructor used by sub-classes.
    */
   protected ProError()
   {
      populateCallStack();
   }

   /**
    * Implicit, no parameter, method associated with the implicit legacy constructor for the
    * <code>Progress.Lang.ProError</code> class.
    */
   @LegacySignature(type = Type.CONSTRUCTOR)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void __lang_ProError_constructor__()
   {
      internalProcedure(this, "__lang_ProError_constructor__", new Block((Body) () -> 
      {
         __lang_BaseObject_constructor__();
      }));
   }
   
   /**
    * Explicit method associated with a legacy constructor for the
    * <code>Progress.Lang.ProError</code> class.
    * 
    * @param    _msg
    *           The message.
    * @param    _num
    *           The message number.
    */
   @LegacySignature(type = Type.CONSTRUCTOR, parameters = 
   {
      @LegacyParameter(name = "msg",  type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "num",  type = "INTEGER", mode = "INPUT"),
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void __lang_ProError_constructor__(character _msg, integer _num)
   {
      character msg = TypeFactory.initInput(_msg);
      integer num = TypeFactory.initInput(_num);
      internalProcedure(this, "__lang_ProError_constructor__", new Block((Body) () -> 
      {
         __lang_ProError_constructor__();
         
         addMessage(msg, num);
      }));
   }

   /**
    * Get the number of messages.
    * 
    * @return   See above.
    */
   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "NumMessages")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public integer getNumMessages()
   {
      return new integer(errors.size());
   }
   
   /**
    * Get the call stack.
    * 
    * @return   See above.
    */
   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "CallStack")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public character getCallStack()
   {
      return new character(callStack);
   }
   
   /**
    * Populate the {@link #callStack} traces.  Any existing callstack will be assumed to be the server's
    * stacktrace from the remote call.
    */
   private void populateCallStack()
   {
      String serverCallStack = callStack;
      callStack = null;
      final String sep = System.getProperty("line.separator");

      if (SessionUtils.isErrorStackTrace().booleanValue())
      {
         // if ERROR-STACK-TRACE is enabled, computed it
         final StringBuilder out = new StringBuilder();
         
         final StackTraceElement[] st = Thread.currentThread().getStackTrace();
         final List<StackTraceElement> inBlock = new ArrayList<>();
         
         // Start from the entry 2 to skip the first two entries, which are for the call
         // to Thread.getStackTrace() and this populateCallStack() method.
         for (int i = 2; i < st.length; i++)
         {
            final StackTraceElement el = st[i];
   
            final String className = el.getClassName();
            final Class clazz;
            try
            {
               clazz = Class.forName(className);
            }
            catch (@SuppressWarnings("unused") final ClassNotFoundException e)
            {
               // FIXME: what is the proper action here?
               continue;
            }
            
            // Filter-out non-legacy elements and constructors from ProError subclasses
            if (!SourceNameMapper.isLegacyOrConvertedClass(clazz) ||
               (ProError.class.isAssignableFrom(clazz) && st[i].getMethodName().equals("<init>")))
            {
               continue;
            }
            
            final String methodName = el.getMethodName();
            final int lineNumber = el.getLineNumber();
            
            if (methodName.indexOf('$') >= 0)
            {
               // a lambda with known line number
               if (lineNumber != -1)
               {
                  inBlock.add(el);
               }
               continue;
            }
   
            final String fileName = el.getFileName();
            
            // Add internal entries if any. These entries inherit all current
            // element attributes except the line number
            for(final StackTraceElement blockEl : inBlock)
            {
               if(className.equals(blockEl.getClassName()))
               {
                  if(out.length() > 0)
                  {
                     out.append(sep);
                  }
                     
                  out.append(ProcedureManager.buildTraceLine(
                             className,
                             methodName,
                             fileName,
                             blockEl.getLineNumber()));                  
               }
            }
            inBlock.clear();
            
            if(out.length() > 0)
            {
               out.append(sep);
            }
               
            out.append(ProcedureManager.buildTraceLine(className, methodName, fileName, lineNumber));
         }
   
         final String newStack = out.toString();
         if (!newStack.isEmpty())
         {
            callStack = newStack;
         }
      }
      
      if (serverCallStack != null)
      {
         // if the server's call stack was sent, add it.
         callStack = (callStack == null ? "" : callStack + sep) + 
                     "Server StackTrace:" + sep + serverCallStack;
      }
   }
   
   /**
    * Get the error's severity.
    * 
    * @return   See above.
    */
   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "Severity")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public integer getSeverity()
   {
      return new integer(severity);
   }
   
   /**
    * Set the error's severity.
    * 
    * @param   s
    *          The severity.
    */
   @LegacySignature(type = Type.SETTER, name = "Severity", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   @Override
   public void setSeverity(integer s)
   {
      severity.assign(s);
   }
   
   /**
    * Definition of the GetMessage legacy method.
    * 
    * @param    idx
    *           The message index.
    *           
    * @return   The error message for the specified error.
    */
   @Override
   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "GetMessage", parameters = 
   {
      @LegacyParameter(name = "idx", type = "INTEGER", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public character getMessage(integer idx)
   {
      if (!validIndex(idx))
      {
         return new character("");
      }
      
      return new character(errors.get(idx.intValue() - 1).getMessage());
   }
   
   /**
    * Implementation of the GetMessageNum legacy method.
    * 
    * @param    idx
    *           The message index.
    *           
    * @return   The error message number for the specified error.
    */
   @Override
   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "GetMessageNum", parameters = 
   {
      @LegacyParameter(name = "idx",  type = "INTEGER", mode = "INPUT"),
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public integer getMessageNum(integer idx)
   {
      if (!validIndex(idx))
      {
         return new integer(0);
      }
      
      return new integer(errors.get(idx.intValue() - 1).getNumber());
   }

   /**
    * Returns a flag to indicate if the error has already been displayed.
    *
    * @return   See above.
    */
   @Override
   public boolean _isDisplayed()
   {
      return isDisplayed;
   }

   /**
    * Sets a flag to true to indicate the error has already been displayed.
    */
   @Override
   public void _setDisplayed()
   {
      isDisplayed = true;
   }

   /**
    * Implementation of the AddMessage legacy method.
    * 
    * @param    msg
    *           The error message.
    * @param    num
    *           The error message number.
    */
   public void addMessage(character msg, integer num)
   {
      int inum = num.isUnknown() ? 0 : num.intValue();
      String smsg = msg.isUnknown() ? "" : msg.getValue();
      errors.add(new NumberedException(smsg, inum));
   }
   
   /**
    * The object implements the writeExternal method to save its contents
    * by calling the methods of DataOutput for its primitive values or
    * calling the writeObject method of ObjectOutput for objects, strings,
    * and arrays.
    *
    * @serialData Overriding methods should use this tag to describe
    *             the data layout of this Externalizable object.
    *             List the sequence of element types and, if possible,
    *             relate the element to a public/protected field and/or
    *             method of this Externalizable class.
    *
    * @param out the stream to write the object to
    * @exception IOException Includes any I/O exceptions that may occur
    */
   @Override
   public void writeExternal(final ObjectOutput out)
   throws IOException
   {
      out.writeObject(severity);
      out.writeObject(errors);
      out.writeObject(callStack);      
   }

   /**
    * The object implements the readExternal method to restore its
    * contents by calling the methods of DataInput for primitive
    * types and readObject for objects, strings and arrays.  The
    * readExternal method must read the values in the same sequence
    * and with the same types as were written by writeExternal.
    *
    * @param in the stream to read data from in order to restore the object
    * @exception IOException if I/O errors occur
    * @exception ClassNotFoundException If the class for an object being
    *              restored cannot be found.
    */
   @Override
   public void readExternal(final ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      severity = (integer) in.readObject();
      errors   = (List<NumberedException>) in.readObject();
      callStack = (String) in.readObject();
      
      // append the current stack trace to the server one read
      populateCallStack();
   }
   /**
    * Check if the specified index is valid for {@link #errors}.
    * 
    * @param    idx
    *           The index.
    *           
    * @return   Must be not-unknown and within the size of the {@link #errors}.
    */
   protected boolean validIndex(integer idx)
   {
      if (idx.isUnknown())
      {
         return false;
      }
      
      int i = idx.intValue() - 1;
      if (i < 0 || i >= errors.size())
      {
         return false;
      }
      
      return true;
   }

}