WebClientBuilderOptions.java

/*
** Module   : WebClientBuilderOptions.java
** Abstract : store web client parameters read from the directory
**
** Copyright (c) 2014-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA  20140206 First version, extracted from ClientBuilderOptions.
** 002 MAG 20140711 Enable/Disable clipboard.
** 003 GES 20150310 Made this class generic for web clients, not just for ChUI web clients.
** 004 GES 20150717 Added some GUI configuration values.
** 005 GES 20160202 Added trusted mode support and removed some dead code.
**     SBI 20160321 Added maxBinaryMessage, maxTextMessage, maxIdleTime websocket's parameters and
**                  utilized "webClient/embedded" to set the web client embedded mode.
** 006 SBI 20160427 Added graphicsCached parameter to turn off the graphics cache for testing.
** 007 SBI 20170628 Fixed misspelled variable to set watchdogTimeout correctly.
** 007 CA  20191211 When spawning a Java process programmatically, allow the caller to pass 
**                  environment options specific to that call (beside any jvmArgs configured in 
**                  the directory).
** 008 HC  20201116 Added renderer web client option. Made WebGL the default renderer.
**     SBI 20210331 Added new taskBarStyle web client option.
**     SBI 20210514 Added delayBetweenTriesToConnect, tryToConnectMessage and serverUnavailableMessage options.
**         20210516 Added pingPongInterval, maxLostPings and delayBetweenPingTries options.
**         20210517 Added connectionRestoredMessage option.
**     HC  20210617 Made the default Web client renderer Canvas 2D non-cached.
**     SBI 20210914 Added enableDebugLogging.
**         20210921 Updated JVM arguments for new client.
**     CA  20210924 Replaced 'sticked' with 'fixed' name, for web client taskbar style.
**     SBI 20210926 Removed code that updates jvm arguments for new client.
**     SBI 20211003 Added http configuration settings for the web client.
**     TJD 20220527 Support for text-metrics cache controlled by server
** 009 SBI 20230411 Added lang option and changed getClientOptions to use getOverriddenOptions()
** 010 GBB 20230825 Cleanup for embedded.
** 011 GBB 20231121 Added broadcastChannelPingTimeout.
** 012 SBI 20231208 Used the common constants.
** 013 GBB 20240709 Implemented interface HttpConfigurationConstants renamed to WebConfigurationConstants,
**                  while WebClientConstants removed. Hard-coded config names replaced by ConfigItem 
**                  constants. tokenConfigsCachedPairs to cache the last 10 options resolved by project 
**                  token. Overridden options to be stored in ThreadLocal and allow reset to replace copying 
**                  the whole object for each spawn request.
** 014 GBB 20240718 Config read implemented in concrete class BaseClientBuilderOptions.
** 015 GBB 20240730 Resolve and store the project token for the web driver.
** 016 SBI 20240828 Added default value WATCH_DOG_TIMEOUT for watch dog timeout instead of -1.
 */
/*
** 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.main;

import java.util.*;

import com.goldencode.p2j.net.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.chui.driver.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.web.*;

/**
 * The parameters stored inside this structure are read from directory on server start-up 
 * and are used as default parameters to create and spawn a web client process.
 * <p>
 * This class MUST NOT BE USED from client side, as it uses a direct access to the server.  Also,
 * this class can be instantiated only from threads with a P2J context for the running P2J server.
 */
public class WebClientBuilderOptions
implements ClientBuilderOptions,
           WebConfigurationConstants
{
   /**
    * Cache for configs (resolved by {@link #readConfigs()}), stored by project token. The values are 
    * unmodifiable maps of {@link ConfigItem#name()} : value.
    */
   private static final Map<String, Map<String, String>> tokenConfigsCachedPairs =
      new LinkedHashMap<String, Map<String, String>>() {
         @Override
         protected boolean removeEldestEntry(Map.Entry eldest)
         {
            return size() > 10;
         }
      };
   
   /** Options storage. Unmodifiable map. */
   private final Map<String, String> dirOptions;

   /** Base options. */
   private final BaseClientBuilderOptions baseOptions;

   /** Storage for overridden options. Different on the different Jetty threads. To be reset. */
   private final ThreadLocal<Map<String, String>> overriddenOptions = ThreadLocal.withInitial(HashMap::new);

   /** The web driver type. Can't be in WebClientBuilderOptions to resolve the parent options. */
   private final WebDriverType driverType;

   /** Project token */
   private final String projectToken;
   
   /**
    * Public constructor. Reads the configuration from the directory and calculates certain runtime values 
    * that will be used by web client launching.
    *
    * @param    driverType
    *           The web driver type: gui, chui, embedded.
    * 
    * @throws   IllegalStateException
    *           If this is called from P2J clients.
    */
   public WebClientBuilderOptions(WebDriverType driverType)
   {
      this.driverType = driverType;
      
      this.projectToken = ConfigItem.PROJECT_TOKEN.read(null, driverType);

      if (SessionManager.get() == null || SessionManager.get().isLeaf())
      {
         throw new IllegalStateException("This class can be used only on the server-side!");
      }
      // TODO: this will not allow changing the options via the AdminConsole - add a way to 
      // invalidate and recreate this instance
      
      this.dirOptions = readConfigs();

      this.baseOptions = new BaseClientBuilderOptions(null)
      {
         /**
          * A convenience method to read the client config with the specified default value.
          *
          * @param   clientConfig
          *          The client config to read.
          * @param   defaultValue
          *          The default value returned if no value found in directory.
          * @param   <T>
          *          The type of the client config.
          *
          * @return  The client config value as read from directory.
          */
         @Override
         <T> T read(ConfigItem<T> clientConfig, T defaultValue)
         {
            return clientConfig.read(defaultValue, driverType);
         }

         /**
          * A convenience method to read the client config with the specified default value.
          *
          * @param   clientConfig
          *          The client config to read.
          * @param   defaultValue
          *          The default value returned if no value found in directory.
          * @param   placeholderValue
          *          The value to replace the placeholder in the directory node name for the config.
          * @param   <T>
          *          The type of the client config.
          *
          * @return  The client config value as read from directory.
          */
         @Override
         <T> T read(ConfigItem<T> clientConfig, T defaultValue, String placeholderValue)
         {
            return clientConfig.read(defaultValue, driverType, placeholderValue);
         }

         /**
          * Reads and returns the value of clientConfig/cfgOverrides.
          *
          * @param   defaultValue
          *          The default value returned if no value found in directory.
          *
          * @return  The cfgOverrides value.
          */
         @Override
         String readCfgOverrides(String defaultValue)
         {
            return ConfigItem.CFG_OVERRIDES.read(defaultValue);
         }
      };
   }

   /**
    * Resets the thread local value of {@link #overriddenOptions}.
    */
   public void reset()
   {
      baseOptions.reset();
      overriddenOptions.set(new HashMap<>());
   }

   /**
    * Returns the {@link #driverType}.
    *
    * @return   See above.
    */
   public WebDriverType getDriverType()
   {
      return driverType;
   }

   /**
    * Returns the {@link #projectToken}.
    *
    * @return   See above.
    */
   public String getProjectToken()
   {
      return projectToken;
   }

   /**
    * Reads the configuration from the directory and calculates certain runtime values that will be used by 
    * web client launching.
    * 
    * @return   Unmodifiable map of options.
    */
   static Map<String, String> readConfigs()
   {
      String projectToken = Utils.getProjectToken();
      if (projectToken != null && tokenConfigsCachedPairs.containsKey(projectToken))
      {
         return tokenConfigsCachedPairs.get(projectToken);
      }

      Map<String, String> options = new HashMap<>();

      // ChUI terminal size
      add(options, ConfigItem.ROWS, TerminalOptions.DEF_ROWS);
      add(options, ConfigItem.COLS, TerminalOptions.DEF_COLUMNS);

      // GUI desktop size in pixels
      add(options, ConfigItem.DESKTOP_HEIGHT, 768);
      add(options, ConfigItem.DESKTOP_WIDTH, 1024);
      // for colors, don't include any alpha on bits 24-31, if emitted as FF the result is not
      // interpreted properly by the browser
      add(options, ConfigItem.BACKGROUND, TerminalOptions.DEF_BACKGROUND.asHexString());
      add(options, ConfigItem.FOREGROUND, TerminalOptions.DEF_FOREGROUND.asHexString());
      add(options, ConfigItem.SELECTION, TerminalOptions.DEF_SELECTION.asHexString());
      add(options, ConfigItem.FONT_NAME, TerminalOptions.DEF_FONTNAME, true);
      add(options, ConfigItem.FONT_SIZE, TerminalOptions.DEF_FONTSIZE);
      add(options, ConfigItem.WEB_SOCKET_TIMEOUT, MAX_WEB_SOCKET_IDLE_TIMEOUT);
      add(options, ConfigItem.WATCHDOG_TIMEOUT, WATCH_DOG_TIMEOUT);
      add(options, ConfigItem.PORT, 0);
      add(options, ConfigItem.HOST, null);
      add(options, ConfigItem.CLIPBOARD, true);
      add(options, ConfigItem.TASKBAR, true);
      add(options, ConfigItem.TASKBAR_STYLE, "fixed");
      add(options, ConfigItem.USE_LOCALSTORAGE, false);
      add(options, ConfigItem.BROADCAST_CHANNEL_PING_TIMEOUT, -1);

      // common websocket parameters
      add(options, ConfigItem.MAX_BINARY_MSG_SIZE, MAX_BINARY_MESSAGE_SIZE);
      add(options, ConfigItem.MAX_TEXT_MSG_SIZE, MAX_TEXT_MESSAGE_SIZE);
      add(options, ConfigItem.MAX_IDLE_TIME, MAX_WEB_SOCKET_IDLE_TIMEOUT);
      add(options, ConfigItem.CONNECT_RETRY_DELAY, DELAY_BETWEEN_CONNECT_TRIES);
      add(options, ConfigItem.PING_DELAY, DELAY_BETWEEN_PING_TRIES);
      add(options, ConfigItem.PING_PONG_INTERVAL, PING_PONG_INTERVAL);
      add(options, ConfigItem.MAX_LOST_PINGS, MAX_LOST_PINGS);
      add(options, ConfigItem.TRY_CONNECT_MSG, TRY_TO_CONNECT_MESSAGE, true);
      add(options, ConfigItem.SERVER_UNAVAILABLE_MSG, SERVER_UNAVAILABLE_MESSAGE, true);
      add(options, ConfigItem.CONNECTION_RESTORED_MSG, CONNECTION_RESTORED_MESSAGE, true);

      // http configuration settings for the embedded web server
      add(options, ConfigItem.MAX_OUT_BUFFER_SIZE, OUTPUT_BUFFER_SIZE);
      add(options, ConfigItem.MAX_OUT_AGGREGATION_SIZE, OUTPUT_AGGREGATION_SIZE);
      add(options, ConfigItem.MAX_HTTP_IDLE_TIME, INFINITE_TIMEOUT);
      add(options, ConfigItem.MAX_RESPONSE_HEADER_SIZE, RESPONSE_HEADER_SIZE);
      add(options, ConfigItem.MAX_REQUEST_HEADER_SIZE, REQUEST_HEADER_SIZE);

      // the possible values for renderer are webgl and 2d (case insensitive)
      String renderer = ConfigItem.RENDERER.read("2d").trim().toLowerCase();
      options.put(ConfigItem.RENDERER.name(), renderer);

      // Read if the drawing batches are cached
      // sets true if the web client uses a buffer to cache drawings, otherwise false.
      add(options, ConfigItem.GRAPHICS_CACHED, false);

      add(options, ConfigItem.DISABLE_PIXEL_MANIPULATION, false);

      // Get client font metrics cache size from configuration
      int clientTextMetricsCacheSize =
         Utils.getDirectoryNodeInt(null,
                                   FontTable.FONT_METRICS_NODE_ID + "/" + ConfigItem.CACHE_SIZE.name(),
                                   0,
                                   false);
      options.put(ConfigItem.CACHE_SIZE.name(), String.valueOf(clientTextMetricsCacheSize));

      options.put(ConfigItem.LANG.name(), EnvironmentOps.getCurrentLanguageString());
      
      options.put(ConfigItem.JS_DEBUG_LOGGING.name(), "false");

      options = Collections.unmodifiableMap(options);
      
      if (projectToken != null)
      {
         tokenConfigsCachedPairs.put(projectToken, options);
      }
      
      return options;
   }
   
   /**
    * Returns the base client options. 
    *
    * @return   The base client options. 
    */
   @Override
   public BaseClientBuilderOptions getBaseOptions()
   {
      return baseOptions;
   }

   /**
    * Add the given named option to the overridden options map.
    *
    * @param    name
    *           The option name
    * @param    value
    *           The option value
    */
   void override(String name, String value)
   {
      overriddenOptions.get().put(name, value);
   }

   /**
    * Returns the value stored for the client config parameter. First checks the overridden options and 
    * then falls back to the default options.
    *
    * @param    clientConfig
    *           The client config.
    *           
    * @return   The value stored for the client config parameter. 
    */
   String get(ConfigItem clientConfig)
   {
      String configName = clientConfig.name();
      if (overriddenOptions.get().containsKey(configName))
      {
         return overriddenOptions.get().get(configName);
      }
      if (dirOptions.containsKey(configName))
      {
         return dirOptions.get(configName);
      }
      return null;
   }
   
   /**
    * Returns the default web client options.
    * 
    * @return   See above.
    */
   Map<String, String> getDefaults()
   {
      return dirOptions;
   }

   /**
    * Returns the default web client options.
    *
    * @return   See above.
    */
   Map<String, String> getOverriddenOptions()
   {
      return overriddenOptions.get();
   }

   /**
    * Convenience method to add to the options map the name and value of the client config.
    *
    * @param    options
    *           The options map of client config name : value pairs.
    * @param    clientConfig
    *           The client config.
    * @param    defaultValue
    *           The default value.
    * @param    <T>
    *           The type of the client config value.
    */
   private static <T> void add(Map<String, String> options, ConfigItem<T> clientConfig, T defaultValue)
   {
      add(options, clientConfig, defaultValue, false);
   }
   
   /**
    * Convenience method to add to the options map the name and value of the client config.
    *
    * @param    options
    *           The options map of client config name : value pairs.
    * @param    clientConfig
    *           The client config.
    * @param    defaultValue
    *           The default value.
    * @param    quoteStrings
    *           Flag to add quotes to the string.
    * @param    <T>
    *           The type of the client config value.
    */
   private static <T> void add(Map<String, String> options,
                               ConfigItem<T> clientConfig,
                               T defaultValue,
                               boolean quoteStrings)
   {
      String value = String.valueOf(clientConfig.read(defaultValue));
      options.put(clientConfig.name(), quoteStrings ? BaseClientBuilderOptions.quotedString(value) : value);
   }
}