WebHandler.java

/*
** Module   : WebHandler.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20190526 First version, stubs taken by converting the skeleton using FWD.
** 002 CA  20191024 Added method support levels and updated the class support level.
** 003 ME  20200410 Added setter method for logger.
** 004 ME  20201009 Cast result of dynamic method invoke to integer.
** 005 CA  20210221 Fixed parameters at the LegacySignature annotation.
** 006 ME  20210423 Expose request web context same as for response (WEB-CONTEXT surrogate).
**     CA  20220923 Variable definitions (including associated with parameters) must be done always outside of 
**                  the BlockManager API
** 007 CA  20230707 Let 'handleRequest' actually return a value.
** 008 CA  20230817 Removed ObjectOps.register, as this registration is handled by runtime.
** 009 CA  20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
**                  dropped if is a no-op.
*/

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

import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.Action;
import com.goldencode.p2j.util.BlockManager.Condition;
import com.goldencode.p2j.oo.core.Assert;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.oo.logging.IlogWriter;
import com.goldencode.p2j.oo.logging.LogLevelEnum;
import com.goldencode.p2j.oo.logging.VoidLogger;
import com.goldencode.p2j.oo.net.http.MethodEnum;
import com.goldencode.p2j.oo.net.http.StatusCodeEnum;
import com.goldencode.p2j.security.*;

import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.InternalEntry.Type;

import javax.servlet.http.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in OpenEdge/Web/WebHandler.cls).
 */
@LegacyResource(resource = "OpenEdge.Web.WebHandler")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_PARTIAL)
public abstract class WebHandler
extends BaseObject
implements com.goldencode.p2j.oo.web.IwebHandler,
           com.goldencode.p2j.oo.logging.ISupportLogging
{
   /** Context-local data. */
   private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
   {
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   private object<? extends com.goldencode.p2j.oo.logging.IlogWriter> logger = TypeFactory.object(com.goldencode.p2j.oo.logging.IlogWriter.class);

   public static void clear()
   {
      WorkArea wa = local.get();
      wa.basepath = null;
      wa.target = null;
      wa.paths = null;
      wa.request = null;
      wa.response = null;
   }
   
   public static void initialize(String   basepath,
                                 String   target, 
                                 String[] paths, 
                                 HttpServletRequest request, 
                                 HttpServletResponse response)
   {
      WorkArea wa = local.get();
      wa.basepath = basepath;
      wa.target = target;
      wa.paths = paths;
      wa.request = request;
      wa.response = response;
   }
   
   @LegacySignature(type = Type.EXECUTE)
   public void __web_WebHandler_execute__()
   {
      onBlockLevel(Condition.ERROR, Action.THROW);
   }

   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Logging.ILogWriter", type = Type.GETTER, name = "Logger")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends com.goldencode.p2j.oo.logging.IlogWriter> getLogger()
   {
      return function(this, "Logger", object.class, new Block((Body) () -> 
      {
         returnNormal(logger);
      }));
   }

   /**
    * Set logger.
    * 
    * @param    _logger
    *           The logger instance.
    */
   @LegacySignature(type = Type.SETTER, name = "Logger", parameters = 
   {
      @LegacyParameter(name = "logger", type = "OBJECT", mode = "INPUT", qualified = "OpenEdge.Logging.ILogWriter")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   @Override
   public void setLogger(object<? extends IlogWriter> _logger)
   {
      object<? extends IlogWriter> logger = TypeFactory.initInput(_logger);
      
      internalProcedure(this, "Logger", new Block((Body) () ->
      {
         this.logger.assign(logger);
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __web_WebHandler_constructor__()
   {
      internalProcedure(this, "__web_WebHandler_constructor__", new Block((Body) () -> 
      {
         __lang_BaseObject_constructor__();
         
         // TODO: use loggerbuilder to get the correct logger
         logger.assign(ObjectOps.newInstance(VoidLogger.class));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleDelete", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handleDelete(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandleDelete", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleException", parameters = 
   {
      @LegacyParameter(name = "err", type = "OBJECT", qualified = "progress.lang.error", mode = "INPUT"),
      @LegacyParameter(name = "req", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handleException(final object<? extends com.goldencode.p2j.oo.lang.LegacyError> _err, final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _req)
   {
      object<? extends com.goldencode.p2j.oo.lang.LegacyError> err = TypeFactory.initInput(_err);
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> req = TypeFactory.initInput(_req);
      
      return function(this, "HandleException", integer.class, new Block((Body) () -> {
         int64 code = StatusCodeEnum.internalServerError.ref().getValue();
         
         if (err._isValid()) {
            logger.ref().error(TextOps.substitute("Request error for &1 &2: status code &3", 
                     req.ref().getMethod(), 
                     req.ref().getUri().ref().getPath(), code), 
                     err);
         }
         
         returnNormal(code);
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleGet", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handleGet(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandleGet", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleHead", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handleHead(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandleHead", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleNotAllowedMethod", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected abstract integer handleNotAllowedMethod(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1);

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleNotImplemented", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected abstract integer handleNotImplemented(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1);

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleOptions", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handleOptions(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandleOptions", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandlePatch", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handlePatch(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandlePatch", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandlePost", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handlePost(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandlePost", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandlePut", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected integer handlePut(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandlePut", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleRequest")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public integer handleRequest()
   {
      object<? extends IwebRequest> oreq = TypeFactory.object(IwebRequest.class);
      integer ret = TypeFactory.integer();

      return function(this, "HandleRequest", integer.class, new Block((Init) () -> {
         catchError(com.goldencode.p2j.oo.lang.LegacyError.class, oErr -> {
            returnNormal(handleException(oErr, oreq));
         });
      }, (Body) () -> {
         oreq.assign(ObjectOps.newInstance(RemoteWebRequest.class));

         long method = MethodEnum.getEnum(oreq.ref().getMethod()).ref()._getValue();

         if (method == MethodEnum.get.ref()._getValue())
         {
            ret.assign(handleGet(oreq));
         }
         else if (method == MethodEnum.post.ref()._getValue())
         {
            ret.assign(handlePost(oreq));
         }
         else if (method == MethodEnum.put.ref()._getValue())
         {
            ret.assign(handlePut(oreq));
         }
         else if (method == MethodEnum.patch.ref()._getValue())
         {
            ret.assign(handlePatch(oreq));
         }
         else if (method == MethodEnum.delete.ref()._getValue())
         {
            ret.assign(handleDelete(oreq));
         }
         else if (method == MethodEnum.head.ref()._getValue())
         {
            ret.assign(handleHead(oreq));
         }
         else if (method == MethodEnum.options.ref()._getValue())
         {
            ret.assign(handleOptions(oreq));
         }
         else if (method == MethodEnum.trace.ref()._getValue())
         {
            ret.assign(handleTrace(oreq));
         }
         else
         {
            ret.assign(handleNotImplemented(oreq));
         }
         
         returnNormal(ret);
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "HandleTrace", parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
   protected integer handleTrace(final object<? extends com.goldencode.p2j.oo.web.IwebRequest> _p1)
   {
      object<? extends com.goldencode.p2j.oo.web.IwebRequest> p1 = TypeFactory.initInput(_p1);
      
      return function(this, "HandleTrace", integer.class, new Block((Body) () -> {
         returnNormal(handleNotAllowedMethod(p1));
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "LogMessage", parameters = 
   {
      @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT"),
      @LegacyParameter(name = "p2", type = "INTEGER", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void logMessage(final character _p1, final integer _p2)
   {
      character p1 = TypeFactory.initInput(_p1);
      integer p2 = TypeFactory.initInput(_p2);
      
      internalProcedure(this, "LogMessage", new Block((Body) () -> {
         int level = p2.isUnknown() ? 0 : p2.intValue();
         
         if (level == LogLevelEnum.fatal.ref()._getValue())
            logger.ref().fatal(p1);
         else if (level == LogLevelEnum.error.ref()._getValue())
            logger.ref().error(p1);
         else if (level == LogLevelEnum.info.ref()._getValue())
            logger.ref().info(p1);
         else if (level == LogLevelEnum.debug.ref()._getValue())
            logger.ref().debug(p1);
         else if (level == LogLevelEnum.trace.ref()._getValue())
            logger.ref().trace(p1);
         else
            logger.ref().warn(p1);
      }));
   }
   
   @LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "CanAcceptContentType", parameters = {
            @LegacyParameter(name = "req", type = "OBJECT", qualified = "openedge.web.iwebrequest", mode = "INPUT"),
            @LegacyParameter(name = "content", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   protected logical canAcceptContentType(object<? extends IwebRequest> _req, character _content)
   {
      object<? extends IwebRequest> req = TypeFactory.initInput(_req);
      character content = TypeFactory.initInput(_content);

      return function(this, "CanAcceptContentType", logical.class, new Block((Body) () -> {
         Assert.notNull(req, new character("Request"));
         Assert.notNullOrEmpty(content, new character("Content type"));

         // accept everything???
         if (!req.ref().hasHeader(new character("Accept")).booleanValue())
            returnNormal(true);

         character[] contents = splitMimeType(content.getValue());
         String[] accepts = req.ref().getHeader(new character("Accept")).ref().getValue()
                  .getValue().split(",");

         for (String accept : accepts)
         {
            character[] mime = splitMimeType(accept);

            if ("*".equals(mime[0].getValue()) && "*".equals(mime[1].getValue()))
               returnNormal(true);

            if (contents[0].getValue().equalsIgnoreCase(mime[0].getValue())
                     && ("*".equals(mime[1].getValue())
                              || contents[1].getValue().equalsIgnoreCase(mime[1].getValue())))
               returnNormal(true);
         }

         returnNormal(false);
      }));
   }
   
   private character[] splitMimeType (String mimeType) {
      character[] ret = TypeFactory.characterExtent(2);
      
      // strip quality
      mimeType = mimeType.split(";")[0].trim();
               
      ret[0].assign(TextOps.entry(1, mimeType, "/"));
      ret[1].assign(TextOps.entry(2, mimeType, "/"));
      
      return ret;
   }
   
   protected static HttpServletResponse getHttpResponse()
   {
      return local.get().response;
   }
   
   protected static WebContext getWebContext()
   {
      return new WebContext(local.get());
   }
   
   public static class WebContext
   {
      private HttpServletRequest request;
      
      private String[] paths;
      
      private String target;
      
      private String basepath;

      private WebContext(WorkArea context)
      {
         this.request = context.request;
         this.paths = context.paths;
         this.target = context.target;
         this.basepath = context.basepath;
      }

      public HttpServletRequest getRequest()
      {
         return request;
      }

      public String[] getPaths()
      {
         return paths;
      }

      public String getTarget()
      {
         return target;
      }

      public String getBasepath()
      {
         return basepath;
      }
   }
   
   private static class WorkArea
   {
      private HttpServletResponse response;
      
      private HttpServletRequest request;
      
      private String[] paths;
      
      private String target;
      
      private String basepath;
   }
}