# CookieJar.java

    /*
    ** Module   : CookieJar.java
    ** Abstract : Implementation of the builtin class.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- -------------------------------Description--------------------------------
    ** 001 IAS 20190923 First version.
    ** 002 ME  20200410 Implement ILogger interface fully.
    ** 003 MP  20200611 Complete class with stubs taken by converting the skeleton using FWD.
    ** 004 ME  20201028 Implement all methods as per OE12.2.
    **     CA  20210221 Fixed parameters at the LegacySignature annotation.
    ** 005 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
    **                  signature.
    **     CA  20210609 Updated INPUT/INPUT-OUTPUT parameters to the new approach, where they are explicitly
    **                  initialized at the method's execution, and not at the caller's arguments.
    **     ME  20211012 Increment/decrement object references on add/remove.
    ** 006 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.net.http;

    import com.goldencode.p2j.oo.core.*;
    import com.goldencode.p2j.oo.json.objectmodel.JsonArray;
    import com.goldencode.p2j.oo.json.objectmodel.JsonObject;
    import com.goldencode.p2j.oo.json.objectmodel.ObjectModelParser;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.oo.logging.*;
    import com.goldencode.p2j.security.ContextLocal;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.BlockManager.Action;
    import com.goldencode.p2j.util.BlockManager.Condition;
    import com.goldencode.p2j.util.InternalEntry.*;

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

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Optional;

    /**
     *
     * Stores cookies temporarily and persistently.
     *
     */
    @LegacyResource(resource = "OpenEdge.Net.HTTP.CookieJar")
    @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
    public class CookieJar extends BaseObject
             implements ICookieJar, ISupportInitialize, ISupportLogging
    {

       private class CookieJarEntry
       {
          final object<? extends Cookie> cookie;
          final String domain;
          final String path;
          final String name;

          final datetimetz created = TypeFactory.datetimetz();
          final datetimetz lastAccess = TypeFactory.datetimetz();

          CookieJarEntry(object<? extends Cookie> cookie, String domain, String path)
          {
             this(cookie, domain, path, datetimetz.now(), TypeFactory.datetimetz());
          }

          CookieJarEntry(object<? extends Cookie> cookie, String domain, String path, datetimetz created, datetimetz lastAccess)
          {
             this.cookie = cookie;
             this.name = cookie.ref().getName().getValue();
             this.domain = domain;
             this.path = TextOps.isEmpty(path) ? "/" : path;

             this.created.assign(created);
             this.lastAccess.assign(lastAccess);
          }
       }

       private final static String TAG_COOKIES = "cookies";
       private final static String TAG_COOKIE_NAME = "CookieName";
       private final static String TAG_COOKIE_DOMAIN = "Domain";
       private final static String TAG_COOKIE_PATH = "Path";
       private final static String TAG_COOKIE_DATA = "Cookie";
       private final static String TAG_COOKIE_CREATED = "CreatedAt";
       private final static String TAG_COOKIE_ACCESS = "LastAccessedAt";

       @LegacySignature(type = Type.PROPERTY, name = "Logger")
       private final object<? extends IlogWriter> logger = TypeFactory.object(IlogWriter.class);

       @LegacySignature(type = Type.PROPERTY, name = "CookieJarPath")
       private character cookieJarPath = TypeFactory.character();

       // persistent are stored as static for client session
       private static ContextLocal<List<CookieJarEntry>> persistentCookies = new ContextLocal<List<CookieJarEntry>>()
       {
          protected List<CookieJarEntry> initialValue()
          {
             return new ArrayList<CookieJarEntry>();
          }
       };

       // session cookies are stored only for the jar lifetime
       private List<CookieJarEntry> sessionCookies = new ArrayList<CookieJarEntry>();

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

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

       /**
        * Destroy
        */
       @LegacySignature(type = Type.METHOD, name = "Destroy")
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       @Override
       public void destroy()
       {
          internalProcedure(CookieJar.class, this, "Destroy", new Block((Body) () -> {
             _persistCookies();
          }));
       }

       /**
        * Initialize
        */
       @LegacySignature(type = Type.METHOD, name = "Initialize")
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       @Override
       public void initialize()
       {
          internalProcedure(CookieJar.class, this, "Initialize", new Block((Body) () -> {
             // load persisted cookies, no-op if file not found
             character jarStore = FileSystemOps.searchPath(cookieJarPath);

             if (jarStore.isUnknown())
                return;

             object<ObjectModelParser> parser = ObjectOps.newInstance(ObjectModelParser.class);
             object<JsonObject> data = ObjectOps.cast(parser.ref().parseFile(jarStore), JsonObject.class);

             if (data.ref().has(new character(TAG_COOKIES)).booleanValue()) {
                object<? extends JsonArray> cookies = data.ref().getJsonArray(new character(TAG_COOKIES));

                for (int i = 0; i < cookies.ref().getLength().intValue(); i++)
                {
                   object<? extends JsonObject> cookie = cookies.ref().getJsonObject(new integer(i + 1));
                   CookieJarEntry entry = new CookieJarEntry(Cookie.parse(cookie.ref().getCharacter(new character(TAG_COOKIE_DATA))),
                            cookie.ref().getCharacter(new character(TAG_COOKIE_DOMAIN)).getValue(),
                            cookie.ref().getCharacter(new character(TAG_COOKIE_PATH)).getValue(),
                            cookie.ref().getDatetimeTz(new character(TAG_COOKIE_CREATED)),
                            cookie.ref().getDatetimeTz(new character(TAG_COOKIE_ACCESS)));

                   _addCookie(entry, persistentCookies.get());
                }
             }

          }));
       }

       /**
        * 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(returns = "CHARACTER", type = Type.GETTER, name = "CookieJarPath")
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public character getCookieJarPath()
       {
          return function(CookieJar.class, this, "CookieJarPath", character.class,
                   new Block((Body) () -> {
                      returnNormal(cookieJarPath);
                   }));
       }

       @LegacySignature(type = Type.SETTER, name = "CookieJarPath", parameters = {
                @LegacyParameter(name = "var", type = "CHARACTER", mode = "INPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void setCookieJarPath(final character _var)
       {
          character var = TypeFactory.initInput(_var);

          internalProcedure(CookieJar.class, this, "CookieJarPath", new Block((Body) () -> {
             cookieJarPath.assign(var);
          }));
       }

       @LegacySignature(type = Type.CONSTRUCTOR)
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void __net_http_CookieJar_constructor__()
       {
          internalProcedure(CookieJar.class, this, "__net_http_CookieJar_constructor__",
                   new Block((Body) () -> {
                      __lang_BaseObject_constructor__();
                      cookieJarPath.assign(TextOps.substitute("&1cookies.json",
                               EnvironmentOps.getTempDirectory()));
                      logger.assign(ObjectOps.newInstance(VoidLogger.class));
                   }));
       }

       @LegacySignature(type = Type.DESTRUCTOR)
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void __net_http_CookieJar_destructor__()
       {
          internalProcedure(CookieJar.class, this, "__net_http_CookieJar_destructor__",
                   new Block((Body) () -> {
                      this.destroy();
                   }));
       }

       @LegacySignature(type = Type.METHOD, name = "AddCookie", parameters = {
                @LegacyParameter(name = "pcDomain", type = "CHARACTER", mode = "INPUT"),
                @LegacyParameter(name = "pcPath", type = "CHARACTER", mode = "INPUT"),
                @LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void addCookie(final character _pcDomain, final character _pcPath,
                final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
       {
          character pcDomain = TypeFactory.initInput(_pcDomain);
          character pcPath = TypeFactory.initInput(_pcPath);
          object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
                   .initInput(_poCookie);

          internalProcedure(CookieJar.class, this, "AddCookie", new Block((Body) () -> {
             Assert.notNull(poCookie, new character("Cookie"));

             // no error thrown if domain doesn't match
             if (pcDomain.isUnknown()
                      || pcDomain.getValue().endsWith(poCookie.ref().getDomain().getValue()))
             {
                Assert.notNullOrEmpty(pcDomain, new character("Cookie Domain"));
                Assert.notNull(pcPath, new character("Cookie Path"));

                CookieJarEntry entry = new CookieJarEntry(poCookie, pcDomain.getValue(),
                         pcPath.getValue());

                // no expiration set, session cookie
                if (poCookie.ref().getExpiresAt().isUnknown())
                {
                   _addCookie(entry, sessionCookies);
                }
                else
                {
                   // don't save it if expired already
                   if (!_isExpired(poCookie.ref()))
                      _addCookie(entry, persistentCookies.get());
                }
             }
             else if (logger._isValid())
             {
                logger.ref().debug(TextOps.substitute("Bad domain &1 for &2", pcDomain, poCookie));
             }
          }));
       }

       @LegacySignature(type = Type.METHOD, name = "AddCookie", parameters = {
                @LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void addCookie(final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
       {
          object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
                   .initInput(_poCookie);

          internalProcedure(CookieJar.class, this, "AddCookie", new Block((Body) () -> {
             Assert.notNull(poCookie, new character("Cookie"));
             addCookie(poCookie.ref().getDomain(), poCookie.ref().getPath(), poCookie);
          }));
       }

       @LegacySignature(type = Type.METHOD, name = "AddCookies", parameters = {
                @LegacyParameter(name = "poCookies", type = "OBJECT", extent = -1, qualified = "openedge.net.http.cookie", mode = "INPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void addCookies(
                final object<? extends com.goldencode.p2j.oo.net.http.Cookie>[] _poCookies)
       {
          object<? extends com.goldencode.p2j.oo.net.http.Cookie>[] poCookies = TypeFactory
                   .initInput(_poCookies);

          internalProcedure(CookieJar.class, this, "AddCookies", new Block((Body) () -> {
             Assert.notNull(poCookies, new character("Cookies"));

             for (int i = 0; i < poCookies.length; i++)
             {
                addCookie(poCookies[i]);
             }
          }));
       }

       @LegacySignature(type = Type.METHOD, name = "ClearPersistentCookies")
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void clearPersistentCookies()
       {
          internalProcedure(CookieJar.class, this, "ClearPersistentCookies", new Block((Body) () -> {
             persistentCookies.get().clear();
             _persistCookies();

             if (logger._isValid())
                logger.ref().debug(TextOps.substitute("Cleared persistent cookies: &1", datetimetz.now()));
          }));
       }

       @LegacySignature(type = Type.METHOD, name = "ClearSessionCookies")
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public void clearSessionCookies()
       {
          internalProcedure(CookieJar.class, this, "ClearSessionCookies", new Block((Body) () -> {
             sessionCookies.clear();
             if (logger._isValid())
                logger.ref().debug(TextOps.substitute("Cleared session cookies: &1", datetimetz.now()));
          }));
       }

       @LegacySignature(type = Type.CONSTRUCTOR)
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public static void __net_http_CookieJar_constructor__static__()
       {
          externalProcedure(CookieJar.class, new Block((Body) () -> {
             onBlockLevel(Condition.ERROR, Action.THROW);
          }));
       }

       @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "GetCookies", parameters = {
                @LegacyParameter(name = "poUri", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT"),
                @LegacyParameter(name = "poCookies", type = "OBJECT", extent = -1, qualified = "openedge.net.http.cookie", mode = "OUTPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public integer getCookies(final object<? extends com.goldencode.p2j.oo.net.Uri> _poUri,
                final OutputExtentParameter<object<? extends com.goldencode.p2j.oo.net.http.Cookie>> _extpoCookies)
       {
          object<? extends com.goldencode.p2j.oo.net.Uri> poUri = TypeFactory.initInput(_poUri);
          object[][] extpoCookies = { TypeFactory.initOutput(_extpoCookies) };

          return function(CookieJar.class, this, "GetCookies", integer.class, new Block((Body) () -> {
             String domain = poUri.ref().getHost().getValue().toLowerCase();
             String path = poUri.ref().getPath().getValue().toLowerCase();
             String scheme = poUri.ref().getScheme().getValue();

             boolean secure = "https".equalsIgnoreCase(scheme);
             boolean http = secure || "http".equalsIgnoreCase(scheme);

             ArrayList<object<? extends Cookie>> all = new ArrayList<object<? extends Cookie>>();

             // persistent first
             all.addAll(_getCookies(persistentCookies.get(), domain, path, http, secure));
             all.addAll(_getCookies(sessionCookies, domain, path, http, secure));

             if (all.size() > 0) {
                extpoCookies[0] = ArrayAssigner.resize(extpoCookies[0], all.size());

                for (int i = 0; i < all.size(); i++)
                {
                   extpoCookies[0][i].assign(all.get(i));
                }
             }
             returnNormal(new integer(extpoCookies[0].length));
          }));
       }

       @LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "RemoveCookie", parameters = {
                @LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
       @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
       public logical removeCookie(
                final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
       {
          object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
                   .initInput(_poCookie);

          return function(CookieJar.class, this, "RemoveCookie", logical.class,
                   new Block((Body) () -> {
                      Assert.notNull(poCookie, new character("Cookie"));

                      returnNormal(new logical(_removeCookie(poCookie.ref(), sessionCookies)
                               || _removeCookie(poCookie.ref(), persistentCookies.get())));
                   }));
       }

       private boolean _removeCookie(Cookie cookie,
                List<CookieJarEntry> store)
       {

          Optional<CookieJarEntry> first = store.stream()
                   .filter(e -> e.domain.equalsIgnoreCase(cookie.getDomain().getValue())
                            && e.path.equalsIgnoreCase(cookie.getPath().getValue())
                            && e.name.equalsIgnoreCase(cookie.getName().getValue()))
                   .findFirst();

          if (first.isPresent()) {
             CookieJarEntry entry = first.get();
             if (store.remove(entry)) {
                if (entry.cookie._isValid())
                   ObjectOps.decrement(entry.cookie.ref());
                return true;
             }
          }

          return false;
       }

       private void _addCookie(CookieJarEntry cookie, List<CookieJarEntry> store)
       {
          Optional<CookieJarEntry> find = store.stream()
                   .filter(e -> e.domain.equalsIgnoreCase(cookie.domain)
                            && e.path.equalsIgnoreCase(cookie.path)
                            && e.name.equalsIgnoreCase(cookie.name))
                   .findFirst();

          if (find.isPresent())
          {
             // already have a cookie with same name for domain/path pair
             CookieJarEntry entry = find.get();
             if (entry.cookie._isValid())
                ObjectOps.decrement(entry.cookie.ref());

             entry.cookie.assign(cookie.cookie);
          }
          else
          {
             store.add(cookie);
          }

          ObjectOps.increment(cookie.cookie.ref());
       }

       private boolean _isExpired(Cookie cookie)
       {

          return !cookie.getExpiresAt().isUnknown()
                   && DateOps.compare(cookie.getExpiresAt(), datetimetz.now()) < 0;
       }

       private object<JsonObject> _serializeCookie(CookieJarEntry poCookie)
       {
          object<JsonObject> jsonObj = ObjectOps.newInstance(JsonObject.class);

          Cookie cookie = poCookie.cookie.ref();

          jsonObj.ref().add(new character(TAG_COOKIE_NAME), new character(poCookie.name));
          jsonObj.ref().add(new character(TAG_COOKIE_DOMAIN), new character(poCookie.domain));
          jsonObj.ref().add(new character(TAG_COOKIE_PATH), new character(poCookie.path));
          jsonObj.ref().add(new character(TAG_COOKIE_DATA), cookie.toLegacyString());
          jsonObj.ref().add(new character(TAG_COOKIE_CREATED), poCookie.created);
          jsonObj.ref().add(new character(TAG_COOKIE_ACCESS), poCookie.lastAccess);

          return jsonObj;
       }

       private Collection<object<? extends Cookie>> _getCookies(List<CookieJarEntry> store,
                String domain, String path, boolean http, boolean secure)
       {

          ArrayList<object<? extends Cookie>> cookies = new ArrayList<object<? extends Cookie>>();

          for (Iterator iterator = store.iterator(); iterator.hasNext();)
          {
             CookieJarEntry cookieJarEntry = (CookieJarEntry) iterator.next();

             // match domain and path
             if (path.startsWith(cookieJarEntry.path.toLowerCase())
                      && (cookieJarEntry.domain.equalsIgnoreCase(domain)
                               || (cookieJarEntry.domain.indexOf('.') == 0
                                        && domain.endsWith(cookieJarEntry.domain.toLowerCase()))))
             {
                Cookie cookie = cookieJarEntry.cookie.ref();
                // check expiration
                if (_isExpired(cookie))
                {
                   // already expired, no point in keeping it around
                   iterator.remove();
                }
                // check if http only/secure is set on cookie
                else if ((!cookie.getHttpOnly().booleanValue() || http)
                         && (!cookie.getSecure().booleanValue() || secure))
                {
                   cookies.add(cookieJarEntry.cookie);
                }
             }
          }

          return cookies;
       }

       private void _persistCookies () {
          final object<JsonObject> jsonObj = ObjectOps.newInstance(JsonObject.class);
          final object<JsonArray> jsonArr = ObjectOps.newInstance(JsonArray.class);

          persistentCookies.get().stream()
                   .filter(c -> c.cookie._isValid() && !_isExpired(c.cookie.ref()))
                   .forEach(c -> jsonArr.ref().add_2(_serializeCookie(c)));

          jsonObj.ref().add(new character(TAG_COOKIES), jsonArr);

          jsonObj.ref().writeFile(cookieJarPath);
       }
    }