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

    }