# I18nOps.java

    /*
    ** Module   : I18nOps.java
    ** Abstract : Progress 4GL compatible I18N environment related methods.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 EVL 20190112 Initial commit.  Adding support for GET-CODEPAGES function.  Adding runtime
    **                  support for CODEPAGE-CONVERT function.  Adding support for CHR/ASC functions.
    **                  If source and target CP are the same - no need to do extra work.  Adding
    **                  implementation stubs fro CP* session attribute getters.
    **     GES 20190509 Moved code to the worker from the external version. The worker no longer uses
    **                  BDT wrappers so it is more efficient if called from other runtime code.
    ** 002 ME  20200401 Make `UNDEFINED` property public, can't be used for longchar.
    ** 003 ME  20200604 Avoid NPE exception if text to convert is null.
    ** 004 RFB 20200722 Fixed javadoc issue
    ** 005 OM  20200918 Fixed UTF-8 variable-bytes support for chr(n, str, str).
    **     OM  20210330 Added data pre-validation which detects issues for which the Java Charsets are too
    **                  permissible. Improved COPY-LOB engine and validations.
    **     OM  20210404 Fine-tuned errors generated by codepage management.
    **     OM  20210407 Reworked asc() and chr() functions.
    **     CA  20210423 Fixed I18nOps.convmap2Java access (key must be uppercased).
    **     OM  20210602 CPSTREAM defaults to the value configured in [p2j.cfg.xml] if not specified in directory.
    **     OM  20210907 CHR(0) returns the empty string. Fixed get4glCharacter() decoding for quad byte mode.
    ** 006 SBI 20220508 Changed getCharsetNative() to use Utils.getCharsetName().
    **     TJD 20220504 Java 11 compatibility minor changes
    ** 007 CA  20230110 Small improvement for context-local resolution.
    **     HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
    **                  for performance.
    ** 008 EVL 20230310 Implemented the set of codepages nedd the NULL byte to check for incoming binary data.
    **                  Used now in prevalidateData() method.
    ** 009 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
    ** 010 TT  20230915 Read properties from StartupParameters.
    ** 011 OM  20231215 Implemented getCPColl / SESSION:CPCOLL to always return 'BASIC'.
    ** 012 SB  20240411 Added getJavaCPInternal to get the JVM charset for the CP defined in
    **                  SESSION:CPINTERNAL.
    ** 013 SP  20240619 Added getWordDelimiters to get a regex list of delimiters based on codepage used.
    ** 014 SBI 20250227 Added missed javadoc for getJavaCharset(String) that can be used by the client code.
    ** 015 SP  20250314 Added getDelimiterPattern to retrieve a cached regex Pattern for word delimiters.
    ** 016 TG  20250516 Added getCodePage(BaseDataType) to retrieve codePage for CLOB and LONGCHAR.
    **     TG  20250519 Raising error when an instance is not a longchar or clob.
    **     TG  20250521 Reverting the previous change. 4GL does not throw error when is an other instance.
    */

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

    import java.io.*;
    import java.nio.charset.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.regex.*;

    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.util.*;

    /**
     * Progress compatible I18N-related environment static utility methods.
     * <p>
     * <pre>
     * Progress Function                    Java Equivalent
     * ---------------------                ----------------------
     * codepage-convert                     {@link #codePageConvert}
     * get-codepages                        {@link #getCodePages}
     * </pre>
     * <p>
     * <pre>
     * System Handle:Attribute              Java Equivalent
     * -------------------------            ----------------------
     * session:charset                      {@link #getCharset}        (get)
     * session:cpcase                       {@link #getCPCase}         (get)
     * session:cpcoll                       {@link #getCPColl}         (get)
     * session:cpinternal                   {@link #getCPInternal}     (get)
     * session:cplog                        {@link #getCPLog}          (get)
     * session:cpprint                      {@link #getCPPrint}        (get)
     * session:cprcodein                    {@link #getCPRCodeIn}      (get)
     * session:cprcodeout                   {@link #getCPRCodeOut}     (get)
     * session:cpstream                     {@link #getCPStream}       (get)
     * session:cpterm                       {@link #getCPTerm}         (get)
     * rcode-info:codepage                  {@link #getCodePage}       (get)
     * </pre>
     */
    public class I18nOps
    {
       /** The 4GL name for ISO8859-1 / Latin-1 character encoding. */
       public static final String ABL_LATIN1 = "ISO8859-1";

       /** The Java name for ISO-8859-1 / Latin-1 character encoding. */
       public static final String JAVA_LATIN1 = "ISO-8859-1";

       /**
        * Maps code pages from defined default convmap.cp to supported Java code page names.
        */
       public static final Map<String, String> convmap2JavaDefault = new LinkedHashMap<>();

       /**
        * Maps code pages from currently defined default convmap.cp to supported Java code page names.
        * This is the working copy after support verification for current JVM.
        */
       public static final Map<String, String> convmap2Java = new CaseInsensitiveLinkedHashMap<>();

       /**
        * Maps code pages from supported Java code page names to Progress compatible code pages.
        * This is the working copy after support verification for current JVM.
        */
       public static final Map<String, String> convmap2Progress = new LinkedHashMap<>();

       /** Special code page meaning no code page conversion for either source or target. */
       public static final String CP_UNDEFINED = "UNDEFINED";

       /** The standard name of the 4GL file containing the conversion map. */
       public static final String CONVERSION_MAP_FILE = "convmap.cp";

       /** Stores context-local state variables. */
       private static final ContextContainer work = new ContextContainer();

       /** Stores the current list of the code pages to do NULL byte check. */
       private static final Set<String> zeroByteCheck;

       /** Maps code pages to regex list of delimiters used for word splitting. */
       private static final Map<String, String> cp2Delimiters = new HashMap<>();

       /** Cache for regex patterns used in word splitting. */
       private static final Map<String, Pattern> delimiterPatterns = new ConcurrentHashMap<>(8);

       // initialize code page mappings to Java compatible names
       static
       {
          // These mappings are explicitly being added in the same exact order
          // they appear in the 4GL GET-CODEPAGES() function. Do not change
          // the order, otherwise the result of that function will be incorrect.
          //---------------------- 4GL CP, Java CP
          convmap2JavaDefault.put("1256", "windows-1256");
          convmap2JavaDefault.put("709", "709"); // support is undefined
          convmap2JavaDefault.put("708", "708"); // support is undefined
          convmap2JavaDefault.put("721", "721"); // support is undefined
          convmap2JavaDefault.put("711", "711"); // support is undefined
          convmap2JavaDefault.put("786", "786"); // support is undefined
          convmap2JavaDefault.put("714", "714"); // support is undefined
          convmap2JavaDefault.put("710", "710"); // support is undefined
          convmap2JavaDefault.put("720", "720"); // support is undefined
          convmap2JavaDefault.put("BIG-5", "Big5");
          convmap2JavaDefault.put("GB2312", "GB2312");
          convmap2JavaDefault.put("CP936", "CP936"); // support is undefined
          convmap2JavaDefault.put("CP950", "CP950"); // support is undefined
          convmap2JavaDefault.put("IBM852", "IBM852");
          convmap2JavaDefault.put("1250", "windows-1250");
          convmap2JavaDefault.put("ISO8859-2", "ISO-8859-2");
          convmap2JavaDefault.put("1253", "windows-1253");
          convmap2JavaDefault.put("IBM851", "IBM851"); // support is undefined
          convmap2JavaDefault.put("ISO8859-8", "ISO-8859-8");
          convmap2JavaDefault.put("IBM862", "IBM862");
          convmap2JavaDefault.put("IBM850", "IBM850");
          convmap2JavaDefault.put("IBM858", "IBM858"); // support is undefined
          convmap2JavaDefault.put(ABL_LATIN1, JAVA_LATIN1);
          convmap2JavaDefault.put("ISO8859-15", "ISO-8859-15");
          convmap2JavaDefault.put("SHIFT-JIS", "Shift_JIS");
          convmap2JavaDefault.put("EUCJIS", "EUCJIS"); // support is undefined
          convmap2JavaDefault.put("KSC5601", "KSC5601"); // support is undefined
          convmap2JavaDefault.put("CP949", "x-windows-949");
          convmap2JavaDefault.put("CP1361", "CP1361"); // support is undefined
          convmap2JavaDefault.put("1252", "windows-1252");
          convmap2JavaDefault.put("1257", "windows-1257");
          convmap2JavaDefault.put("MAZOVIA", "MAZOVIA"); // support is undefined
          convmap2JavaDefault.put("ROMAN-8", "ROMAN-8"); // support is undefined
          convmap2JavaDefault.put("KOI8-R", "KOI8-R");
          convmap2JavaDefault.put("1251", "windows-1251");
          convmap2JavaDefault.put("IBM866", "IBM866");
          convmap2JavaDefault.put("ISO8859-5", "ISO-8859-5");
          convmap2JavaDefault.put("620-2533", "x-iso-8859-11");
          convmap2JavaDefault.put("1254", "windows-1254");
          convmap2JavaDefault.put("IBM857", "IBM857");
          convmap2JavaDefault.put(CP_UNDEFINED, CP_UNDEFINED);
          convmap2JavaDefault.put("IBM861", "IBM861");
          convmap2JavaDefault.put("IBM437", "IBM437");
          convmap2JavaDefault.put("UTF-8", "UTF-8");
          convmap2JavaDefault.put("UCS2", "UCS2"); // support is undefined
          convmap2JavaDefault.put("UTF-32", "UTF-32");
          convmap2JavaDefault.put("UTF-16", "UTF-16");
          convmap2JavaDefault.put("UTF-16BE", "UTF-16BE");
          convmap2JavaDefault.put("UTF-16LE", "UTF-16LE");
          convmap2JavaDefault.put("UTF-32BE", "UTF-32BE");
          convmap2JavaDefault.put("UTF-32LE", "UTF-32LE");
          convmap2JavaDefault.put("ISO6937", "ISO6937"); // support is undefined
          convmap2JavaDefault.put("CP950-HKSCS", "x-MS950-HKSCS");
          convmap2JavaDefault.put("GB18030", "GB18030");

          // possible list of the codepages to do NULL byte checking
          String[] zeroByteCheckCandidates =
             {"1250", "1251", "1252", "1253", "1254", "1256", "1257", "620-2533", "708", "709", "710", "711",
              "714", "720", "721", "786", "IBM437", "IBM850", "IBM851", "IBM852", "IBM857", "IBM858", "IBM861",
              "IBM862", "IBM866", "ISO8859-1", "ISO8859-2", "ISO8859-5", "ISO8859-8", "ISO8859-15", "KOI8-R",
              "MAZOVIA", "ROMAN-8"};

          Set<String> zeroByteCheckTmp = new HashSet<String>();
          // set up the codepages to check for 0 byte
          // undefined and latin1 should always be here
          zeroByteCheckTmp.add(ABL_LATIN1);
          zeroByteCheckTmp.add(CP_UNDEFINED);
          for (String cpName : zeroByteCheckCandidates)
          {
             String cpJavaName = convmap2JavaDefault.get(cpName);
             if (cpJavaName != null && Charset.isSupported(cpJavaName))
             {
                zeroByteCheckTmp.add(cpName);
             }
          }

          // make unmodifiable list
          zeroByteCheck = Collections.unmodifiableSet(zeroByteCheckTmp);

          // check if the current Java environment supports all defaults
          for (String csName : convmap2JavaDefault.keySet())
          {
             String csJavaName = convmap2JavaDefault.get(csName);
             // special UNDEFINED code page is always supported
             if (Charset.isSupported(csJavaName) || CP_UNDEFINED.equals(csJavaName))
             {
                // add supported code page to current convmap for further usage
                convmap2Java.put(csName, csJavaName);
                // back mapping to improve search performance
                convmap2Progress.put(csJavaName, csName);
             }
          }

          cp2Delimiters.put("ISO8859-1", "[\\x01-\\x22\\x26\\x28-\\x2f\\x3a-\\x3f\\x5b-\\x5e\\x60\\x7b-\\x89"
               + "\\x9f\\xa1-\\xa3\\xa5\\xa9\\xab\\xb8\\xbb\\xbc\\xbd\\xbf\\xc5\\xc6\\xd7\\xd8\\xe5\\xe6\\xf8]");
          cp2Delimiters.put("ISO8859-15", "[\\x01-\\x22\\x26\\x28-\\x2f\\x3a-\\x3f\\x5b-\\x5e\\x60\\x7b-\\x83"
               + "\\x85-\\x87\\x89\\xa1\\xa3\\xab\\xbb\\xc5\\xc6\\xd8\\xe5\\xe6\\xf8]");
          cp2Delimiters.put("1252", "[\\x01-\\x22\\x26\\x28-\\x2f\\x3a-\\x3f\\x5b-\\x5e\\x60\\x7b-\\x89"
               + "\\x8f\\xa1\\xa2\\xa5\\xa9\\xab\\xb8\\xbb-\\xbd\\xbf\\xc5\\xc6\\xd8\\xe5\\xe6\\xf8]");
          cp2Delimiters.put("IBM850", "[\\x01-\\x22\\x26\\x28-\\x2f\\x3a-\\x3f\\x5b-\\x5e\\x60\\x7b-\\x7f\\x9c"
               + "\\x9e\\x9f\\xa6-\\xa9\\xab-\\xb4\\xb8-\\xc5\\xc8-\\xcf\\xd9-\\xdd\\xdf\\xe6\\xee-\\xff]");
          cp2Delimiters.put("UTF-8", "[\\x01-\\x22\\x26\\x28-\\x2f\\x3a-\\x3f\\x5b-\\x5e\\x60\\x7b-\\x7f]");
       }

       /**
        * Gets the comma separated list of the codepages supported by the conversion map for the
        * Progress compatible environment.
        * <p>
        * In original environment the map is defined either in <code>convmap.cp</code> file or
        * by the <code>-convmap</code> startup parameter for current ABL session.
        *
        * @return   The list of the currently supported codepages.
        */
       public static character getCodePages()
       {
          // TODO: implement me with taking into account the convmap.cp compatibility
          StringBuilder sbRes = new StringBuilder();

          // getting available code pages
          Iterator<String> csIter = convmap2Java.keySet().iterator();
          while (csIter.hasNext())
          {
             sbRes.append(csIter.next().toUpperCase());
             if (csIter.hasNext())
             {
                sbRes.append(",");
             }
          }

          return new character(sbRes.toString());
       }

       /**
        * Returns the code page of the specified longchar/clob variable.
        * If the variable is not a longchar/clob, the function will return an empty character.
        *
        * @param   value
        *          The value for which the codepage will be retrieved.
        *
        * @return  See above
        */
       public static character getCodePage(BaseDataType value)
       {
          if (value instanceof longchar)
          {
             return ((longchar) value).getCodePage();
          }
          else if (value instanceof clob)
          {
             return ((clob) value).getCodePage();
          }
          return character.UNKNOWN;
       }

       /**
        * Checks if the given code page name in currently defined convmap registry.
        *
        * @param    cpName
        *           The name of the Progress code page name.
        *           Can be passed with lower case
        *
        * @return   <code>TRUE</code> is code page is in current convmap,
        *           <code>FALSE</code> otherwise.
        */
       public static boolean isSupported(String cpName)
       {
          return convmap2Java.containsKey(cpName);
       }

       /**
        * Converts the given text using the specified codepage, assuming the text is encoded with
        * the given source codepage.
        *
        * @param    source
        *           The text to be converted.
        * @param    targetCP
        *           The target codepage.
        * @param    sourceCP
        *           The source codepage. If the text is a {@link longchar}
        *           instance, then the text's internal codepage is used.
        *
        * @return   The converted text.
        */
       public static <T extends Text> T codePageConvert(T source, String targetCP, String sourceCP)
       {
          if (source == null)
          {
             return null;
          }

          try
          {
             // create return value instance based on input parameter type
             T resTxt = (T) BaseDataTypeFactory.instantiate(source.getClass());

             // the return value has been constructed
             if (source instanceof longchar)
             {
                ((longchar) resTxt).fixCodePage(targetCP);
             }

             resTxt.assign(source.getValue());
             return resTxt;
          }
          catch (ReflectiveOperationException roe)
          {
             // text instance is incompatible with extends Text
             return null;
          }
       }

       /**
        * Converts the given text using the specified codepage, assuming the text is encoded with
        * the given source codepage.  This is the internal version to be called for service purposes.
        *
        * @param    text
        *           The text to be converted.
        * @param    srcCP
        *           The source codepage.
        * @param    tarCP
        *           The target codepage.
        *
        * @return   The converted text.
        */
       @Deprecated
       public static String codePageConvertWorker(String text, String srcCP, String tarCP)
       {
          String result = text;

          String srcJCP = null;
          String tarJCP = null;

          // check if the target codepage was specified, no target then no conversion
          if (tarCP != null && !tarCP.isEmpty())
          {
             // convert to uppercase version
             tarJCP = convmap2Java.get(tarCP);

             if (tarJCP != null)
             {
                // target CP is valid, check the source CP
                if (srcCP == null || srcCP.isEmpty())
                {
                   srcCP = _getCharset();
                }

                srcJCP = convmap2Java.get(srcCP);

                // source/target CP validation completed
                if (srcJCP != null)
                {
                   // if same source and target CP's, don't do anything else
                   if (text != null && !srcJCP.equals(tarJCP))
                   {
                      // the real conversion is here
                      try
                      {
                         // undefined encoding forces the source to be untouched
                         byte[] bytes = CP_UNDEFINED.equals(srcJCP) ? text.getBytes() : text.getBytes(srcJCP);

                         // undefined encoding forces the target to be untouched
                         result = CP_UNDEFINED.equals(tarJCP) ? new String(bytes) : new String(bytes, tarJCP);
                      }
                      catch (UnsupportedEncodingException uee)
                      {
                         // TODO: does the 4GL really ignore this condition?
                      }
                   }
                }
                else
                {
                   // TODO: no mapping for source CP name, log this or add the 4GL error
                }
             }
             else
             {
                // TODO: no mapping for target CP name, log this or add the 4GL error
             }
          }

          return result;
       }

       /**
        * Converts the given integer character code to char value according to source and target
        * codepage values.
        *
        * @param    ascCode
        *           The key code to be converted.
        * @param    targetCP
        *           The target codepage of the char to return (Progress code page name).
        * @param    sourceCP
        *           The source codepage of the integer key code (Progress code page name).
        * @param    defaultCP
        *           The default codepage to be used when target or source codepages is invalid.
        *           This is the Java code page name which must be always valid.
        *
        * @return   The converted single character value inside integer.
        */
       public static int chr(int ascCode, String targetCP, String sourceCP, String defaultCP)
       {
          // default return value is invalid char
          int intChRes = 0xFFFF;

          // calculate Java codepages for source and target
          String targetJavaCP = null;
          String sourceJavaCP = null;
          if (targetCP == null || targetCP.isEmpty())
          {
             targetJavaCP = CP_UNDEFINED;
          }
          else
          {
             targetJavaCP = convmap2Java.get(targetCP);
          }

          // if target code page is not valid we return invalid character
          if (targetJavaCP != null)
          {
             // source code page was not specified, use default charset for session
             if (sourceCP == null || sourceCP.isEmpty())
             {
                sourceJavaCP = defaultCP;
             }
             else
             {
                sourceJavaCP = convmap2Java.get(sourceCP);
             }
             // here we should have both target and source code pages validated
             if (sourceJavaCP != null)
             {
                // if target is undefined, no conversion should occur
                if (CP_UNDEFINED.equals(targetJavaCP) || sourceJavaCP.equals(targetJavaCP))
                {
                   intChRes = ascCode;
                }
                else
                {
                   // start conversion
                   try
                   {
                      // TODO: is this valid? Add source code page usage
    //                  byte[] baSrc = ascCode >= 0 && ascCode <= 255
    //                                    ? new byte[] {(byte)ascCode}
    //                                    : new byte[] {(byte)(ascCode >> 8), (byte)ascCode};
    //                  byte[] baSrc = new byte[] {(byte)ascCode, (byte)(ascCode >> 8),
    //                                             (byte)(ascCode >> 16), (byte)(ascCode >> 24)};
    //                  String strSrc = CP_UNDEFINED.equals(sourceJavaCP)
    //                                     ? new String(baSrc) : new String(baSrc, sourceJavaCP);
                      // this approach ignores source code page option for out of the 255 codes
                      // considering unicode
                      String strSrc = null;
                      if (ascCode >= 0 && ascCode <= 255)
                      {
                         byte[] baSrc = new byte[] {(byte)ascCode};
                         strSrc = CP_UNDEFINED.equals(sourceJavaCP)
                                     ? new String(baSrc) : new String(baSrc, sourceJavaCP);
                      }
                      else
                      {
                         int[] codePointArr = new int[] {ascCode};
                         strSrc = new String(codePointArr, 0, 1);
                      }

                      byte [] baSrcTmp = strSrc.getBytes(targetJavaCP);

                      // undefined encoding forces the target to be untouched
                      String strRes = new String(baSrcTmp, targetJavaCP);

                      intChRes = strRes.codePointAt(0);
                   }
                   catch (UnsupportedEncodingException uee)
                   {
                      // nothing to do for now just leave return value as invalid
                      // DEBUGGING, remove when done
                      int dummy = 0;
                   }
                }
             }
          }

          return intChRes;
       }

       /**
        * Converts the given integer character code to char value according to source and target
        * codepage values (double byte enabled).
        *
        * @param    ascCode
        *           The key code to be converted.
        * @param    targetCP
        *           The target codepage of the char to return (Progress code page name).
        * @param    sourceCP
        *           The source codepage of the integer key code (Progress code page name).
        *
        * @return   The converted single character value inside integer.
        */
       public static String chr(int ascCode, String targetCP, String sourceCP)
       {
          WorkArea wa = work.obtain();

          // calculate Java codepages for source and target
          if (sourceCP == null || sourceCP.equalsIgnoreCase(CP_UNDEFINED))
          {
             sourceCP = _getCPInternal(); // default to CPINTERNAL if not specified
          }
          if (targetCP == null || targetCP.equalsIgnoreCase(CP_UNDEFINED))
          {
             targetCP = _getCPInternal(); // default to CPINTERNAL if not specified
          }

          Charset sourceJavaCP = null;
          Charset targetJavaCP = null;

          try
          {
             sourceJavaCP = Charset.forName(convmap2Java.get(sourceCP));
             targetJavaCP = Charset.forName(convmap2Java.get(targetCP));
          }
          catch (Exception ex)
          {
             // ignore errors
          }

          if (sourceJavaCP == null || targetJavaCP == null)
          {
             // Oddly enough 4GL first check if there is a mapping in convmap but only if source and target are
             // different. If both source and target are the same invalid value then the error is 912, codepage
             // not found.

             if (sourceCP.equalsIgnoreCase(targetCP))
             {
                ErrorManager.recordOrThrowError(912, sourceCP, CONVERSION_MAP_FILE);
                // Code page attribute table for <code-page> was not found in <filename>. (912)
             }
             else
             {
                final String msg1 = "Code page conversion table for %s to %s was not found in %s";
                final String msg2 = "CHR could not find the appropriate conversion table in %s";

                ErrorManager.recordOrThrowError(new int[] { 6063, 1586 },
                         new String[] {
                                  String.format(msg1, sourceCP, targetCP, CONVERSION_MAP_FILE),
                                  String.format(msg2, CONVERSION_MAP_FILE) }, false);
             }

             return null;
          }

          // interpret key code using source codepage
          String chr = get4glCharacter(ascCode, sourceJavaCP);

          // 4gl checks for lead byte, however there should be only one character
          if (chr == null || chr.length() > 1)
          {
             return null;
          }

          return chr;
       }

       /**
        * Use the specified {@code charset} to construct a string containing the code of character passed as
        * first parameter.
        *
        * @param   ascCode
        *          The code (double byte supported) of the character to be converted.
        * @param   charset
        *          The charset to be used in conversion.
        *
        * @return  The string as noted above. If the {@code ascCode} is negative or {@code charset} is somehow
        *          unable to process the input, {@code null} is returned.
        */
       private static String get4glCharacter(int ascCode, Charset charset)
       {
          // return unknown value for negative arguments
          if (ascCode < 0)
          {
             return null;
          }

          // there is no "\0" character in 4GL. Returning empty string.
          if (ascCode == 0)
          {
             return "";
          }

          byte[] data;
          if (ascCode < 0x0100)
          {
             // single byte case
             data = new byte[] { (byte) ascCode };
          }
          else if (ascCode < 0x010000)
          {
             // double byte mode, BigEndian mode
             data = new byte[2];
             data[0] = (byte) ((ascCode >>> 8) & 0x0FF);
             data[1] = (byte) (ascCode & 0x0FF);
          }
          else if (ascCode < 0x01000000)
          {
             // triple byte mode, BigEndian mode
             data = new byte[3];
             data[0] = (byte) ((ascCode >>> 16) & 0x0FF);
             data[1] = (byte) ((ascCode >>> 8) & 0x0FF);
             data[2] = (byte) (ascCode & 0x0FF);
          }
          else
          {
             // quad byte mode, BigEndian mode
             data = new byte[4];
             data[0] = (byte) ((ascCode >>> 24) & 0x0FF);
             data[1] = (byte) ((ascCode >>> 16) & 0x0FF);
             data[2] = (byte) ((ascCode >>> 8) & 0x0FF);
             data[3] = (byte) (ascCode & 0x0FF);
          }

          try
          {
             String ret = new String(data, charset);
             if (ret.length() == 1)
             {
                char test = ret.charAt(0);
                if (test == 0xFFFD || // REPLACEMENT CHARACTER
                    test == 0xFFFE || // not unassigned
                    test == 0xFFFF)   // not unassigned
                {
                   // debatable whether this is an error, from tests 4GL does not raise any condition, usually
                   return "";
                }
             }
             return ret;
          }
          catch (Exception e)
          {
             // if the charset is unable to convert the data to a valid string
             return null;
          }
       }

       /**
        * Converts the given char value to integer character code according to source and target
        * codepage values.
        *
        * @param    intChValue
        *           The character value to be converted packed into int.
        * @param    targetCP
        *           The target codepage of the integer key code to return (Progress code page name).
        * @param    sourceCP
        *           The source codepage of the char (Progress code page name).
        * @param    defaultCP
        *           The default codepage to be used when target or source codepages is invalid.
        *           This is the Java code page name which must be always valid.
        *
        * @return   The converted single character value.
        */
       public static int asc(int intChValue, String targetCP, String sourceCP, String defaultCP)
       {
          // default return value is invalid key code
          int intRes = -1;

          // calculate Java codepages for source and target
          String targetJavaCP = null;
          String sourceJavaCP = null;

          // need to validate code pages first
          if (targetCP == null || targetCP.isEmpty())
          {
             targetJavaCP = CP_UNDEFINED;
          }
          else
          {
             targetJavaCP = convmap2Java.get(targetCP);
          }

          // if target code page is not valid we return invalid character
          if (targetJavaCP != null)
          {
             // source code page was not specified, use default charset for session
             if (sourceCP == null || sourceCP.isEmpty())
             {
                sourceJavaCP = defaultCP;
             }
             else
             {
                sourceJavaCP = convmap2Java.get(sourceCP);
             }
             // here we should have both target and source code pages validated
             if (sourceJavaCP != null)
             {
                // if target is undefined, no conversion should occur
                if (CP_UNDEFINED.equals(targetJavaCP) || sourceJavaCP.equals(targetJavaCP))
                {
                   intRes = intChValue;
                }
                else
                {
                   // start conversion
                   try
                   {
                      byte[] baSrc = new byte[] {(byte)intChValue, (byte)(intChValue >> 8)};
                      String strSrc = CP_UNDEFINED.equals(sourceJavaCP)
                                         ? new String(baSrc) : new String(baSrc, sourceJavaCP);
                      byte [] baRes = strSrc.getBytes(targetJavaCP);
                      // reconstruct returning integer from array of bytes
                      intRes = 0;
                      for (int i = 0; i < baRes.length; i ++)
                      {
                         // this can be paranoid but ensure we do not exceed 32-bit integer
                         // 0 byte means stop reconstruction
                         if (i > 3 || baRes[i] == 0)
                         {
                            break;
                         }
                         // the approach is to push byte values until 0
                         intRes = (intRes << 8) | (0x000000FF & baRes[i]);
                      }
                   }
                   catch (UnsupportedEncodingException uee)
                   {
                      // nothing to do for now just leave return value as invalid DEBUGGING, remove when done
                      int dummy = 0;
                   }
                }
             }
          }

          return intRes;
       }

       /**
        * Converts the given char value to integer character code according to source and target codepage values.
        *
        * @param    chValue
        *           The character to convert back to char point.
        * @param    targetCP
        *          The target codepage, if set conversion from source codepage is done before (Progress code
        *          page name).
        * @param    sourceCP
        *           The source codepage, the char point is relative to this (Progress code page name).
        *
        * @return  The converted single character value, -1 if not a single character or invalid codepages
        *          specified.
        */
       public static int asc(String chValue, String targetCP, String sourceCP)
       {
          if (chValue == null || chValue.isEmpty())
          {
             return -1;
          }

          // calculate Java codepages for source and target
          if (sourceCP == null || sourceCP.equalsIgnoreCase(CP_UNDEFINED))
          {
             sourceCP = _getCPInternal();
          }

          String sourceJavaCP = convmap2Java.get(sourceCP);
          if (sourceJavaCP == null)
          {
             return raiseErrorAndReturn(912, sourceCP.toUpperCase(), CONVERSION_MAP_FILE, null);
             // Code page attribute table for <code-page> was not found in <filename>. (912)
          }

          if (chValue.length() != 1)
          {
             return -1;
          }

          if (targetCP == null || targetCP.equalsIgnoreCase(CP_UNDEFINED))
          {
             targetCP = _getCPInternal();
          }

          String targetJavaCP = convmap2Java.get(targetCP);
          if (targetJavaCP == null)
          {
             ErrorManager.recordOrShowError(6063, sourceCP.toUpperCase(), targetCP.toUpperCase(), CONVERSION_MAP_FILE);
             // Code page conversion table for <table-name> to <table-name> was not found in <file-name>. (6063)

             return raiseErrorAndReturn(1587, CONVERSION_MAP_FILE, "", null);
             // ASC could not find the appropriate conversion table in <file-name>. (1587)
          }

          try
          {
             byte[] bytes = chValue.getBytes(targetJavaCP);
             int ret = bytes[0] & 0xFF;
             if (bytes.length > 1)
             {
                ret = (ret << 8) | (bytes[1] & 0xFF);
             }
             if (bytes.length > 2)
             {
                ret = (ret << 8) | (bytes[2] & 0xFF);
             }
             if (bytes.length > 3)
             {
                ret = (ret << 8) | (bytes[3] & 0xFF);
             }
             if (ret == 0x00EFBFBD)
             {
                return 0;
             }
             return ret;
          }
          catch (UnsupportedEncodingException e)
          {
             ErrorManager.recordOrShowError(6063, sourceCP.toUpperCase(), targetCP.toUpperCase(), CONVERSION_MAP_FILE);
             // Code page conversion table for <table-name> to <table-name> was not found in <file-name>. (6063)
             return -1;
          }
       }

       /**
        * This function WILL return value in NO-ERROR mode even if a fatal condition occurred.
        *
        * @param   errCode
        *          The code of the error to be raised.
        * @param   p1
        *          The first parameter.
        * @param   p3
        *          The second parameter.
        *
        * @return  Always -1 when executed in silent mode. Otherwise, will throw an exception and will NOT return.
        */
       private static int raiseErrorAndReturn(int errCode, String p1, String p2, String p3)
       {
          if (ErrorManager.isSilentError())
          {
             ErrorManager.recordOrShowError(errCode, p1, p2);
             return -1;
          }

          ErrorManager.recordOrThrowError(errCode, p1, p2);
          return -2; // actually this point is never reached
       }

       /**
        * Reports the current value of the CPINTERNAL attribute.
        * <p>
        * If the 4GL environment is actually configured for UTF-8, then this approach won't work.
        * If the default codepage name for the JVM is not UTF-8, it is assumed that the codepage
        * has been overriden already (at JVM startup).  In that case, the value reported by the JVM
        * is returned unchanged.
        *
        * @return   The codepage used for internal operations.
        */
       public static character getCPInternal()
       {
          return new character(_getCPInternal());
       }

       /**
        * Reports the JVM charset that maps to the defined CPINTERNAL code page.
        *
        * @return   The JVM corresponding charset or null.
        */
       public static String getJavaCPInternal()
       {
          return convmap2Java.get(_getCPInternal());
       }

       /**
        * Reports the current value of the CPINTERNAL attribute.  String returning version.
        * <p>
        * If the 4GL environment is actually configured for UTF-8, then this approach won't work.
        * If the default codepage name for the JVM is not UTF-8, it is assumed that the codepage
        * has been overridden  already (at JVM startup).  In that case, the value reported by the JVM
        * is returned unchanged.
        *
        * @return   The codepage used for internal operations.
        */
       public static String _getCPInternal()
       {
          WorkArea wa = work.get();

          if (wa.cpInternal == null)
          {
             String cpInternal = SessionUtils._startupParameters().getCpInternal();

             wa.cpInternal = cpInternal == null ? getCharsetNative() : cpInternal;
          }

          return wa.cpInternal;
       }

       /**
        * Reports the default charset used by the JVM.
        *
        * @return   The default charset name.
        */
       public static character getCharset()
       {
          return new character(_getCharset());
       }

       /**
        * Reports the default charset used by the JVM.  String returning version.
        *
        * @return   The default charset name.
        */
       public static String _getCharset()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.charSet == null)
          {
             wa.charSet = _getCPInternal();
          }

          return wa.charSet;
       }

       /**
        * Reports the default charset used by the JVM (Java mapping).
        *
        * @return   The default charset name.
        */
       public static String getJavaCharset()
       {
          return convmap2Java.get(_getCharset());
       }

       /**
        * Reports the JVM charset that maps to the 4GL code page.
        *
        * @param    proCharset
        *           The Progress code page
        *
        * @return   The JVM corresponding charset or null.
        */
       public static String getJavaCharset(character proCharset)
       {
          return getJavaCharset(proCharset.getValue());
       }

       /**
        * Retrieve the java charset name corresponding to 4GL charset name. For example, if the given
        * 4GL charset name is 1252, then the corresponding JVM charset name is WINDOWS-1252.
        * This method is accessed on both the client and the server.
        *
        * @param    proCharset
        *           The given 4GL charset name
        *
        * @return   The corresponding java charset name.
        */
       public static String getJavaCharset(String proCharset)
       {
          return proCharset == null ? null : convmap2Java.get(proCharset);
       }

       /**
        * Reports the codepage used for streams (the equivalent of the CPSTREAM 4GL attribute), which
        * reports the value set using the <code>-cpstream</code> startup parameter.
        *
        * @return   The codepage used for streams.
        */
       public static character getCPStream()
       {
          return new character(_getCPStream());
       }

       /**
        * Reports the codepage used for streams (the equivalent of the CPSTREAM 4GL attribute, which
        * reports the value set using the <code>-cpstream</code> startup parameter.  String returning
        * version.
        *
        * @return   The codepage used for streams.
        */
       public static String _getCPStream()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpStream == null)
          {
             wa.cpStream = _getCPInternal();
          }

          return wa.cpStream;
       }

       /**
        * Gets the value of the SESSION:CPCASE attribute.
        *
        * @return   The current CPCASE attribute for session.
        */
       public static character getCPCase()
       {
          // for now this is the same as CPINTERNAL
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpCase == null)
          {
             wa.cpCase = _getCPInternal();
          }

          return new character(wa.cpCase);
       }

       /**
        * Gets the value of the SESSION:CPCOLL attribute.
        *
        * @return   The current CPCOLL attribute for session.
        */
       public static character getCPColl()
       {
          WorkArea wa = work.obtain();

          if (wa.cpColl == null)
          {
             wa.cpColl = "Basic"; // all string compare operations are done using the native Java String
          }

          return new character(wa.cpColl);
       }

       /**
        * Gets the value of the SESSION:CPLOG attribute.
        *
        * @return   The current CPLOG attribute for session.
        */
       public static character getCPLog()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpLog == null)
          {
             wa.cpLog = _getCPInternal();
          }

          return new character(wa.cpLog);
       }

       /**
        * Gets the value of the SESSION:CPPRINT attribute.
        *
        * @return   The current CPPRINT attribute for session.
        */
       public static character getCPPrint()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpPrint == null)
          {
             wa.cpPrint = _getCPInternal();
          }

          return new character(wa.cpPrint);
       }

       /**
        * Gets the value of the SESSION:CPRCODEIN attribute.
        *
        * @return   The current CPRCODEIN attribute for session.
        */
       public static character getCPRCodeIn()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpRCodeIn == null)
          {
             wa.cpRCodeIn = _getCPInternal();
          }

          return new character(wa.cpRCodeIn);
       }

       /**
        * Gets the value of the SESSION:CPRCODEOUT attribute.
        *
        * @return   The current CPRCODEOUT attribute for session.
        */
       public static character getCPRCodeOut()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpRCodeOut == null)
          {
             wa.cpRCodeOut = _getCPInternal();
          }

          return new character(wa.cpRCodeOut);
       }

       /**
        * Gets the value of the SESSION:CPTERM attribute.
        *
        * @return   The current CPTERM attribute for session.
        */
       public static character getCPTerm()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.cpTerm == null)
          {
             wa.cpTerm = _getCPInternal();
          }

          return new character(wa.cpTerm);
       }

       /**
        * Reports the codepage used for r-code file systems handle.
        *
        * @return   The codepage used for r-code file.
        */
       public static character getCodePage()
       {
          WorkArea wa = work.obtain();

          // if override was not defined in directory.xml use current CPINTERNAL
          if (wa.codePage == null)
          {
             wa.codePage = _getCPInternal();
          }

          return new character(wa.codePage);
       }

       /**
        * Get word delimiter pattern.
        *
        * @param   codePage
        *          codePage (character encoding) used.
        *
        * @return  a regex pattern of word delimiters based on a codepage.
        */
       public static Pattern getDelimiterPattern(String codePage)
       {
          return delimiterPatterns.computeIfAbsent(getWordDelimiters(codePage), Pattern::compile);
       }

       /**
        * A regex list of word delimiters based on a codePage.
        * If the delimiters for the specified {@code codePage} was not found,
        * the delimiters for ISO8859-1 is returned as default.
        *
        * @param codePage
        *        codePage (character encoding) used.
        *
        * @return a regex list of word delimiters based on a codepage.
        */
       public static String getWordDelimiters(String codePage)
       {
          if (codePage == null)
          {
             codePage = _getCPInternal();
          }
          String delimiters = cp2Delimiters.get(codePage.toUpperCase());
          if (delimiters == null)
          {
             delimiters = cp2Delimiters.get("ISO8859-1");
          }
          return delimiters;
       }

       /**
        * Analyze the first bytes of the {@code data} stream for BOM markers. If one is detected, the appropriate
        * charset is ued to decode it and the resulting string is returned. If no BOM is detected, the default
        * charset is used.
        *
        * @param   data
        *          The data to be decoded.
        * @param   offset
        *          the offset where the returned string starts
        * @param   len
        *          The number of bytes to be processed. This might not be the actual size of the returned string
        *          because of BOM or multibyte character encoding.
        *
        * @return  A string containing in {@code data} bytes, decoded automatically with detected charset.
        */
       public static String getStringWithBomCheck(byte[] data, int offset, int len)
       {
          // check the BOM UTF-8: 0xEF_BB_BF
          if (data.length >= 3                  &&
              (0xFF & data[offset]) == 0xEF     &&
              (0xFF & data[offset + 1]) == 0xBB &&
              (0xFF & data[offset + 2]) == 0xBF)
          {
             return new String(data, offset + 3, len - 3, StandardCharsets.UTF_8);
          }
          // check the UTF-16: 0xFE_FF
          else if (data.length >= 2              &&
                   (0xFF & data[offset]) == 0xEF &&
                   (0xFF & data[offset + 1]) == 0xFF)
          {
             return new String(data, offset + 2, len - 2, StandardCharsets.UTF_16);
          }
          // else : we have no information, take it as provided
          else
          {
             String cpInternal = _getCPInternal();
             if (!prevalidateData(data, offset, len, cpInternal))
             {
                ErrorManager.recordOrThrowError(12008, cpInternal.toUpperCase(), "");
                // Invalid character code found in data for codepage  (12008)
                return null;
             }

             try
             {
                return new String(data, offset, len, convmap2Java.get(cpInternal));
             }
             catch (UnsupportedEncodingException e)
             {
                ErrorManager.recordOrThrowError(912, cpInternal, I18nOps.CONVERSION_MAP_FILE);
                // Code page attribute table for <code-page> was not found in <filename>.
                return null;
             }
          }
       }

       /**
        * Prevalidate the byte array before allowing the Java charset to decode it to a string. We do this because
        * the Java character decoder is more permissible than OE counterpart.
        *
        * @param   data
        *          The byte array to be validated.
        * @param   offset
        *          the offset where the returned string starts
        * @param   len
        *          The number of bytes to be processed. This might not be the actual size of the returned string
        *          because of BOM or multibyte character encoding.
        * @param   oecp
        *          The original CP to be used in validation (only used for displaying OE errors).
        *          Not Java codepage.
        *
        * @return  {@code true} if the data passed prevalidation. Otherwise {@code false} is returned.
        */
       public static boolean prevalidateData(byte[] data, int offset, int len, String oecp)
       {
          // basic validation: in OE's single byte codepages, null characters ('\0') are not allowed
          String cpUpCase = oecp != null && !oecp.isEmpty() ? oecp.toUpperCase() : CP_UNDEFINED;
          if (zeroByteCheck.contains(cpUpCase))
          {
             for (int i = offset; i < len; i++)
             {
                if (data[i] == 0)
                {
                   return false;
                }
             }
          }

          return true;
       }

       /**
        * Initializes supplied class (aka referent class) for the purpose of i18n. The method returns a unique
        * identifier associated with the supplied referent class. Any further i18n requests must contain this
        * unique id.
        * By convention a referent class has associated set of resource bundles, one for each target language.
        * The resource bundles reside in the same Java package as the referent class, whereas the resource
        * bundle class name is in the form [referent class simple name]_[target language].
        * The current target language is determined by the assigned value to {@code CURRENT-LANGUAGE@} session
        * variable.
        *
        * @param   referentClass
        *          The referent class.
        *
        * @return  Unique id of to the initialized referent class. Any further i18n requests must contain this
        *          id.
        */
       public static long initReferent(Class<?> referentClass)
       {
          return TranslationManager.getInstance().initReferent(referentClass);
       }

       /**
        * Reports the current value of the JVM charset properly mapped to valid 4GL code page.
        * <p>
        * Always return valid 4GL compatible charset or UNKNOWN one.
        *
        * @return   The codepage used for internal operations as String.
        */
       private static String getCharsetNative()
       {
          String javaCharset = Utils.getCharsetName();

          // getting Progress name from respective map
          String cpRes = convmap2Progress.get(javaCharset);

          // this is practically impossible but to have NPE protection
          if (cpRes == null || cpRes.isEmpty())
          {
             cpRes = CP_UNDEFINED;
          }

          return cpRes;
       }

       /**
        * Helper to convert the character code from its integer value into string in Java native representation.
        *
        * @param    charCode
        *           The integer to convert.
        *
        * @return   The one-character {@code String} containing the character whose code was sent as parameter.
        */
       private static String getNativeCharacter(int charCode, Charset charset)
       {
          byte[] bytes;

          if (charset == StandardCharsets.UTF_8)
          {
             // see the table in https://en.wikipedia.org/wiki/UTF-8#Encoding for theoretical part
             if (charCode <= 0x7F)
             {
                bytes = new byte[] {(byte) (charCode & 0b0_1111111) };
             }
             else if (charCode <= 0x7FF)
             {
                bytes = new byte[] {
                      (byte) (0b110_00000 | ((charCode >>>  6) & 0b000_11111)),
                      (byte) (0b10_000000 | ( charCode         & 0b00_111111))
                };
             }
             else if (charCode <= 0xFFFF)
             {
                bytes = new byte[] {
                      (byte) (0b1110_0000 | ((charCode >>> 12) & 0b0000_1111)),
                      (byte) (0b10_000000 | ((charCode >>>  6) & 0b00_111111)),
                      (byte) (0b10_000000 | ( charCode         & 0b00_111111))
                };
             }
             else if (charCode <= 0x10FFFF)
             {
                bytes = new byte[] {
                      (byte) (0b11110_000 | ((charCode >>> 18) & 0b00000_111)),
                      (byte) (0b10_000000 | ((charCode >>> 12) & 0b00_111111)),
                      (byte) (0b10_000000 | ((charCode >>>  6) & 0b00_111111)),
                      (byte) (0b10_000000 | ( charCode         & 0b00_111111))
                };
             }
             else
             {
                return ""; // not supported
             }
          }
          // TODO: the UTF16 might also need variable byte encoding.
          // NOTE: UTF16 comes in two flavours: are to endianness, see below
          else if (charset == StandardCharsets.UTF_16 || charset == StandardCharsets.UTF_16BE)
          {
             // constant byte distribution
             if ((charCode >>> 24) > 0)
             {
                // must check charset, this is a big-endian code, ex: StandardCharsets.UTF_16BE
                bytes = new byte[]
                {
                   (byte) (charCode >>> 24),
                   (byte) (charCode >>> 16),
                   (byte) (charCode >>> 8),
                   (byte) charCode
                };
             }
             else if ((charCode >> 16) > 0)
             {
                bytes = new byte[]{(byte) (charCode >>> 16), (byte) (charCode >>> 8), (byte) charCode};
             }
             else if ((charCode >> 8) > 0)
             {
                bytes = new byte[]{(byte) (charCode >>> 8), (byte) charCode};
             }
             else
             {
                bytes = new byte[]{(byte) charCode};
             }
          }
          else if (charCode < 0x100) // 7 or 8 bit encoding
          {
             if (charCode > 0x7F)
             {
                charCode -= 0x100;
             }
             bytes = new byte[] { (byte) charCode };
          }
          else
          {
             throw new RuntimeException("Do not know how to get the character " + charCode + " using " + charset);
          }
          return new String(bytes, charset);
       }

       /**
        * Stores global data relating to the state of the current context.
        */
       private static class WorkArea
       {
          /** The local or remote directory service to be used on both client and server sides. */
          private final Directory dir = DirectoryManager.getInstance();

          /** Stores any context-specific effective value for the <code>CPINTERNAL</code>. */
          private String cpInternal = null;

          /** Stores any context-specific effective value for the <code>CPSTREAM</code>. */
          private String cpStream = SessionUtils._startupParameters().getCpStream();

          /** Stores any context-specific effective value for the <code>CPCASE</code>. */
          private String cpCase = SessionUtils._startupParameters().getCpCase();

          /** Stores any context-specific effective value for the <code>CPCOLL</code>. */
          private String cpColl = SessionUtils._startupParameters().getCpColl();

          /** Stores any context-specific effective value for the <code>CPLOG</code>. */
          private String cpLog = SessionUtils._startupParameters().getCpLog();

          /** Stores any context-specific effective value for the <code>CPPRINT</code>. */
          private String cpPrint = SessionUtils._startupParameters().getCpPrint();

          /** Stores any context-specific effective value for the <code>CPRCODEIN</code>. */
          private String cpRCodeIn = SessionUtils._startupParameters().getCpRCodeIn();

          /** Stores any context-specific effective value for the <code>CPRCODEOUT</code>. */
          private String cpRCodeOut = SessionUtils._startupParameters().getCpRCodeOut();

          /** Stores any context-specific effective value for the <code>CPTERM</code>. */
          private String cpTerm = SessionUtils._startupParameters().getCpTerm();

          /** Stores any context-specific effective value for the <code>CHARSET</code>. */
          private String charSet = SessionUtils._startupParameters().getCharset();

          /** Stores any context-specific effective value for the <code>RCODE-INFO:CODEPAGE</code>. */
          private String codePage = getOverrideFromDirectory("i18n/codepage");

          /**
           * Requests the server for code page related option available to be overridden in
           * directory.xml file.  All CP options can be overridden.
           *
           * @return   The codepage related override from directory or <code>null</code> if none.
           */
          private String getOverrideFromDirectory(String optName)
          {
             return dir.getString(Directory.ID_RELATIVE_BOTH, optName, null);
          }
       }

       /**
        * Simple container that stores and returns a context-local instance of
        * the global work area.
        */
       private static class ContextContainer
       extends ContextLocal<WorkArea>
       {
          /**
           * Obtains the context-local instance of the contained global work
           * area.
           *
           * @return   The work area associated with this context.
           */
          public WorkArea obtain()
          {
             return this.get();
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context.
           *
           * @return   The newly instantiated work area.
           */
          protected synchronized WorkArea initialValue()
          {
             WorkArea wa = new WorkArea();

             return wa;
          }
       }
    }