# FileScope.java

    /*
    ** Module   : FileScope.java
    ** Abstract : Represents the data associated with Progress files from
    **            the preprocessor perspective, such as file name and arguments.
    **
    ** Copyright (c) 2004-2019, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------Description----------------------------
    ** 001 NVS 20041109   @18763 Created. Initial implementation of a class
    **                           that would represent scope payload objects in
    **                           the preprocessor dictionary.
    ** 002 NVS 20050207   @19631 This new constructor takes the Progress
    **                           arguments as a string and internally parses
    **                           them into a Map of arguments using a virtual
    **                           include file techninue for a special file
    **                           name '.', which can't be an include file
    **                           reference anyway.
    ** 003 NVS 20050207   @19632 Changed the pushback buffer depths to 4096.
    **                           1024 was not enough in one of the files with
    **                           a dynamic RUN statement.
    ** 004 NVS 20050211   @19769 Replaced the new constructor introduced with
    **                           change #002 with one that takes a filename
    **                           instead of input stream, which makes no sense
    ** 005 NVS 20050221   @19901 Reworked the constructor again. This time,
    **                           the string of arguments gets tokenized by
    **                           a new instance of TextLexer as multiple
    **                           strings representing positional arguments.
    **                           The change was due to the inability of the
    **                           regular include file syntax to represent the
    **                           empty values.
    ** 006 NVS 20050512   @21159 Changes forced by changed ClearStream
    **                           constructor. Added fields to track file
    **                           argument usage.
    ** 007 GES 20070313   @32395 Code formatting cleanup.
    ** 008 GES 20070403   @32710 Simplified the use of default options.
    ** 009 GES 20071120   @36194 Added the deferred pushback member.
    ** 010 GES 20071205   @36215 Added full support for referencing a
    **                           duplicate named argument via its positional
    **                           name and via {*}. See normalizeArguments().
    ** 011 GES 20080308   @37648 Code cleanup, removal of unneeded code and
    **                           switch to a reader approach for I18N.
    ** 012 ECF 20101001          Added case-insensitive file search to open()
    **                           method. Necessary for files which originate on a
    **                           case-insensitive file system when processing them
    **                           on a case-sensitive file system.
    ** 013 GES 20110623          Search entire project for a relative match to an
    **                           include file when the propath search fails. This
    **                           is known as the fallback search. This will make
    **                           the project more tolerant of propath cfg issues.
    ** 014 GES 20110901          Move to standard warning mechanism.
    ** 015 OM  20130607          Fixed com.goldencode.p2j.util / Hibernate import conflict.
    ** 016 CA  20140313          getFile and getFileName need to report the resolved, project-home
    **                           relative name.
    ** 017 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 018 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 019 HC  20160802          Fixed handling of file separators when including files during
    **                           preprocessing.
    ** 020 HC  20170110          Fixed a name conflict with antlr.
    ** 021 HC  20170708          Added source-charset uast preprocessor hint.
    ** 022 CA  20180410          Some java generic fixes.  The allKeyArgs string must double-quote any
    **                           quote char inside the argument's value, as the structure is
    **                           key="val".
    ** 023 HC  20190227          Fixed hints files not loaded by the preprocessor.
    ** 024 ECF 20190619          Added include file reference prefix remapping.
    */
    /*
    ** 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.preproc;

    import antlr.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.Utils;

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

    /**
     * Represents the data associated with Progress files from the preprocessor
     * perspective, such as file name and arguments. An instance is created for
     * top level source file (external procedure) as well as for every include
     * file that is referenced.
     * <p>
     * Instances of this class serve scoping levels in a dictionary of scopes.
     * <p>
     * Preprocessor arguments are stored as mapped data. Both positional and
     * named arguments have names. These names are used as map keys. To avoid
     * key clashes between positional and named arguments, the names for
     * positional arguments are chosen to have a form that is never possible with
     * named arguments. Those names are <code>"{N"</code> where N is the argument
     * position (from left to right). Simply using names like <code>"N"</code>
     * would be insufficient as those can be redefined like this:
     * <code>{file &amp;1=value}</code> etc. So, whenever a reference like
     * <code>{1}</code> is encountered, it is mapped into the key
     * <code>"{1"</code>.
     * <p>
     * At the time of the instance construction, the special Progress references
     * <code>{*}</code> and <code>{&amp;*}</code> are also derived.
     */
    public class FileScope
    {
       /**
        * Saved line counter. The line number of the last include file reference
        * from this file.
        */
       int savedLine = 0;

       /**
        * Saved column counter. The column number of the last include file
        * reference from this file.
        */
       int savedColumn = 0;

       /**
        * Name of the file to preprocess, exactly as specified. Corresponds to
        * Progress' <code>{0}</code> argument reference.
        */
       private String fileName = null;

       /** Reader connected to this file. */
       private Reader input = null;

       /** Progress arguments to this file. */
       private Map<String, String> args = null;

       /** All positional arguments for Progress' <code>{*}</code> reference. */
       private String allPosArgs = "";

       /** All named arguments for Progress' <code>{&amp;*}</code> reference. */
       private String allKeyArgs = "";

       /** Names of referenced arguments. */
       private Set<String> usedArgs = new HashSet<>();

       /** <code>true</code> when {*} reference is used. */
       private boolean usedAllPos = false;

       /** <code>true</code> when {&amp;*} reference is used. */
       private boolean usedAllNamed = false;

       /** Text to be pushed back onto the stream after this scope is over. */
       private String deferred = null;

       /** Name of the file found during the fallback search. */
       private String fallbackName = null;

       /** Track which c'tor was used. */
       private boolean explicitInput = false;

       /** The charset used for reading input streams. */
       private Charset charset = Charset.defaultCharset();

       /**
        * Wrap an existing input stream for a scope that needs no arguments.
        * <p>
        * Typically this is only used for the top level source file.
        *
        * @param    fname
        *           File name of this scope.
        * @param    input
        *           The reader to use as the input source.
        */
       public FileScope(String fname, Reader input)
       {
          // save the input stream
          this.input = input;

          // create input file representations
          fileName = fname;

          explicitInput = true;
       }

       /**
        * Constructor for an input file with arguments. Typically used for
        * include files.
        * <p>
        * File separators of <code>fname</code> are replaced to match the
        * Java runtime's file separators. To recognize file separators of the
        * original <code>fname</code> value, the passed in <code>options</code>
        * is inspected.
        * <p>
        * If being called for an included file, any configured filename prefix
        * remappings are performed on the name at this time.
        * <p>
        * Positional arguments are those named in a special way so the names
        * never clash: <code>"{N"</code> where N is the relative position from
        * left to right (starting at 1).
        * <p>
        * Named arguments have their regular names as specified in the source
        * code of an include file reference.
        * <p>
        * The first argument in the sequence defines the form of the whole list.
        * If its name is <code>"{1"</code>, the list is considered positional,
        * otherwise it is named.
        * <p>
        * A positional list is normalized to only have <code>"{N"</code> names.
        * A named list is normalized to only have the original regular names plus
        * newly assigned positional names in the form <code>"{N"</code>. This
        * enables named arguments to be referenced by a positional reference,
        * which is valid in Progress.
        * <p>
        * To guarantee that the order of the arguments as they appear in the
        * <code>{*}</code> and <code>{&amp;*}</code> references is preserved, the
        * passed parameter must be an instance of <code>LinkedHashMap</code>.
        *
        * @param    fname
        *           Source file to be preprocessed. Any <code>UNIX</code> style
        *           (<code>"/"</code>) file separator characters will be replaced
        *           with the platform-specific separator character IF DIFFERENT.
        * @param    args
        *           Arguments.
        * @param    options
        *           The preprocessor options, must not be <code>null</code>.
        * @param    include
        *           {@code true} if this object is to be used for an include file reference,
        *           else {@code false}.
        */
       public FileScope(String fname, Map<String, String> args, Options options, boolean include)
       {
          // create input file representations,
          // 4GL on Windows allows \ and / as file separators
          if ('/' != File.separatorChar)
          {
             fname = fname.replace('/', File.separatorChar);
          }

          // 4GL on Linux/Unix only allows /
          if ('\\' != File.separatorChar && !options.isUnixEscapes())
          {
             fname = fname.replace('\\', File.separatorChar);
          }

          // apply any include file prefix remappings

          Map<String, String> pfxMap = include ? options.getIncludeFilePrefixMap() : null;

          if (pfxMap != null && !pfxMap.isEmpty())
          {
             // check for include file prefix that needs to be replaced/removed;
             // note that this will change the fileName instance variable if a prefix is matched

             String fileSep = options.getFileSeparator();
             boolean caseSens = options.isCaseSensitive();
             String fn = caseSens ? fname : fname.toLowerCase();
             fn = Utils.replaceSeparator(fn, File.separator, fileSep);

             for (Map.Entry<String, String> e : pfxMap.entrySet())
             {
                String prefix = e.getKey();
                if (!caseSens)
                {
                   prefix = prefix.toLowerCase();
                }
                prefix = Utils.replaceSeparator(prefix, File.separator, fileSep);

                if (fn.startsWith(prefix))
                {
                   fname = e.getValue() + fname.substring(prefix.length());

                   // first match wins
                   break;
                }
             }
          }

          fileName = Configuration.normalizeFilename(fname);

          // normalize the arguments
          normalizeArguments(args);
       }

       /**
        * Returns the filename as was specified at creation.
        *
        * @return  file name used to construct this object.
        */
       public String getFileName()
       {
          return fileName;
       }

       /**
        * Returns the filename that was found during the fallback attempt.
        *
        * @return   The fallback filename that was dynamically found or
        *           <code>null</code> if no fallback was attempted or if the
        *           fallback attempt was not successfull.
        */
       public String getFallbackName()
       {
          return fallbackName;
       }

       /**
        * Returns the embedded File object.
        *
        * @return  <code>File</code> object for this Progress source file.
        */
       public File getFile()
       {
          return new File(explicitInput ? "." : fileName);
       }

       /**
        * Returns the opened stream.
        *
        * @return  Input source or <code>null</code> if closed or never opened.
        */
       public Reader getStream()
       {
          return input;
       }

       /**
        * Returns Progress arguments.
        *
        * @return  set of (name, value) pairs representing Progress arguments
        *          organized into a <code>Map</code>.
        *          <code>null</code> if no arguments were given.
        */
       public Map<String, String> getArguments()
       {
          return args;
       }

       /**
        * Copies named Progress arguments into the specified dictionary
        * as symbols.
        *
        * @param   dictionary
        *          symbol dictionary to which the named Progress arguments are
        *          to be copied
        * @return  number of named Progress arguments copied.
        */
       public int copyNamedArguments(ScopedSymbolDictionary dictionary)
       {
          if (args == null)
             return 0;

          Iterator<Map.Entry<String, String>>  mi = args.entrySet().iterator();
          Map.Entry<String, String> me = null;
          String key = null;
          String val = null;

          while (mi.hasNext())
          {
             me  = mi.next();
             key = me.getKey();
             val = me.getValue();
             dictionary.addSymbol(false, key, new Symbol(Symbol.ARGUMENT, val));
          }

          return args.size();
       }

       /**
        * Returns a string representing all positional Progress arguments
        * matching the Progress' <code>{*}</code> reference.
        *
        * @return  all positional arguments concatenated into a single string.
        */
       public String getAllPositionalArguments()
       {
          return allPosArgs;
       }

       /**
        * Returns a string representing all named Progress arguments
        * matching the Progress' <code>{&amp;*}</code> reference.
        *
        * @return  all named arguments concatenated into a single string
        *          in the form &amp;name="value".
        */
       public String getAllNamedArguments()
       {
          return allKeyArgs;
       }

       /**
        * Resolves the absolute name of the previously specified (relative) file name.
        *
        * @param    paths
        *           array of strings specifying paths to search for the file
        *           or <code>null</code>.
        * @param    env
        *           Preprocessor environment.
        *
        * @return   <code>true</code> if open succeeded.
        */
       public String resolveFileName(String[] paths, Environment env)
       {
          String fullname = null;

          boolean hit = false;

          try
          {
             String fileSep = env.getOpt().getFileSeparator();

             File file = new File(fileName);

             // locate the file using given paths
             if (file.isAbsolute() || paths == null)
             {
                fullname = Utils.canonicalizePath(fileName, File.separator, fileSep);
                if ((new File(fullname)).canRead())
                {
                   hit = true;
                }
             }
             else
             {
                for (int i = 0; !hit && i < paths.length; i ++)
                {
                   StringBuilder sb = new StringBuilder(paths[i]);
                   sb.append(File.separator).append(fileName);
                   String path = sb.toString();
                   fullname = Utils.canonicalizePath(path, File.separator, fileSep);

                   if ((new File(fullname)).canRead())
                   {
                      hit = true;
                   }
                }
             }
          }
          catch (IOException exc)
          {
             CharScanner lex = env.getLex();
             env.eprint("Error canonicalizing filename '" +
                        fileName +
                        "';  " +
                        exc.getMessage(),
                        lex.getLine(), lex.getColumn());
          }

          // special processing for files originating on a case-insensitive file
          // system, when processing them on a case-sensitive file system
          if (!hit && !env.getOpt().isCaseSensitive())
          {
             File file = new File(fileName);

             // locate the file using given paths
             if (file.isAbsolute() || paths == null)
             {
                fullname = fileName;
                File fileci = findCaseInsensitively(fullname, env);
                if (fileci != null && fileci.canRead())
                {
                   fullname = fileci.getAbsolutePath();
                   hit = true;
                }
             }
             else
             {
                for (int i = 0; !hit && i < paths.length; i ++)
                {
                   StringBuilder sb = new StringBuilder(paths[i]);
                   sb.append(File.separator).append(fileName);
                   fullname = sb.toString();
                   File fileci = findCaseInsensitively(fullname, env);
                   if (fileci != null && fileci.canRead())
                   {
                      fullname = fileci.getAbsolutePath();
                      hit = true;
                   }
                }
             }
          }

          return hit ? fullname : null;
       }

       /**
        * Opens the input source using the previously specified file.
        *
        * @param    paths
        *           array of strings specifying paths to search for the file
        *           or <code>null</code>.
        * @param    env
        *           Preprocessor environment.
        *
        * @return   <code>true</code> if open succeeded.
        */
       public boolean open(String[] paths, Environment env)
       {
          // avoid repeated attempts
          if (input != null)
          {
             return true;
          }

          String fullname = resolveFileName(paths, env);

          if (fullname != null)
          {
             // re-adjust the name to the resolved one
             fileName = Configuration.normalizeFilename(fullname);

             try
             {
                input = new BufferedReader(getFileReader(fullname));
             }
             catch (FileNotFoundException exc)
             {
                return false;
             }
          }

          return fullname != null;
       }

       /**
        * Attempt a fallback process to find the file and then open the input
        * source using the found filename.  This is only used when the normal
        * <code>open()</code> method fails. Only a unique match will be accepted.
        * The search algorithm walks all files under the basepath and if one and
        * only one file is found that ends with the filename we are searching
        * for, then that match is accepted.
        *
        * @param    env
        *           Preprocessor environment.
        *
        * @return   <code>true</code> if open succeeded.
        */
       public boolean attemptFallback(Environment env)
       {
          Options  options = env.getOpt();
          String[] list    = options.getFallbackList();

          StringBuilder sb = new StringBuilder();
          // prepend the file sep so that only complete path segments are matched
          sb.append(File.separator).append(fileName);
          String target = sb.toString();

          if (!options.isCaseSensitive())
             target = target.toLowerCase();

          int    matches = 0;
          String found   = null;

          for (int i = 0; i < list.length && matches < 2; i++)
          {
             if (list[i].endsWith(target))
             {
                matches++;

                // only ever use the first match
                if (matches == 1)
                   found = list[i];
             }
          }

          // only accept a unique match, otherwise this fails
          if (matches == 1)
          {
             try
             {
                input = new BufferedReader(getFileReader(found));
                fallbackName = found;
             }
             catch (FileNotFoundException exc)
             {
                return false;
             }
          }

          return (matches == 1);
       }

       /**
        * Closes the input reader.
        */
       public void close()
       {
          if (input == null)
             return;

          try
          {
             input.close();
             input = null;
          }
          catch (IOException e)
          {
             // ignore
          }
       }

       /**
        * Saves the line counter.
        *
        * @param  savedLine
        *         line number to be saved. Normally stores the current
        *         line number within this file while an include file
        *         reference is being processed.
        */
       public void setSavedLine(int savedLine)
       {
          this.savedLine = savedLine;
       }

       /**
        * Gets the saved line counter.
        *
        * @return  saved line counter.
        */
       public int getSavedLine()
       {
          return savedLine;
       }

       /**
        * Saves the column counter.
        *
        * @param  savedColumn
        *         column number to be saved. Normally stores the current
        *         column number within this file while an include file
        *         reference is being processed.
        */
       public void setSavedColumn(int savedColumn)
       {
          this.savedColumn = savedColumn;
       }

       /**
        * Gets the saved column counter.
        *
        * @return  saved column counter.
        */
       public int getSavedColumn()
       {
          return savedColumn;
       }

       /**
        * Marks argument as used.
        *
        * @param name
        *        argument name
        */
       public void setArgUsed(String name)
       {
          usedArgs.add(name);
       }

       /**
        * Checks if the argument was used.
        *
        * @param name
        *        argument name
        * @return <code>true</code> if the argument was used
        */
       public boolean isArgUsed(String name)
       {
          return usedArgs.contains(name);
       }

       /**
        * Marks all positional arguments as used.
        */
       public void setAllPosUsed()
       {
          usedAllPos = true;
       }

       /**
        * Returns "all positional arguments used" flag.
        */
       public boolean isAllPosUsed()
       {
          return usedAllPos;
       }

       /**
        * Marks all named arguments as used.
        */
       public void setAllNamedUsed()
       {
          usedAllNamed = true;
       }

       /**
        * Returns "all named arguments used" flag.
        */
       public boolean isAllNamedUsed()
       {
          return usedAllNamed;
       }

       /**
        * Obtain any text that must be pushed back onto the stream after this
        * scope is complete.
        *
        * @return   Deferred pushback text.
        */
       public String getDeferredPushback()
       {
          return deferred;
       }

       /**
        * Set any text that must be pushed back onto the stream after this
        * scope is complete.
        *
        * @param    deferred
        *           Deferred pushback text.
        */
       public void setDeferredPushback(String deferred)
       {
          this.deferred = deferred;
       }

       /**
        * Sets the charset used for reading input streams.
        *
        * @param   charset
        *          If null, the default JVM charset will be set to the instance.
        */
       public void setCharset(Charset charset)
       {
          this.charset = charset == null ? Charset.defaultCharset() : charset;
       }

       /**
        * Converts this object into a string.
        *
        * @return  a string representation of this object useful for
        *          diagnostic output.
        */
       public String toString()
       {
          return new String("#" + savedLine + ":" + savedColumn +
                            fileName + "{" + allPosArgs + "}{" +
                            allKeyArgs + "}");
       }

       /**
        * Find a file on the file system using a case insensitive matching
        * algorithm.  Warn (but do not fail) if more than one match is found
        * (possible when finding case-insensitively on a case-sensitive file
        * system).  The order in which files are matched is dependent upon the
        * default order in which underlying file system lists files.
        *
        * @param   filename
        *          Name of file to be found;  case of file name and qualifying
        *          path need not match the case of matching file(s) on the file
        *          system.
        * @param   env
        *          Preprocessor environment (used to emit error/warning messages).
        *
        * @return  The first file whose name is matched case-insensitively to the
        *          given <code>filename</code>, or <code>null</code> if no match
        *          is found.
        */
       private File findCaseInsensitively(String filename, Environment env)
       {
          try
          {
             String fileSep = env.getOpt().getFileSeparator();
             List<File> list =
                com.goldencode.p2j.util.Utils.getCaseInsensitiveFilenameMatches(filename, fileSep);

             int matches = list.size();
             if (matches >= 1)
             {
                // emit warning if we had more than one match
                if (matches > 1)
                {
                   CharScanner lex = env.getLex();
                   String      msg = String.format("More than one match for file " +
                                                   "'%s'; only first will be used" +
                                                   " (%s);",
                                                   filename,
                                                   list);
                   env.wprint(msg,
                              lex == null ? -1 : lex.getLine(),
                              lex == null ? -1 : lex.getColumn());
                }

                // if we found it, it exists;  no need for an additional check,
                // just return it
                return list.get(0);
             }
          }
          catch (IOException exc)
          {
             CharScanner lex = env.getLex();
             env.eprint("Error searching for include file \"" +
                        filename + "\":" + exc.getMessage(),
                        lex.getLine(), lex.getColumn());
          }

          return null;
       }

       /**
        * Normalizes the naming of arguments in the given map and store them.
        * Create the all positional and all named argument strings.
        * <p>
        * A positional list is normalized to only have names in the form
        * <code>"{N"</code>. Consider this include reference:
        * <p>
        * <pre>
        *    {ti.i p1 p2 &amp;a="c" p3 &amp;b=d p4}
        * </pre>
        * <p>
        * In this example, the result would be normalized this way:
        * <p>
        * <pre>
        * Name     Value
        * -------  -------
        * {1       p1
        * {2       p2
        * {3       a="c"
        * {4       p3
        * {5       b="d"
        * {6       p4
        * </pre>
        * <p>
        * The corresponding <code>{*}</code> reference would be
        * <code>"p1 p2 c p3 d p4"</code> and <code>{&amp;*}</code> is empty.
        * <p>
        * A named list is normalized to only have the original regular
        * names plus one newly assigned corresponding <code>"{N"</code> name for
        * each argument (including any duplicate named argument). This allows the
        * argument values to be referenced by either their names or by their
        * relative position such as <code>{&amp;1}</code> for the first named
        * argument.  Note that duplicate named arguments (arguments where the
        * specified name appears more than once in the same argument list) are
        * handled as follows:
        * <p>
        * <ol>
        *    <li> the value of the first named instance is what is returned when
        *         the name is referenced (the duplicate value is dropped from the
        *         list of named arguments)
        *    <li> all instances can be referenced by their positional name (even
        *         the duplicated name)
        *    <li> the {*} reference will include all values (even the values for
        *         any duplicated name)
        * </ol>
        * <p>
        * For example:
        * <p>
        * <pre>
        *   {ti.i &amp;a="c" p1 p2 p3 &amp;b=d p4}
        * </pre>
        * <p>
        * This would produce:
        * <p>
        * <pre>
        * Name     Value
        * -------  -------
        * a        c
        * b        d
        * {1       c
        * {2       d
        * </pre>
        * <p>
        * The corresponding <code>{&amp;*}</code> reference is
        * <code>"&amp;a=c &amp;b=d"</code> and <code>{*}</code> reference is
        * <code>"c d"</code>.
        * <p>
        * To guarantee that the order of the arguments as they appear in the
        * <code>{*}</code> and <code>{&amp;*}</code> references is preserved, the
        * passed parameter must be an instance of <code>LinkedHashMap</code>.
        *
        * @param   args
        *          Include file arguments list. This list will store either named
        *          or positional arguments. Positional arguments are those named
        *          so the names never clash: "{N" where n is the argument number.
        *          Named arguments have their explicitly provided name without
        *          the ampersand. It is not valid to have both except in the
        *          special case where there is a duplicate named argument. In
        *          that case there will be a positional argument placeholder so
        *          that the full positional argument list and the all arguments
        *          list will contain the value of the duplicated name even though
        *          the matching named argument never had its value overridden.
        *          The first argument in the sequence defines the form of the
        *          whole list. If its name is {1, the list is considered
        *          positional, otherwise named.
        */
       private void normalizeArguments(Map<String, String> args)
       {
          if (args == null)
             return;

          StringBuilder allpos = new StringBuilder();
          StringBuilder allkey = new StringBuilder();

          // create a normalized copy of arguments
          this.args = new HashMap<>();
          Iterator<Map.Entry<String, String>>  mi = args.entrySet().iterator();
          Map.Entry<String, String> me = null;
          String key = null;
          String val = null;
          int n = 0;
          int m = 0;
          boolean named = false;

          while (mi.hasNext())
          {
             boolean duplicate = false;

             // fetch next pair
             me  = mi.next();
             n++;
             key = me.getKey();
             val = me.getValue();

             if (val == null)
             {
                val = "";
             }

             // determine whether positional or named
             if (n == 1)
             {
                named = !key.startsWith("{");
             }

             // normalize arguments
             if (!named)
             {
                // rename positional arguments into "{n"
                if (!key.startsWith("{"))
                   key = new String("{" + n);
             }
             else
             {
                // detect the duplicate argument placeholder provided by the
                // caller
                if (key.startsWith("{"))
                {
                   duplicate = true;
                }

                m++;
             }

              // insert normalized pair into the table
             this.args.put(key, val);

             if (named && !duplicate)
             {
                // add the manufactured positional name unless this is a duplicate
                // placeholder (in which case the code above has already handled
                // this)
                this.args.put("{" + m, val);

                // contribute to the {&*} string
                if (allkey.length() > 0)
                {
                   allkey.append(" ");
                }

                allkey.append("&")
                      .append(key)
                      .append("=\"")
                      .append(val.replaceAll("\"", "\"\""))
                      .append("\"");
             }

             // contribute to the {*} string
             if (allpos.length() > 0)
             {
                // this must not be quoted.  4GL will expand {*} with the actual values, separated by
                // spaces; thus, the {*} at the caller may be different from the {*} at the callee,
                // i.e. {some-include.i {*} } and some-include.i's {*} will have different semantics
                // and may not match by argument position and value, if any value contains spaces.
                allpos.append(" ");
             }
             allpos.append(val);
          }

          // create {*} and {&*} references
          allPosArgs = allpos.toString();
          allKeyArgs = allkey.toString();
       }

       /**
        * Creates a file reader instance with the current charset.
        *
        * @param   fileName
        *          The file name.
        *
        * @return  a file reader instance
        *
        * @throws FileNotFoundException
        */
       private Reader getFileReader(String fileName)
       throws FileNotFoundException
       {
          return new InputStreamReader(new FileInputStream(fileName), charset);
       }
    }