# RestService.java

    /*
    ** Module   : RestService.java
    ** Abstract : Details of a REST service specification.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20190614 First version.
    ** 002 CA  20190628 Proper SINGLETON and SINGLE-RUN implementation; misc improvements.
    ** 003 CA  20200514 Refactoring to allow common code for SOAP services support.
    ** 004 CA  20210226 Pass the argument modes to the invocation, as the Agent will not pre-process the args
    **                  otherwise.
    **     CA  20211112 Fixed some problems resetting the last invoke configuration.
    **     CA  20220120 Mutable state must be thread-local.
    **     CA  20220329 Added support for REST services written directly in Java.
    **     CA  20220405 Added authentication and authorization for web requests.  When this is enabled, the target
    **                  API call will be executed under the authenticated FWD context, and not the agent's context.
    ** 005 SBI 20230216 Fixed buildDefinitionParameters() because defaultValue() was added to LegacyServiceParameter.
    */

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

    import java.lang.annotation.*;
    import java.util.*;
    import java.util.function.*;

    import com.goldencode.p2j.util.*;

    /**
     * Base class for an invocation implementation in a REST or SOAP request.
     */
    public abstract class RestService
    {
       /** The REST key. */
       public final ServiceKey key;

       /** The annotation with the details for this REST operation. */
       public final LegacyService ls;

       /** The service signature. */
       public final LegacySignature serviceSignature;

       /** The invocation timeout. */
       protected final int timeout;

       /** The configuration for the last service call. */
       protected ThreadLocal<InvokeConfig> cfg = new ThreadLocal<>();

       /**
        *  The parameters as they appear at the target Java method, converted to {@link LegacyServiceParameter}.
        */
       protected LegacyServiceParameter[] definitionParameters = null;

       /**
        * Create a new instance to handle invocations for the specified REST operation.
        *
        * @param    ls
        *           The operation details.
        * @param    sig
        *           The service signature.  Non-null only for legacy services.
        * @param    key
        *           The service key.
        * @param    timeout
        *           The invocation timeout.
        */
       public RestService(LegacyService ls, LegacySignature sig, ServiceKey key, int timeout)
       {
          this.ls = ls;
          this.serviceSignature = sig;
          this.key = key;
          this.timeout = timeout;

          for (LegacyServiceParameter lsp : ls.parameters())
          {
             if (lsp.name().isEmpty())
             {
                throw new RuntimeException("The name for parameter " + lsp.ordinal() + " at service " +
                                           ls.name() + " must be provided");
             }
             else if (lsp.type().isEmpty())
             {
                throw new RuntimeException("The type for parameter " + lsp.ordinal() + " at service " +
                                           ls.name() + " must be provided");
             }
          }

          buildDefinitionParameters();
       }

       /**
        * Invoke this service.
        *
        * @param    helper
        *           The appserver helper used to execute the requests.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        * @param    args
        *           The arguments.
        *
        * @return   The returned value.
        */
       public abstract Object invoke(AppServerHelper helper, String token, Object[] args);

       /**
        * Check if the last call of this service was for running a persistent procedure.
        *
        * @return   See above.
        */
       public boolean isPersistent()
       {
          InvokeConfig cfg = this.cfg.get();

          return cfg != null && cfg.isPersistent();
       }

       /**
        * Get the request ID for the last call.
        *
        * @return   See above.
        */
       public String getRequestID()
       {
          InvokeConfig cfg = this.cfg.get();

          // if cfg is not set (i.e. argument parsing fails), then build a request ID)
          return cfg == null ? nextRequestId() : cfg.getRequestId();
       }

       /**
        * Get the persistent procedure handle set by the last call.
        *
        * @return   See above.
        */
       public handle getProcedureHandle()
       {
          InvokeConfig cfg = this.cfg.get();

          return cfg == null ? null : cfg.getProcedureHandle();
       }

       /**
        * Get the {@link #definitionParameters}, as they appear at the Java method's definition.
        * <pre>
        * These are built via {@link #buildDefinitionParameters()}, depending on service's type.  For legacy REST
        * services they are built using the signature's {@link LegacyParameter} annotation, while for Java REST
        * services, they are built using the Java method's signature.
        *
        * @return   See above.
        */
       public LegacyServiceParameter[] getDefinitionParameters()
       {
          return definitionParameters;
       }

       /**
        * Get the execution mode for this service.
        *
        * @return   See above.
        */
       public String getExecutionMode()
       {
          return ls.executionMode();
       }

       /**
        * Reset the last invoke {@link #cfg configuration}.
        */
       public void reset()
       {
          cfg.set(null);
       }

       /**
        * Compute a request ID, a 22 byte string with ':' delimiters after the 7th, 11th and 17th byte.
        *
        * @return   See above.
        */
       protected String nextRequestId()
       {
          Random r = new Random();
          //
          //  0 1 2 3 4 5 6 7  8 9 0 1  2 3 4 5 6 7   8 9  0 1
          // 25f594c9579abdba:017f9ab5:016d6c6ca0d0:-7fff#02fa
          byte[] bytes = new byte[20];
          r.nextBytes(bytes);
          String s = "";
          for (int i = 0; i < bytes.length; i++)
          {
             int k = bytes[i] & 0xFF;
             String hex = Integer.toHexString(k);
             while (hex.length() < 2)
             {
                hex = "0" + hex;
             }

             s = s + hex + (i == 7 || i == 11 || i == 17 ? ":" : "");
          }

          s = s + "#" + Integer.toHexString(r.nextInt(65535));

          return s;
       }

       /**
        * Resolve the parameter modes from the {@link #serviceSignature signature}.
        */
       protected String resolveModes()
       {
          String modes = null;
          for (LegacyParameter lp : serviceSignature.parameters())
          {
             if (modes == null)
             {
                modes = "";
             }

             switch (lp.mode())
             {
                case "INPUT":
                   modes = modes + "I";
                   break;
                case "OUTPUT":
                   modes = modes + "O";
                   break;
                case "INPUT-OUTPUT":
                   modes = modes + "U";
                   break;
             }
          }

          return modes;
       }

       /**
        * Get the parser to be used for parsing the arguments, to be attached to the service's parser.
        *
        * @param    argumentsParser
        *           The service's parser.
        *
        * @return   Always a new {@link LegacyArgumentsParser} instance.
        */
       protected ArgumentsParser getParser(ServiceArgumentsParser argumentsParser)
       {
          return new LegacyArgumentsParser(argumentsParser);
       }

       /**
        * Get the serialized to be used for writing the response arguments.
        *
        * @param    argumentsSerializer
        *           The service's serializer.
        *
        * @return   Always a {@link LegacyArgumentsSerializer} instance.
        */
       protected ArgumentsSerializer getSerializer(ResponseArguments argumentsSerializer)
       {
          return new LegacyArgumentsSerializer(argumentsSerializer);
       }

       /**
        * Build the parameters, from the target method's {@link LegacyParameter} annotations.  This will set the
        * {@link #definitionParameters} array.
        */
       protected void buildDefinitionParameters()
       {
          BiFunction<LegacyParameter, Integer, LegacyServiceParameter> create = (lp, ordinal) ->
          new LegacyServiceParameter()
          {
             @Override
             public Class<? extends Annotation> annotationType()
             {
                return LegacyServiceParameter.class;
             }

             @Override
             public String type()
             {
                return lp.type();
             }

             @Override
             public int ordinal()
             {
                return ordinal;
             }

             @Override
             public String name()
             {
                return lp.name();
             }

             @Override
             public boolean output()
             {
                return lp.mode().equals("OUTPUT") || lp.mode().equals("INPUT-OUTPUT");
             }

             @Override
             public boolean input()
             {
                return lp.mode().equals("INPUT") || lp.mode().equals("INPUT-OUTPUT");
             }

             @Override
             public int extent()
             {
                return lp.extent();
             }

             public String target() { return null; }
             public String source() { return null; }
             public boolean returnValue() { return false; }
             public boolean allowUnknown() { return false; }
             @Override
             public String defaultValue()
             {
                return "";
             }
          };

          int size = serviceSignature.parameters().length;
          LegacyServiceParameter[] params = new LegacyServiceParameter[size];
          for (int i = 0; i < params.length; i++)
          {
             LegacyParameter lp = serviceSignature.parameters()[i];
             int ordinal = i + 1;

             params[i] = create.apply(lp, ordinal);
          }
          definitionParameters = params;
       }
    }