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