EnvironmentDaemon.java

/*
** Module   : EnvironmentDaemon.java
** Abstract : Daemon to handle Progress 4GL compatible Windows environment handling
**
** Copyright (c) 2013-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20130604 Created initial version.
** 002 EVL 20140327 Changing constructor parameter list to be able to request the server for
**                  properties.
** 003 CA  20140702 Added isCustomEnvironment and getEnvironmentName APIs.
** 004 MAG 20140908 Added getEnvironmentType APIs. Added return value to load() method.
**                  Change the unload() implementation. Implements read only environments loading.
**                  Added setDefaultEnvironment() which override default environment.              
** 005 HC  20151124 Improved loading of boolean environment variables from Progress INI files and
**                  Windows Registry.
**     EVL 20151130 Fix for image clipping regression.  The server side received wrong pixels per
**                  row value because none started value was not replaced with default one.
** 006 CA  20180523 Fixed environment name, when used in maps: registries are case-insensitive,
**                  stanza INI are case-sensitive. 
** 007 RFB 20220310 The currentEnv was not being set to the one being loaded. Ref. #6088.
**     SVL 20220620 Fixed LOAD which effectively called USE. Support for USE("ininame") for the config file
**                  specified at the client startup.
**     SBI 20221202 Added subscribe(String, String, Consumer<String>).
** 008 SAT 20231020 Updated getEnvironmentName() to return "" instead of null when env is null.
** 009 GBB 20231027 Fallback env for non-Win, non-ini LOAD, UNLOAD, SET, GET-KEY-VALUE, PUT-KEY-VALUE.
** 010 GBB 20231204 Adds base key to the fallback environment reader. Case-insensitivity improved.
**                  Instantiate the fallback reader as a default env for non-Win OS.
** 011 GBB 20240826 Moving getOperatingSystem() and getCurrentPrinterName() here from OsPropertiesDaemon.
**                  Removing dependency on Session. Replacing env prop subscription mechanism rpc from server.
*/
/*
** 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.util.osresource;

import java.io.*;
import java.util.*;
import java.util.function.*;

import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.ui.client.widget.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;

import javax.print.*;

/**
 * A class to manipulate environments. The environment can be the INI file, registry sections, java user 
 * Preferences or localStorage.
 *
 * @author    EVL
 */
class EnvironmentDaemon
implements EnvironmentAccessor
{
   /** The default environment to use when load() and use() was not specified. */
   private EnvironmentReader defaultEnv = null;
   
   /** The currently loaded environment to use instead of default. */
   private EnvironmentReader currentEnv = null;
   
   /** Map to store registry environments for possible reuse. */
   private Map<String, EnvironmentReader> envRegMap = new HashMap<String, EnvironmentReader>();
   
   /** Map to store INI environments for possible reuse. */
   private Map<String, EnvironmentReader> envIniMap = new HashMap<String, EnvironmentReader>();
   
   /** Map to store Fallback environments for possible reuse. */
   private Map<String, EnvironmentReader> envFallbackMap = new HashMap<String, EnvironmentReader>();

   /** INI files read only property */
   private boolean isReadOnly = false;
   
   /** Token used to authenticate with the dispatcher when registering APIs. */
   private Object modToken = null;

   /** ClientStorage instance. */
   private final ClientStorage clientStorage;

   /**
    * Create an instance and export its API to the network.
    *
    * @param   envVersion
    *          The current version string of the Progress compatible environment.
    * @param   local
    *          <code>true</code> to startup within the server process which must bypass the
    *          shared infrastructure initialization. Use <code>false</code> for the normal
    *          client JVM startup.
    * @param   isWin
    *          <code>true</code> if this is an WIN32 environment backed up by a default. 
    * @param   clientStorage
    *          ClientStorage instance. 
    */
   EnvironmentDaemon(String envVersion, boolean local, boolean isWin, ClientStorage clientStorage)
   {
      this.clientStorage = clientStorage;
      isReadOnly = getReadOnly();

      defaultEnv = isWin ?
         new Registry(envVersion) :
         new FallbackEnvironmentReader("__default", clientStorage, null);

      modToken = RemoteObject.registerServer(EnvironmentAccessor.class, this, modToken, local);
   }
   
   /**
    * Creates application defaults implementing Progress LOAD statement.
    * 
    * @param env 
    *        The environment to create or load. Can be the registry key, initialization file.
    * @param directory 
    *        The directory path for existing initialization file or place to create
    *        initialization file.
    * @param flagNew
    *        If <code>true</code> - the new file or registry key will be created overwriting the
    *        possibly existed one.
    * @param baseKey
    *        When the parameter is specified the searching is performed under the particular key
    *        value. It is possible to specify "INI" as parameter. In this case search or creation
    *        will be done inside initialization file specified in env parameter.
    *        
    * @return  <code>true</code> if loaded false otherwise      
    */
   @Override
   public boolean load(String env,
                       String directory,
                       boolean flagNew,
                       String baseKey)
   {
      EnvironmentReader envAcc;
      Map<String, EnvironmentReader> envs;

      boolean isIniType = EnvironmentReader.TYPE_INI.equalsIgnoreCase(baseKey);
      if (!isIniType && !PlatformHelper.isUnderWindowsFamily())
      {
         envs = envFallbackMap;
         env = env.toLowerCase();
         envAcc = new FallbackEnvironmentReader(env, clientStorage, baseKey.toLowerCase());
      }
      else if (isIniType)
      {
         envs = envIniMap;
         envAcc = new StanzaIni(env, directory, flagNew, isReadOnly);
      }
      else
      {
         envs = envRegMap;
         env = env.toLowerCase();
         envAcc = new Registry(env, flagNew, baseKey);
      }
      
      boolean loaded = envAcc.load();
      
      if (loaded)
      {
         envs.put(env, envAcc);
      }
      
      return loaded;
   }

   /**
    * Specifies environment defaults to use in subsequent windows.
    * 
    * @param    env 
    *           The environment to load. If the parameter is empty string ("") the default
    *           environment becomes the current one.
    *           
    * @return   The loaded environment's name, as it was saved on LOAD.  <code>null</code> if the
    *           environment can't be found.
    */
   @Override
   public String use(String env)
   {
      // Empty string as environment makes the default environment the current one
      if (env == null || env.isEmpty())
      {
         currentEnv = null;
         return "";
      }

      // registry first
      currentEnv = envRegMap.get(env.toLowerCase());
      if (currentEnv != null)
      {
         return currentEnv.getEnvironmentName();
      }

      String envIni = env.indexOf('.') >= 0 ? env : env + ".ini";

      // default ini second
      if (defaultEnv != null && envIni.equals(defaultEnv.getEnvironmentName()))
      {
         currentEnv = null;
         return "";
      }

      // stanza third
      currentEnv = envIniMap.get(envIni);
      if (currentEnv != null)
      {
         return currentEnv.getEnvironmentName();
      }

      // fallback
      currentEnv = envFallbackMap.get(env.toLowerCase());
      if (currentEnv != null)
      {
         return currentEnv.getEnvironmentName();
      }

      // not found
      return null;
   }
   
   /**
    * Check if a custom environment is in use.
    * 
    * @return   See above.
    */
   @Override
   public boolean isCustomEnvironment()
   {
      return currentEnv != null;
   }
   
   /**
    * Get the current environment's name.
    * 
    * @return   See above.
    */
   @Override
   public String getEnvironmentName()
   {
      EnvironmentReader env = (currentEnv == null ? defaultEnv : currentEnv);
      
      return (env == null ? "" : env.getEnvironmentName());
   }

   /**
    * Get the current environment's type.
    * 
    * @return  environment type.
    */
   @Override
   public String getEnvironmentType()
   {
      EnvironmentReader env = (currentEnv == null ? defaultEnv : currentEnv);
      
      return (env == null ? null : env.getEnvironmentType());
   }

   /**
    * Set new value for keys in the current environment or the default environment if
    * the current one is not loaded.
    * 
    * @param    section 
    *           The name of the section containing the key to modify.
    * @param    key 
    *           The name of the key to modify
    * @param    value 
    *           The new value of the key under modification.
    */
   @Override
   public void setKeyValue(String section, String key, String value)
   {
      EnvironmentReader envReader = currentEnv != null ? currentEnv : defaultEnv;
      if (envReader == null)
      {
         return;
      }
      
      if (!envReader.getEnvironmentType().equals(EnvironmentReader.TYPE_FALLBACK) &&
         (section == null || value == null))
      {
         return;
      }
      if (!envReader.getEnvironmentType().equals(EnvironmentReader.TYPE_INI))
      {
         section = section.toLowerCase();
         key = key == null ? null : key.toLowerCase();
      }
      envReader.setKeyValue(section, key, value);
   }

   /**
    * Getting the key value from the current environment or default environment if the current one
    * is not loaded.
    * 
    * @param    section 
    *           The name of the section containing the key to get.
    * @param    key 
    *           The name of the key to get.
    * 
    * @return   The value of the key.
    */
   @Override
   public String getKeyValue(String section, String key)
   {
      EnvironmentReader envReader = currentEnv != null ? currentEnv : defaultEnv;
      if (section == null || envReader == null)
      {
         return "";
      }

      if (!envReader.getEnvironmentType().equals(EnvironmentReader.TYPE_INI))
      {
         section = section.toLowerCase();
         key = key == null ? null : key.toLowerCase();
      }
      return envReader.getKeyValue(section, key);
   }
   
   /**
    * Getting the boolean key value from the current environment.
    * 
    * @param    section
    *           The name of the section containing the key to get.
    * @param    key
    *           The name of the key key to get or default key if not specified.
    * @param    defValue
    *           The default value, to be returned when the value does not exist
    *           in the environment.
    * 
    * @return   The boolean key value, <code>defValue</code> if the value
    *           does not exist in the environment.
    */
   @Override
   public Boolean getBooleanKeyValue(String section, String key, Boolean defValue)
   {
      String strValue = getKeyValue(section, key);
      // if no value for the key - return default one
      if (strValue == null || strValue.isEmpty())
      {
         return defValue;
      }
      
      // the only valid boolean true for Progress INI file is the case insensitive 'yes' 
      // TODO: is this compatible with the allowed boolean values in Windows registry?
      return strValue.trim().equalsIgnoreCase("yes"); 
   }

   /**
    * Unloads specifies environment from the current one.
    * 
    * @param    env 
    *           The environment to unload. Can be the registry or initialization file. The
    *           environment to unload should be previously loaded by load() method.
    *           
    * @return   The loaded environment's name, as it was saved on LOAD.  <code>null</code> if the
    *           environment can't be found.
    */
   @Override
   public String unload(String env)
   {
      if (env == null)
      {
         return null;
      }
      
      Map<String, EnvironmentReader> envs = null;

      if (envRegMap.containsKey(env.toLowerCase()))
      {
         envs = envRegMap;
         env = env.toLowerCase();
      }
      else
      {
         String envIni = env.indexOf('.') >= 0 ? env : env + ".ini";
         if (envIniMap.containsKey(envIni))
         {
            env = envIni;
            envs = envIniMap;
         }
      }
      
      if (envs == null && !PlatformHelper.isUnderWindowsFamily() && envFallbackMap.containsKey(env.toLowerCase()))
      {
         envs = envFallbackMap;
         env = env.toLowerCase();
      }

      for (TitledWindow<?> window : WindowManager.windowList())
      {
         WindowConfig cfg = window.config();
         if (cfg != null && env.equalsIgnoreCase(cfg.envName))
         {
            return null;
         }
      }
      
      if (envs == null)
      {
         return null;
      }
      
      EnvironmentReader envReader = envs.remove(env);
      
      envReader.unload();

      // set current environment to default if unloaded environment was current environment
      if (currentEnv.getEnvironmentType().equals(EnvironmentReader.TYPE_INI) && env.equals(currentEnv.getEnvironmentName()) || 
         env.equalsIgnoreCase(currentEnv.getEnvironmentName()))
      {
         currentEnv = null;
      }

      // notify about unload
      ColorManager.unload(env);
      FontManager.unload(env);

      return envReader.getEnvironmentName();
   }

   /**
    * Sets the default environment to a stanza ini file, overwriting any default registry for
    * WIN32.
    * 
    * @param   iniFile
    *          The name of the ini file backing the stanzas.
    */
   @Override
   public void setDefaultEnvironment(String iniFile)
   {
      File file = new File(iniFile);

      // check if file exists when the file is not loaded from server.
      if (!isReadOnly && !file.exists()) 
      {
         // client aborts if environment file is not found
         throw new RuntimeException(iniFile + " file not found. (5643)");
      }
      
      // directory
      String dir = file.getParent();
      // file name
      String fileName = file.getName();
      
      // load the file and store it as the default environment 
      EnvironmentReader reader = new StanzaIni(fileName, dir, false, isReadOnly);
      if (reader.load())
      {
         defaultEnv = reader;
      }
      else
      {
         // client aborts if environment file is not loaded
         throw new RuntimeException(iniFile + " file not found. (5643)");         
      }
   }

   /**
    * If there is still no match, then any <code>os.name</code> with the
    * lowercase text "unix" will return "UNIX" and any <code>os.name</code>
    * with the lowercase text "windows" will return "WIN32". 
    * <p>
    * If there is still no match, the <code>os.name</code> returned by the
    * Java will be returned. 
    *
    * @return   The name of the current operating system.
    */
   @Override
   public String getOperatingSystem()
   {
      return System.getProperty("os.name").toLowerCase();
   }

   /**
    * Get the name of the current printer on the client side.
    *
    * @return  the printer name.
    */
   @Override
   public String getCurrentPrinterName()
   {
      PrintService prnService = PrintServiceLookup.lookupDefaultPrintService();
      return prnService != null ? prnService.getName() : null;
   }
   
   /**
    * Get environments read only flag.  When read only flag is set to <code>true</code> the 
    * environments INI files are loaded as resources from remote server jar files otherwise
    * will follow default behavior and INI files are loaded from client file system.  
    * 
    * @return  Read only flag (see above).
    */
   private boolean getReadOnly()
   {
      boolean result = false;
      
      Directory remoteDir = DirectoryManager.getInstance();
      if (remoteDir != null)
      {
         result = remoteDir.getBoolean(Directory.ID_RELATIVE, "readOnlyEnvironments", false);
      }      
      
      return result;
   }
}