# ClassEvent.java

    /*
    ** Module   : ClassEvent.java
    ** Abstract : Implementation of 4GL-style class event management.
    **
    ** Copyright (c) 2018-2022, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20181216 First version.
    ** 002 CA  20190219 Added runtime support.
    ** 003 CA  20220106 For PUBLISH statement, arguments must be evaluated only if the subscription is found. For
    **                  CLASS events, they are evaluated immediately (that's why there is no lambda emitted for
    **                  them).
    */

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

    import java.lang.reflect.*;
    import java.util.*;

    import com.goldencode.p2j.oo.lang.*;

    /**
     * Management of class events.
     */
    public class ClassEvent
    {
       /** Representation of the event's signature. */
       private final InternalEntry signature;

       /** The event's name. */
       private final character event = new character(UUID.randomUUID().toString());

       /**
        * The event's publisher (object instance where it was defined or the pseudo-object for the
        * legacy class, if is static.
        */
       private final handle publisher = new handle();

       /** The event's legacy name. */
       private final String evtName;

       /**
        * Create a new class event from the given publisher (which is a static class).
        *
        * @param    publisher
        *           The legacy type.
        * @param    evtName
        *           The legacy class event name.
        * @param    fieldName
        *           The converted name for the Java field.
        */
       public ClassEvent(Class<? extends _BaseObject_> publisher, String evtName, String fieldName)
       {
          ObjectOps.load(publisher);
          this.publisher.assign(new ExternalProgramWrapper(ObjectOps.getStaticInstance(publisher)));

          this.evtName   = evtName;
          this.signature = buildSignature(publisher, fieldName);
       }

       /**
        * Create a new class event from the given publisher.
        *
        * @param    publisher
        *           The legacy object instance.
        * @param    evtName
        *           The legacy class event name.
        * @param    fieldName
        *           The converted name for the Java field.
        */
       public ClassEvent(_BaseObject_ publisher, String evtName, String fieldName)
       {
          this.publisher.assign(new ExternalProgramWrapper(publisher));

          this.evtName   = evtName;
          this.signature = buildSignature(publisher.getClass(), fieldName);
       }

       /**
        * Notify all subscribers by publishing this event using the given arguments.
        *
        * @param    args
        *           The published arguments.
        */
       public void publish(Object... args)
       {
          // the modes are not required - it is a 4GL compile-time error to not match exactly as
          // the specified signature, but only for the OO subscribers.  internal procedures can
          // convert arguments.
          ProcedureManager.publish(true, event, publisher, signature.getParameterModes(), () -> args);
       }

       /**
        * Unsubscribe the specified method, in the given object, from this event.
        * <p>
        * The method must be defined in the given object.
        *
        * @param    subscriber
        *           The subscriber reference.
        * @param    method
        *           The method name.
        */
       public void subscribe(handle subscriber, character method)
       {
          if (!valid(subscriber, method))
          {
             return;
          }

          if (!ProcedureManager.subscribe(subscriber, event, publisher, method, null))
          {
             ErrorManager.recordOrThrowError(15329,
                                             "Subscribe operation failed. " +
                                             method.toStringMessage() + " has already subscribed " +
                                             "to the " + evtName + " event",
                                             false, false);
          }
       }

       /**
        * Subscribe the specified method, in the given object, for notifications from this event.
        * <p>
        * The method must be defined in the given object.
        *
        * @param    ref
        *           The object reference.
        * @param    method
        *           The method name.
        */
       public void subscribe(object<? extends _BaseObject_> ref, character method)
       {
          // this API is emitted always with a valid method
          // object may be a progress.lang.class, then method must be static method in the target class
          // referent is always valid

          Object referent = ref.ref();
          Class<? extends _BaseObject_> type = (Class<? extends _BaseObject_>) referent.getClass();
          if (referent instanceof LegacyClass)
          {
             type = ((LegacyClass) referent).getType();
             referent = ObjectOps.getStaticInstance(type);
          }

          // hook the method with a real Java method based on the signature!
          InternalEntry target = SourceNameMapper.findLegacyMethod(type,
                                                                   method.toStringMessage(),
                                                                   signature);
          if (target == null)
          {
             // error (but should never happen)
             throw new RuntimeException("Could not resolve the target method " +
                                        method.toStringMessage() + " in class " + type +
                                        " having the event's signature!");
          }

          handle subscriber = new handle(new ExternalProgramWrapper(referent));
          if (!ProcedureManager.subscribe(subscriber, event, publisher, method, target))
          {
             ErrorManager.recordOrThrowError(15329,
                                             "Subscribe operation failed. " +
                                             method.toStringMessage() + " has already subscribed " +
                                             "to the " + evtName + " event",
                                             false, false);
          }
       }

       /**
        * Unsubscribe the specified internal procedure, in the given program, from this event.
        * <p>
        * The internal procedure must be defined in the given program.
        *
        * @param    subscriber
        *           The program handle.
        * @param    method
        *           The method name.
        */
       public void unsubscribe(handle subscriber, character method)
       {
          if (!valid(subscriber, method))
          {
             return;
          }

          ProcedureManager.unsubscribe(false, subscriber, event, publisher, method);
       }

       /**
        * Unsubscribe the specified method, in the given object, from this event.
        * <p>
        * The method must be defined in the given object.
        *
        * @param    ref
        *           The object reference.
        * @param    method
        *           The method name.
        */
       public void unsubscribe(object<? extends _BaseObject_> ref, character method)
       {
          // this API is emitted always with a valid method
          // object may be a progress.lang.class, then method must be static
          // referent is always valid

          Object referent = ref.ref();
          if (referent instanceof LegacyClass)
          {
             referent = ObjectOps.getStaticInstance(((LegacyClass) referent).getType());
          }
          handle subscriber = new handle(new ExternalProgramWrapper(referent));
          ProcedureManager.unsubscribe(false, subscriber, event, publisher, method);
       }

       /**
        * Validate the specified subscriber and method name.
        *
        * @param    subscriber
        *           The subscriber handle - must be a valid external program.
        * @param    method
        *           The method name - must be a defined internal procedure.
        *
        * @return   See above.
        */
       private boolean valid(handle subscriber, character method)
       {
          if (!ProcedureManager.isProcedure(subscriber))
          {
             ErrorManager.recordOrThrowError(15327,
                                             "Unable to resolve handler when subscribing to or " +
                                             "unsubscribing from event " + evtName,
                                             false, false);
             return false;
          }

          // the method must exist in the given handle, but the signature is not validated
          String iename = method.isUnknown() ? "" : method.toStringMessage();
          String pname = ProcedureManager.getAbsoluteName(subscriber.get());
          String jname = SourceNameMapper.getMethodName(pname, iename, false);
          if (jname == null)
          {
             int[] nums = { 14589, 15327 };
             String[] texts =
             {
                "Subscribe or Unsubscribe method failed: unable to find internal procedure " + iename,
                "Unable to resolve handler when subscribing to or unsubscribing from event " +
                evtName + " "
             };

             ErrorManager.recordOrThrowError(nums, texts, false, false);
             return false;
          }

          return true;
       }

       /**
        * Build an {@link InternalEntry} using the {@link LegacySignature} annotation appearing at the
        * Java field declaring this class event.
        *
        * @param    def
        *           The OO class.
        * @param    field
        *           The converted field name.
        *
        * @return   An {@link InternalEntry} representing the event's signature.
        */
       private InternalEntry buildSignature(Class<?> def, String field)
       {
          if (def == null || !_BaseObject_.class.isAssignableFrom(def))
          {
             throw new RuntimeException("Can't initialize CLASS EVENT signature!");
          }
          try
          {
             Field f = def.getDeclaredField(field);
             LegacySignature ls = f.getAnnotation(LegacySignature.class);

             return SourceNameMapper.buildInternalEntry(ls, field);
          }
          catch (NoSuchFieldException e)
          {
             return buildSignature(def.getSuperclass(), field);
          }
          catch (Exception e)
          {
             throw new RuntimeException("Can't initialize CLASS EVENT signature!", e);
          }
       }
    }