WebResponseWriter.java

/*
** Module   : WebResponseWriter.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 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
**                  signature.
** 004 ME  20210413 Implementation as per OE12.2 - webStream property not implemented.
**     ME  20210916 Added method to check if in 'webspeed' environment. 
**     CA  20220120 Do not used TypeFactory.object when the OO reference must not be tracked or registered.
**                  Do not use TypeFactory.object for internal usages, use ObjectVar if the reference must 
**                  be tracked.
**                  All TypeFactory.object variable definitions must be done outside of the top-level block.
**     TJD 20220331 Suppress warnings in writeStatusLine 
**     CA  20220923 Variable definitions (including associated with parameters) must be done always outside of
**                  the BlockManager API.
**     OM  20221220 Replaced array container with FinalParameterContainer.
** 005 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.*;
import com.goldencode.p2j.oo.net.http.Cookie;
import com.goldencode.p2j.oo.net.http.HttpHeader;
import com.goldencode.p2j.oo.net.http.HttpHeaderBuilder;
import com.goldencode.p2j.oo.net.http.TransferEncodingEnum;
import com.goldencode.p2j.oo.net.http.filter.payload.MessageWriter;
import com.goldencode.p2j.oo.net.http.filter.writer.BodyWriterBuilder;

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

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.http.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in OpenEdge/Web/WebResponseWriter.cls).
 */
@LegacyResource(resource = "OpenEdge.Web.WebResponseWriter")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_PARTIAL)
public class WebResponseWriter
extends com.goldencode.p2j.oo.io.OutputStream
{
   private object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> response = TypeFactory.object(com.goldencode.p2j.oo.net.http.IhttpResponse.class);

   private handle webStream = TypeFactory.handle();
   
   private boolean isOpen;
   private boolean preambleWriten;
   private boolean bodyWriten;

   private HttpServletResponse httpResponse;
   
   @LegacySignature(type = Type.EXECUTE)
   public void __web_WebResponseWriter_execute__()
   {
      onBlockLevel(Condition.ERROR, Action.THROW);
   }


   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Net.HTTP.IHttpMessage", type = Type.GETTER, name = "Message")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends com.goldencode.p2j.oo.net.http.IhttpMessage> getMessage()
   {
      return function(this, "Message", object.class, new Block((Body) () -> 
      {
         returnNormal(response);
      }));
   }

   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Net.HTTP.IHttpResponse", type = Type.GETTER, name = "Response")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> getResponse()
   {
      return function(this, "Response", object.class, new Block((Body) () -> 
      {
         returnNormal(response);
      }));
   }
   
   @LegacySignature(type = Type.SETTER, name = "Response", parameters = {
            @LegacyParameter(qualified = "OpenEdge.Net.HTTP.IHttpResponse", name = "var", type = "OBJECT", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void setResponse(
            final object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> _var)
   {
      object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> var = TypeFactory
               .initInput(_var);

      internalProcedure(WebResponseWriter.class, this, "Response", new Block((Body) () -> {
         Assert.notNull(var, new character("Http Response"));
         
         response.assign(var);
         open();
      }));
   }

   @LegacySignature(returns = "HANDLE", type = Type.GETTER, name = "WebStream")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
   public handle getWebStream()
   {
      return function(this, "WebStream", handle.class, new Block((Body) () -> 
      {
         returnNormal(webStream);
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __web_WebResponseWriter_constructor__()
   {
      internalProcedure(this, "__web_WebResponseWriter_constructor__", new Block((Body) () -> 
      {
         __io_OutputStream_constructor__();
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = 
   {
      @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.net.http.ihttpresponse", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void __web_WebResponseWriter_constructor__(final object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> _p1)
   {
      object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> p1 = TypeFactory
               .initInput(_p1);
      
      internalProcedure(this, "__web_WebResponseWriter_constructor__", new Block((Body) () -> 
      {
         __web_WebResponseWriter_constructor__();
         setResponse(p1);
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "Close")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void close()
   {
      internalProcedure(this, "Close", new Block((Body) () ->
      {
         flush();
         // TODO: close the webStream
         httpResponse = null;
         isOpen = false;
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "Flush")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void flush()
   {
      internalProcedure(this, "Flush", new Block((Body) () ->
      {
         writeHttpPreamble();
         writeBody();
         
         try
         {
            httpResponse.getOutputStream().flush();
         }
         catch (Exception e)
         {
         }
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "Open")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public void open()
   {
      internalProcedure(this, "Open", new Block((Body) () ->
      {
         // TODO: set the webStream handle
         httpResponse = WebHandler.getHttpResponse();
         preambleWriten = false;
         bodyWriten = false;
         isOpen = true;
      }));
   }

   @LegacySignature(returns = "INT64", type = Type.METHOD, name = "Write", parameters = 
   {
      @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public int64 write(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);
      
      return function(this, "Write", int64.class, new Block((Body) () -> 
      {
         bodyWriten = true;
         flush();
         
         if (isWebStreamSet()) {
            try
            {
               httpResponse.getOutputStream().print(p1.toStringMessage());
            }
            catch (Exception e)
            {
               // TODO error handling
            }
         }
         
         returnNormal(TextOps.length(p1));
      }));
   }

   @LegacySignature(returns = "INT64", type = Type.METHOD, name = "Write", parameters = 
   {
      @LegacyParameter(name = "p1", type = "LONGCHAR", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public int64 write(final longchar _p1)
   {
      longchar p1 = TypeFactory.initInput(_p1);
      
      return function(this, "Write", int64.class, new Block((Body) () -> 
      {
         bodyWriten = true;
         flush();
         
         if (isWebStreamSet()) {
            try
            {
               httpResponse.getOutputStream().print(p1.toStringMessage());
            }
            catch (Exception e)
            {
               // TODO error handling
            }
         }
         
         returnNormal(TextOps.length(p1));
      }));
   }

   @LegacySignature(returns = "INT64", type = Type.METHOD, name = "Write", parameters = 
   {
      @LegacyParameter(name = "data", type = "MEMPTR", mode = "INPUT"),
      @LegacyParameter(name = "offset", type = "INT64", mode = "INPUT"),
      @LegacyParameter(name = "len", type = "INT64", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public int64 write(final memptr _data, final int64 _offset, final int64 _len)
   {
      memptr data = TypeFactory.initInput(_data);
      int64 offset = TypeFactory.initInput(_offset);
      int64 len = TypeFactory.initInput(_len);
      
      return function(this, "Write", int64.class, new Block((Body) () ->
      {
         bodyWriten = true;
         flush();
         
         int dataLen = data.length().intValue();
         int from = !offset.isUnknown() && offset.intValue() > 0 ? offset.intValue() - 1: -1;
         int to = from >= 0 && !len.isUnknown() && len.intValue() > 0 ? from + len.intValue() : 0;
         
         if (dataLen > 0 && from >= 0 
                  && to > 0 
                  && to <= dataLen) {
            
            if (isWebStreamSet()) {
               byte[] bytes = data.getByteArray();
               try
               {
                  if (from > 0 || to != dataLen) 
                  {
                     httpResponse.getOutputStream().write(Arrays.copyOfRange(bytes, from, to));
                  }
                  else 
                  {
                     httpResponse.getOutputStream().write(bytes);
                  }
               } catch (Exception ex) 
               {
                  // TODO: error handling
                  returnNormal(0);
               }
            }
            
            returnNormal(len);
         }
         
         returnNormal(0);
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "WriteBody")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   protected void writeBody()
   {
      object<? extends MessageWriter> writer = TypeFactory.object(MessageWriter.class);

      internalProcedure(this, "WriteBody", new Block((Body) () -> {
         if (!bodyWriten && response._isValid() && response.ref().getEntity()._isValid())
         {
            bodyWriten = true;

            writer.assign(BodyWriterBuilder.build_1(response).ref().getWriter());

            if (writer._isValid())
            {
               writer.ref().open();
               writer.ref().write(response.ref().getEntity());
               writer.ref().close();

               memptr data = ObjectOps.cast(writer.ref().getEntity(), ByteBucket.class).ref()
                        .getBytes().ref().getValue();
               
               try {
                  // try to set content-length header before preamble
                  response.ref().setContentLength(new integer(data.length()));
                  writeHttpPreamble();
                  
                  write(data, new int64(1), data.length());
               } finally {
                  data.setLength(0);
               }

            }
         }
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "WriteCookies")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   protected void writeCookies()
   {
      object[][] cookies = { TypeFactory.objectExtent(Cookie.class, false) };
      internalProcedure(this, "WriteCookies", new Block((Body) () -> {
         if (response._isValid() && isWebStreamSet())
         {
            int nh = response.ref()
                     .getCookies(new OutputExtentParameter<object<? extends Cookie>>()
                     {
                        @Override
                        public object<? extends Cookie>[] getVariable()
                        {
                           return cookies[0];
                        }

                        @Override
                        public void setVariable(object<? extends Cookie>[] reference)
                        {
                           cookies[0] = reference;
                        }
                     }).intValue();

            for (int i = 0; i < nh; i++)
            {
               object<? extends Cookie> cookie = cookies[0][i];
               javax.servlet.http.Cookie httpCookie = new javax.servlet.http.Cookie(
                        cookie.ref().getName().getValue(), cookie.ref().getValue().getValue());

               httpCookie.setDomain(cookie.ref().getDomain().getValue());
               httpCookie.setPath(cookie.ref().getPath().getValue());
               
               if (!cookie.ref().getMaxAge().isUnknown())
                  httpCookie.setMaxAge(cookie.ref().getMaxAge().intValue());
               
               if (cookie.ref().getSecure().booleanValue())
                  httpCookie.setSecure(true);
               if (cookie.ref().getHttpOnly().booleanValue())
                  httpCookie.setHttpOnly(true);
               
               httpResponse.addCookie(httpCookie);
            }
         }
      }));
   }
   
   @LegacySignature(type = Type.METHOD, name = "WriteHeaders")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void writeHeaders()
   {
      FinalParameterContainer<object<? extends HttpHeader>[]> headers = 
            new FinalParameterContainer<>(TypeFactory.objectExtent(HttpHeader.class, false));
      
      internalProcedure(this, "WriteHeaders", new Block((Body) () -> 
      {
         if (response._isValid() && isWebStreamSet()) {
            
            int nh = response.ref().getHeaders(
               new OutputExtentParameter<object<? extends HttpHeader>>()
               {
                  @Override
                  public object<? extends HttpHeader>[] getVariable()
                  {
                     return headers.getValue();
                  }
                  
                  @Override
                  public void setVariable(object<? extends HttpHeader>[] reference)
                  {
                     headers.setValue(reference);
                  }
               }).intValue();
               
            for (int i = 0; i < nh; i++)
            {
               if (i < headers.getValue().length)
               {
                  writeHeader(headers.getValue()[i]);
               }
               else
               {
                  System.out.println("Missing header! " + i + " >= " + headers.getValue().length + "!");
               }
            }
         }
      }));
   }
   
   private void writeHeader(object<? extends HttpHeader> hdr)
   {
      if (hdr.ref() == null)
      {
         System.out.println("Something went very bad: the header object was DELETED before being used.");
         return;
      }
      
      if (isWebStreamSet())
      {
         httpResponse.setHeader(hdr.ref().getName().getValue(),
                                hdr.ref().getBaseValue().getValue() + hdr.ref().getParameterValues().getValue());
      }
   }
   
   @LegacySignature(type = Type.METHOD, name = "WriteHttpPreamble")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void writeHttpPreamble()
   {
      internalProcedure(this, "WriteHttpPreamble", new Block((Body) () -> 
      {
         if (preambleWriten)
            return;
         
         preambleWriten = true;
         
         if (!CompareOps._isEqual(response.ref().getTransferEncoding(), character.valueOf(TransferEncodingEnum.none)))
            writeHeader(HttpHeaderBuilder.build(new character("Transfer-Encoding")).ref().value(response.ref().getTransferEncoding()).ref().getHeader());
         
         writeStatusLine();
         
         // this should cover content-type, character encoding as well 
         writeHeaders();
         writeCookies();
         
      }));
   }

   @SuppressWarnings("deprecation")
   @LegacySignature(type = Type.METHOD, name = "WriteStatusLine")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   protected void writeStatusLine()
   {
      internalProcedure(this, "WriteStatusLine", new Block((Body) () -> 
      {
         if (response._isValid() && isWebStreamSet()) 
         {
            if (response.ref().getStatusReason().isUnknown())
            {
               httpResponse.setStatus(response.ref().getStatusCode().intValue());
            }
            else 
            {
               httpResponse.setStatus(response.ref().getStatusCode().intValue(), 
                        response.ref().getStatusReason().toStringMessage());
            }
         }
      }));
   }
   
   private boolean isWebStreamSet () {
      // in 4GL trying to write to a closed stream raise error, 
      // however the "WEB" stream can be open in a non web environment and no error is thrown
      if (!isOpen) {
         ErrorManager.recordOrThrowError(1387, "Attempt to write to closed stream WebStream.", true, true);
      }
      
      return httpResponse != null;
   }

}