# BaseAdminRestHandler.java

    /*
    ** Module   : AdminRestHandler.java
    ** Abstract : Class that handles requests for administration of 4GL resources purposes.
    **
    ** Copyright (c) 2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 OM  20250310 First revision, extracted common REST code from Tenant/AdminHandler.
    */

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

    import com.fasterxml.jackson.core.*;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.logging.*;
    import org.eclipse.jetty.server.*;
    import org.eclipse.jetty.server.handler.*;
    import javax.servlet.*;
    import javax.servlet.http.*;
    import java.io.*;
    import java.util.*;
    import java.util.logging.*;
    import java.util.stream.*;

    /**
     * Base class for all administration REST handlers.
     * <br>
     * The derived classes will declare their {@code type} and {@code contextPath} when they are constructed and
     * will receive the notifications for processing requests by implementing the four specific methods
     * corresponding to HTTP verbs: {@code handleGet()}, {@code handlePut()}, {@code handlePost()}, and
     * {@code handleDelete()}.
     */
    public abstract class BaseAdminRestHandler
    extends AbstractHandler
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(BaseAdminRestHandler.class);

       /** The URL path it will handle.*/
       protected final String contextPath;

       /** The handler used for authentication and authorization. */
       protected final WebAuthHandler authenticationHandler;

       /** Instance used for executing requests in a context. */
       protected final LegacyWebSecurityManager wsm;

       /** Used to parse the HTTP body as a JSON object. */
       protected final ThreadLocal<ObjectMapper> mapper = ThreadLocal.withInitial(ObjectMapper::new);

       /**
        * Constructs a new handler for administration of some resource using REST APIs.
        *
        * @param   contextPath
        *          The URL path it will handle.
        * @param   authenticationHandler
        *          The WebAuthHandler used for authorisation.
        */
       protected BaseAdminRestHandler(String contextPath, WebAuthHandler authenticationHandler)
       {
          this.wsm = SecurityManager.getInstance().legacyWebSm;
          this.authenticationHandler = authenticationHandler;
          this.contextPath = contextPath;
       }

       /**
        * Method for handling the requests. Will perform checking, authentication and authorization before
        * dispatching the execution to proper method/verb.
        *
        * @param   target
        *          The target of the request - either a URI or a name.
        * @param   baseRequest
        *          The original unwrapped request object.
        * @param   request
        *          The request either as the {@link Request} object or a wrapper of that request.
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        */
       @Override
       public void handle(String target,
                          Request baseRequest,
                          HttpServletRequest request,
                          HttpServletResponse response)
       throws IOException, ServletException
       {
          // quick validation
          if (baseRequest.isHandled()) // already handled by other handler?
          {
             return;
          }

          if (target == null) // should not be possible
          {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
             return;
          }
          else if (!target.startsWith(contextPath))
          {
             return; // not my business
          }

          baseRequest.setHandled(true);
          response.setContentType("application/json");

          // check authorization:
          String token = authenticationHandler.authorize(target, request, response);
          if (token == null)
          {
             response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
             return;
          }

          String body = request.getReader().lines().collect(Collectors.joining());
          target = trimContextPrefix(target);

          switch (baseRequest.getMethod())
          {
             case "GET":
                handleGet(target, body, response, token);
                break;
             case "POST":
                handlePost(target, body, response, token);
                break;
             case "PUT":
                handlePut(target, body, response, token);
                break;
             case "DELETE":
                handleDelete(target, body, response, token);
                break;
             default:
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
          }
       }

       /**
        * Implement this method for handling the GET requests. The result should be written directly to
        * {@code response}'s writer. The default implementation will simply return error 400 / Bad request.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handleGet(String target, String body, HttpServletResponse response, String token)
       {
          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       }

       /**
        * Implement this method for handling the POST requests. The result should be written directly to
        * {@code response}'s writer. The default implementation will simply return error 400 / Bad request.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handlePost(String target, String body, HttpServletResponse response, String token)
       {
          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       }

       /**
        * Implement this method for handling the PUT requests. The result should be written directly to
        * {@code response}'s writer. The default implementation will simply return error 400 / Bad request.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handlePut(String target, String body, HttpServletResponse response, String token)
       {
          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       }

       /**
        * Implement this method for handling the DELETE requests. The result should be written directly to
        * {@code response}'s writer. The default implementation will simply return error 400 / Bad request.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handleDelete(String target, String body, HttpServletResponse response, String token)
       {
          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       }

       /**
        * Execute the specified task in the context associated with the web request identified via the specified
        * authentication token.
        *
        * @param    token
        *           The authentication token.
        * @param    task
        *           The task to be performed.
        */
       protected final void executeInContext(String token, Runnable task)
       {
          wsm.executeInContext(token, () -> {
             AdminServerImpl.setTargetLive(true, false, true);
             task.run();
          }, true);
       }

       /**
        * Execute the given task and assign a message to the HTTP response.
        *
        * @param   token
        *          The token used in the request.
        * @param   response
        *          The HTTP response.
        * @param   task
        *          The task to be executed.
        * @param   result
        *          The field that will contain the result after the execution is performed.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operations finish successfully, {@code false} otherwise.
        */
       protected final boolean executeAndWriteResponse(String token,
                                                       HttpServletResponse response,
                                                       Runnable task,
                                                       Boolean[] result,
                                                       List<String> errorSink)
       {
          return executeAndWriteResponse(token, response, task, "success", result, errorSink);
       }

       /**
        * Execute the given task and assign a message to the HTTP response.
        *
        * @param   token
        *          The token used in the request.
        * @param   response
        *          The HTTP response.
        * @param   task
        *          The task to be executed.
        * @param   message
        *          The key value that will be written in the JSON response.
        * @param   result
        *          The field that will contain the result after the execution is performed.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operations finish successfully, {@code false} otherwise.
        */
       protected final boolean executeAndWriteResponse(String token,
                                                       HttpServletResponse response,
                                                       Runnable task,
                                                       String message,
                                                       Boolean[] result,
                                                       List<String> errorSink)
       {
          wsm.executeInContext(token, task, true);

          try
          {
             if (errorSink.isEmpty())
             {
                response.getWriter().write("{\n \"" + message + "\": " + result[0] + " \n}");
                return true;
             }

             ObjectMapper objectMapper = mapper.get();
             ObjectNode outerNode = objectMapper.createObjectNode();
             outerNode.put(message, result[0]);

             ArrayNode errorArray = objectMapper.createArrayNode();
             for (String errMsg : errorSink)
             {
                errorArray.add(errMsg);
             }

             outerNode.set("errors", errorArray);
             response.getWriter().write(outerNode.toString());
             return true;
          }
          catch (IOException e)
          {
             LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
             return false;
          }
       }

       /**
        * Cleaning up the target by removing the context path. This information is not useful since all requests
        * dispatched to this handler are filtered to match this prefix.
        */
       private String trimContextPrefix(String originalTarget)
       {
          if (!originalTarget.startsWith(contextPath))
          {
             LOG.warning(
                   "Invalid context received '" + originalTarget + "'. Was expecting '" + contextPath + "'.");
             return originalTarget;
          }

          String newTarget = originalTarget.substring(contextPath.length());

          // drop the starting "/" if any:
          if (newTarget.startsWith("/"))
          {
             newTarget = newTarget.substring(1);
          }
          return newTarget;
       }

       /**
        * Parse a JSON and extract the value (String, int or boolean for the moment) of a given field.
        *
        * @param   body
        *          The JSON data.
        * @param   fieldName
        *          The field for which the value is extracted.
        * @param   cls
        *          The expected type of value that is retrieved.
        *
        * @return  The value of the given field.
        */
       protected Object extractFirstLevelValue(String body, String fieldName, Class<?> cls)
       {
          try
          {
             JsonNode rootNode = mapper.get().readTree(body);
             if (rootNode.has(fieldName))
             {
                if (cls == String.class)
                {
                   return rootNode.get(fieldName).asText();
                }
                else if (cls == boolean.class || cls == Boolean.class)
                {
                   return rootNode.get(fieldName).asBoolean();
                }
                else if (cls == int.class || cls == Integer.class)
                {
                   return rootNode.get(fieldName).asInt();
                }
             }
          }
          catch (JsonProcessingException e)
          {
             LOG.log(Level.WARNING, "Unable to parse JSON due to " + e.getMessage());
          }

          return null;
       }
    }