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);
   }
}