PreprocessorHints.java

/*
** Module   : PreprocessorHints.java
** Abstract : reads a preprocessor hints file and cleanly exposes the contents
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 NVS 20050525   @21287 Created initial version. This supports reading an XML file that 
**                           "lives" in the same location (and basename) as the associated 
**                           Progress 4GL source file to which it refers. The hints file is the 
**                           source file's basename + ".pphints". Based on UastHints.java by GES.
** 002 NVS 20050601   @21366 Added getMaxIncludeDepth() method which gets the maximum nesting 
**                           level (depth) of includes in this PreprocessorHints instance.
** 003 SIY 20051011   @23017 Added handling of null hintsRoot in getAllIncludes() and 
**                           getNumUniqueIncludes().
** 004 GES 20060328   @25242 All public methods are now protected against a null hints root.
** 005 GES 20090515   @42218 Import changes.
** 006 CA  20140313          Allow unused include nodes with missing value argument.
** 007 ECF 20150715          Replace StringBuffer with StringBuilder.
** 008 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 009 CA  20170714          Added getSource.
** 010 CA  20170825          Enhanced to allow collection and reporting of preprocessor constant
**                           symbols.
** 011 CA  20180731          getSymbolValue() now looks in the entire hint tree, for the specified
**                           symbol (uses a DFS search).
** 012 VVT 20220823          Fixed incorrect file name manipulation in createInclude() (see #6237-24)
**                           and minor code style refactorings.
** 013 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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 java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.logging.*;

import com.goldencode.p2j.convert.*;
import org.w3c.dom.*;

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

/**
 * Loads hints information from a preprocessor hints XML file into memory and
 * exposes the data in an easily accessible format. The purpose of this class
 * is to separate the hints persistence mechanism (i.e., XML) from the user
 * of these hints.
 * <p>
 * Once constructed, the hints file has been loaded into memory and the
 * contents are available via simple 'getter' method calls such as
 * {@link #getArguments}. This class hides the storage mechanism and the
 * file format from the callers.  It also handles any conversion from the
 * string oriented XML format to the proper data type of the value in
 * question.  Generally, if the hints file doesn't exist or if the value
 * being queried doesn't exist in that hints file, the result from a
 * lookup will be <code>null</code>.
 * <p>
 * The following is the structure of the XML file:
 * <p>
 * <ul>
 *    <li> hints (root element)
 *    <ul>
 *       <li> preprocessor-output (element, always 1)
 *       <ul>
 *          <li> include (element, may be nested, the top level element  
 *               describes the original source file)
 *          <ul>
 *            <li> file (attribute, name of the included file)
 *            <li> start-line (attribute, line in the cache file
 *                 where contribution of this include file begins)
 *            <li> start-column (attribute, column in the cache file
 *                 where contribution of this include file begins)
 *            <li> end-line (attribute, line in the cache file
 *                 where contribution of this include file ends)
 *            <li> end-column (attribute, column in the cache file
 *                 where contribution of this include file ends)
 *            <li> reference (element, may be 0 or more)
 *            <ul>
 *               <li> name (attribute, referenced preprocessor name like 1 for
 *                    {&amp;1} or X for {&amp;X}                           
 *               <li> type (attribute, the nature of the referenced name)
 *               <li> value (attribute, the substitution for the name)
 *               <li> line (attribute, together with column defines the
 *                    location of the reference in the preprocessor output      
 *               <li> column (attribute, together with line defines the
 *                    location of the reference in the preprocessor output      
 *            </ul>
 *            <li> argument (element, one per include file argument)
 *            <ul>
 *               <li> name (attribute, argument name
 *               <li> position (attribute, index of the argument in the list)
 *               <li> value (attribute, the value of the argument)
 *               <li> used (attribute, flag, "yes" means the argument was used
 *                    in the include file at least once)      
 *            </ul>
 *          </ul>
 *       </ul>
 *    </ul>
 * </ul> 
 *
 * @author    NVS
 * @version   1.0.0
 */
public class PreprocessorHints
implements HintsTags
{
   /** Logger */
   private static final ConversionStatus LOG = ConversionStatus.get(PreprocessorHints.class);
   
   /**
    * The standard suffix appended to a source code filename to create a
    * unique filename for storing hints.
    */
   private static final String HINTS_SUFFIX = ".pphints";
   
   /** Normalized source filename */
   private String source = null;
   
   /** The hints tree built from the XML file */
   private HintsTreeNode hintsRoot = null;
   
   /**
    * Constructs an instance, loads the associated hints file if it exists
    * and parses the contents.
    *
    * @param    sourceName
    *           The Progress 4GL source program's filename with which this
    *           hints file is associated.
    * @throws  PreprocessorException
    *          An unknown or unexpected failure.
    */
   public PreprocessorHints(String sourceName)
   throws PreprocessorException
   {
      source = Configuration.normalizeFilename(sourceName);
      StringBuilder sb = new StringBuilder(sourceName);
      sb.append(HINTS_SUFFIX);
      
      File hints = new File(sb.toString());
         
      if (hints.exists() && hints.isFile())
      {
         loadHints(hints);
      }
   }
   
   /**
    * Get the {@link #source} filename.
    * 
    * @return   See above.
    */
   public String getSource()
   {
      return source;
   }
   
   /**
    * Get the root of this preprocessor hints tree.
    * 
    * @return    See above.
    */
   public HintsTreeNode getRoot()
   {
      return hintsRoot;
   }
   
   /**
    * Dumps the tree.
    */
   public void dumpHintsTree()
   {
      Iterator ti = hintsRoot.descendantsIterator();
      HintsTreeNode tn = null;
      int depth = 0;
      IncludeHint ih = null;
      List refs = null;
      List args = null;
      TreeNode[] path = null;

      while (ti.hasNext())
      {
         tn = (HintsTreeNode)ti.next();
         depth = tn.getDepth();
         path = tn.getPath();
         ih = tn.getIncludeHint();
         refs = tn.getReferenceHints(false);
         args = tn.getArgumentHints(false);
         System.out.println("Level " + depth + ", file " + ih.getFilename());
         System.out.println("   references: " + refs.size());
         System.out.println("   arguments : " + args.size());
         for (int i = 0; i < path.length; i ++)
            System.out.println("   path[" + i + "]: " + 
                     ((HintsTreeNode)path[i]).getIncludeHint().getFilename());
      }
   }

   /**
    * Get the symbol value, performing a DFS search, from the root node.
    * 
    * @param    name
    *           The symbol name.
    *           
    * @return   The symbol value.
    */
   public String getSymbolValue(String name)
   {
      return getSymbolValue(hintsRoot, name);
   }
   

   /**
    * Get the symbol value, performing a DFS search, from the specified root node.
    * 
    * @param    root
    *           The hints node to start the search.
    * @param    name
    *           The symbol name.
    *           
    * @return   The symbol value.
    */
   private String getSymbolValue(HintsTreeNode root, String name)
   {
      if (root == null)
      {
         return null;
      }

      String val = root.getSymbolValue(name);
      if (val != null)
      {
         return val;
      }
      
      Iterator<HintsTreeNode> iter = (Iterator<HintsTreeNode>) root.childIterator();
      while (val == null && iter.hasNext())
      {
         HintsTreeNode child = iter.next();
         
         val = getSymbolValue(child, name);
      }
      
      return val;
   }
   
   /**
    * Gets the array of argument hints for the top level file.
    *
    * @param deep
    *        tells to get only this node's hints (<code>false</code>) or
    *        everything
    * @return list of ArgumentHint objects.
    */
   public List getArguments(boolean deep)
   {
      if (hintsRoot == null)
         return null;

      return hintsRoot.getArgumentHints(deep);
   }

   /**
    * Gets the array of reference hints for the top level file.
    *
    * @param deep
    *        tells to get only this node's hints (<code>false</code>) or
    *        everything
    * @return array of ReferenceHint objects produced for the top level file.
    */
   public List getReferences(boolean deep)
   {
      if (hintsRoot == null)
         return null;

      return hintsRoot.getReferenceHints(deep);
   }

   /**
    * Locates the include file where specific line and column of the output
    * came from.
    *
    * @param  line
    *         line of the output file under consideration
    * @param  column
    *         column of the output file under consideration. line and column
    *         together refer to the position in the output file that is under
    *         consideration
    * @return the innermost include file name or the source file name if no 
    *         include file contributed to the specified output location.
    */
   public String getSourceFor(int line, int column)
   {
      if (hintsRoot == null)
      {
         return source;
      }
      
      // locate the deepest node that contains this line and column
      // this method always returns a valid node - hintsRoot if nothing else
      HintsTreeNode htn = getPathFor(hintsRoot, line, column);

      // get the path for this node
      TreeNode[] path = htn.getPath();
      // return the last item of the path
      String filename = ((HintsTreeNode)path[path.length - 1]).
                         getIncludeHint().
                         getFilename();

      return filename;
   }

   /**
    * Locates all include files where specific line and column of the output
    * came from.
    *
    * @param  line
    *         line of the output file under consideration
    * @param  column
    *         column of the output file under consideration. line and column
    *         together refer to the position in the output file that is under
    *         consideration
    * @return list of names of include files contributed to the contents of  
    *         the output file at the specified location, or the source file
    *         name. 
    */
   public List getSourcesFor(int line, int column)
   {
      List list = new LinkedList();
      
      if (hintsRoot == null)
      {
          list.add(source);
          return list;
      }
      
      // locate the deepest node that contains this line and column
      // this method always returns a valid node - hintsRoot if nothing else
      HintsTreeNode htn = getPathFor(hintsRoot, line, column);

      // get the path for this node
      TreeNode[] path = htn.getPath(); 
      for (int i = 0; i < path.length; i ++)
         list.add(((HintsTreeNode)path[i]).getIncludeHint().getFilename());

      // return the whole path
      return list;
   }

   /**
    * Returns the total number of included files.
    *
    * @return number of included files. may be 0. 
    */
   public int getNumAllIncludes()
   {
      if (hintsRoot == null)
         return 0;
      
      // enumerate all nodes
      Iterator iter = hintsRoot.descendantsIterator();

      // count all nodes
      int i = 0;
      while (iter.hasNext())
      {
         iter.next();
         i ++;
      }

      return i - 1;
   }

   /**
    * Returns the number of unique include files.
    *
    * @return number of unique include files. may be 0. 
    */
   public int getNumUniqueIncludes()
   {
      if (hintsRoot == null)
         return 0;
      
      // enumerate all nodes
      Iterator iter = hintsRoot.descendantsIterator();

      // collect unique includes
      Set s = new HashSet();
      HintsTreeNode htn = null;
      String name = null;

      while (iter.hasNext())
      {
         htn = (HintsTreeNode)iter.next();
         name = htn.getIncludeHint().getFilename();
         s.add(name);
      }

      return s.size() - 1;
   }

   /**
    * Returns the array of names of all included files.
    *
    * @return List of names of included files. may be of size 0. 
    */
   public List getAllIncludes()
   {
      if (hintsRoot == null)
         return null;
      
      // enumerate all nodes
      Iterator iter = hintsRoot.descendantsIterator();

      // count all nodes
      List list = new LinkedList();
      HintsTreeNode htn = null;
      String name = null;

      iter.next();              // skip the top level node
      while (iter.hasNext())
      {
         htn = (HintsTreeNode)iter.next();
         name = htn.getIncludeHint().getFilename();
         list.add(name);
      }

      return list;
   }

   /**
    * Returns the array of names of unique include files.
    *
    * @return Set of names of unique include files. may be of size 0. 
    */
   public Set getUniqueIncludes()
   {
      Set s = new HashSet();
      
      if (hintsRoot == null)
         return s;
      
      // enumerate all nodes
      Iterator iter = hintsRoot.descendantsIterator();

      // collect unique includes
      HintsTreeNode htn = null;
      String name = null;

      iter.next();              // skip the top level node
      while (iter.hasNext())
      {
         htn = (HintsTreeNode)iter.next();
         name = htn.getIncludeHint().getFilename();
         s.add(name);
      }

      return s;
   }

   /**
    * Returns the maximum nesting level of includes for this instance.
    *
    * @return maximum nesting level or -1 if not initialized 
    */
   public int getMaxIncludeDepth()
   {
      return hintsRoot != null ? hintsRoot.getChildDepth() : -1;
   }

   /**
    * Checks whether the specified file was included or not.
    * If the filename is absolute, the check is done for the full name,
    * otherwise for the specified components only.
    *
    * @param filename
    *        include file to check for inclusion from  
    *
    * @return <code>true</code> if the file was included.
    *  
    * @throws ConfigurationException 
    * @throws IOException 
    */
   public boolean isIncluded(String filename)
   throws ConfigurationException,
          IOException
   {
      if (hintsRoot == null)
         return false;
      
      File f = new File(filename);
      boolean abs = f.isAbsolute();
      String file = abs ? filename : File.separator + filename;
      Set s = getUniqueIncludes();
      String home = Configuration.home() + File.separator;
      String test = null;

      Iterator iter = s.iterator();
      while (iter.hasNext())
      {
         test = (String)iter.next();
         f = new File(home + test);
         test = f.getCanonicalPath();
         if (abs)
         {
            if (test.equals(file))
               return true;
         }
         else
         {
            if (test.endsWith(file))
               return true;
         }
      }

      return false;
   }

   /**
    * Checks whether the specified location in the file was included from
    * an include file or not.
    *
    * @param line
    *        line number within the source file  
    * @param column
    *        column number within the source file; line and column specify
    *        the location within the file to check for inclusion
    *
    * @return <code>true</code> if the location was included. 
    */
   public boolean isIncluded(int line, int column)
   {
      return hintsRoot != null
             && !getSourceFor(line, column).equals(source);
   }

   /**
    * Checks whether there are unused arguments to this file.
    * Unused arguments may indicate potential coding problems.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    * @return <code>true</code> if there are unused arguments. 
    */
   public boolean isUnused(boolean deep)
   {
      if (hintsRoot == null)
         return false;
      
      List hints = getArguments(deep);
      Iterator iter = hints.iterator();
      ArgumentHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ArgumentHint)iter.next();
         if (!hint.getUsed())
            return true;
      }

      return false;
   }

   /**
    * Checks whether the specified positional argument is used or not in
    * this file.
    * Unused arguments may indicate potential coding problems.
    *
    * @param posArgNum
    *        positional argument to test  
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return <code>true</code> if the specified argument was not used. 
    */
   public boolean isUnused(int posArgNum, boolean deep)
   {
      if (hintsRoot == null)
         return false;
      
      String argName = "{" + posArgNum + "}";
      List hints = getArguments(deep);
      Iterator iter = hints.iterator();
      ArgumentHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ArgumentHint)iter.next();
         if (!hint.getName().equals(argName))
            continue;
         return !hint.getUsed();
      }

      return false;
   }

   /**
    * Checks whether the specified named argument is used or not in
    * this file.
    * Unused arguments may indicate potential coding problems.
    *
    * @param namedArg
    *        named argument to test; case does not matter  
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return <code>true</code> if the specified argument was not used. 
    */
   public boolean isUnused(String namedArg, boolean deep)
   {
      if (hintsRoot == null)
         return false;
      
      List hints = getArguments(deep);
      Iterator iter = hints.iterator();
      ArgumentHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ArgumentHint)iter.next();
         if (hint.getName().compareToIgnoreCase(namedArg) != 0)
            continue;
         
         return !hint.getUsed();
      }

      return false;
   }

   /**
    * Checks whether the specified variable was referenced or not in
    * this file.
    *
    * @param var
    *        variable to test; case does not matter  
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return <code>true</code> if the specified variable was referenced. 
    */
   public boolean isReferenced(String var, boolean deep)
   {
      if (hintsRoot == null)
         return false;
      
      List hints = getReferences(deep);      
      Iterator iter = hints.iterator();
      ReferenceHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ReferenceHint)iter.next();
         if (hint.getName().compareToIgnoreCase(var) != 0)
            continue;
         return true;
      }

      return false;
   }

   /**
    * Gets the number of references to the specified variable in
    * this file.
    *
    * @param var
    *        variable to test; case does not matter  
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumReferenced(String var, boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      int count = 0;
      List hints = getReferences(deep);      
      Iterator iter = hints.iterator();
      ReferenceHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ReferenceHint)iter.next();
         if (hint.getName().compareToIgnoreCase(var) == 0)
            count ++;
      }

      return count;
   }

   /**
    * Gets the number of references to global variables in this file.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumRefGlobal(boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      return getNumRefs("global", deep);
   }

   /**
    * Gets the number of references to scoped variables in this file.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumRefScoped(boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      return getNumRefs("scoped", deep);
   }

   /**
    * Gets the number of references to builtin variables in this file.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumRefBuiltin(boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      return getNumRefs("builtin", deep);
   }

   /**
    * Gets the number of references to arguments in this file.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumRefArgument(boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      return getNumRefs("argument", deep);
   }

   /**
    * Gets the number of references to undefined variables in this file.
    *
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   public int getNumRefUndefined(boolean deep)
   {
      if (hintsRoot == null)
         return 0;
      
      return getNumRefs("undefined", deep);
   }

   /**
    * Load preprocessor hints from an XML document associated with the
    * specified filename.
    *
    * @param   hintsFile
    *          The hints file to read.
    *
    * @throws  PreprocessorException
    *          An unknown or unexpected failure.
    */
   private void loadHints(File hintsFile)
   throws PreprocessorException
   {
      try
      {
         Document  dom  = XmlHelper.parse(hintsFile);
         Element   root = dom.getDocumentElement();

         // check the root and subroot nodes
         if (!root.getTagName().toUpperCase().equals(
                                              ELEM_TAG_ROOT.toUpperCase()))
            throw new PreprocessorException("Invalid root node: " + 
                                             root.getTagName());
         NodeList cl = root.getChildNodes();
         Node cn = null;
         Element subroot = null;

         // check the subroot node
         int elCount = 0;
         for (int i = 0; cl != null && i < cl.getLength(); i ++)
         {
            cn = cl.item(i);
            if (cn.getNodeType() != Node.ELEMENT_NODE)
               continue;
            elCount ++;
            if (elCount == 1 && !cn.getNodeName().toUpperCase().equals(
                                              ELEM_TAG_SUBROOT.toUpperCase()))
               throw new PreprocessorException("Invalid subroot node: " + 
                                                cn.getNodeName());
            if (elCount > 1)
               throw new PreprocessorException("Multiple subroot nodes");
            subroot = (Element)cn;
         }

         // create the hints tree recursively
         String topName = hintsFile.getAbsolutePath();
         topName = topName.substring(0, topName.length() - 
                                     HINTS_SUFFIX.length());
         IncludeHint topInclude = new IncludeHint(topName);
         topInclude.setStartLine(1);
         topInclude.setStartColumn(1);
         topInclude.setEndLine(Integer.MAX_VALUE);
         topInclude.setEndColumn(Integer.MAX_VALUE);

         hintsRoot = createHintsNode(topInclude, subroot);

         // check for the top level include element presence:
         // - it has to be the only node (element) under the subroot;
         // - it has to have the file attribute matching the base filename
         if (hintsRoot.getNumChildren() == 1)
         {
            HintsTreeNode first = (HintsTreeNode)hintsRoot.getFirstChild();
            IncludeHint ih = first.getIncludeHint();
            if (topInclude.getFilename().equals(ih.getFilename()))
            {
               first.setParent(null);
               hintsRoot = first;
            }
         }
      }
      catch (Exception exc)
      {
         throw new PreprocessorException(
            "Error loading hints file: " + hintsFile.getAbsolutePath(), exc);
      }
   }

   /**
    * Creates a hints tree node from the information in the passed DOM node.
    *
    * @param   top
    *          the top level include hint for the subtree being created
    * @param   elem
    *          the DOM element to analyze
    * @return  <code>HintsTreeNode</code> representing the specified DOM node
    * @throws  PreprocessorException
    *          when the original XML document contains invalid nodes
    */
   private HintsTreeNode createHintsNode(IncludeHint top, Element elem)
   throws PreprocessorException
   {
      ReferenceHint[] refs = new ReferenceHint[0];
      SymbolHint[]    syms = new SymbolHint[0];
      ArgumentHint[]  args = new ArgumentHint[0];

      List refList = new LinkedList();
      List symList = new LinkedList();
      List argList = new LinkedList();
      String refTag = ELEM_TAG_REF.toUpperCase();
      String symTag = ELEM_TAG_SYM.toUpperCase();
      String argTag = ELEM_TAG_ARG.toUpperCase();
      String incTag = ELEM_TAG_INCL.toUpperCase();

      // enumerate all children elements of the passed element
      NodeList cl = elem.getChildNodes();
      Node cn = null;

      // check and process element nodes
      // valid elements are include, reference and argument
      // first pass: checks, references and arguments
      String name = null;
      for (int i = 0; cl != null && i < cl.getLength(); i ++)
      {
         cn = cl.item(i);
         if (cn.getNodeType() != Node.ELEMENT_NODE)
            continue;
         name = cn.getNodeName().toUpperCase();
         if (name.equals(incTag))
            continue;
         if (name.equals(refTag))
         {
            refList.add(createReference((Element)cn));
            continue;
         }
         if (name.equals(symTag))
         {
            symList.add(createSymbol((Element)cn));
            continue;
         }
         if (name.equals(argTag))
         {
            argList.add(createArgument((Element)cn));
            continue;
         }
         throw new PreprocessorException("Invalid element: " + name);
      }

      // create the HintsTree node
      refs = (ReferenceHint[])refList.toArray(refs);
      args = (ArgumentHint[])argList.toArray(args);
      syms = (SymbolHint[])symList.toArray(syms);
      HintsTreeNode ht = new HintsTreeNode(top, refs, syms, args);
      HintsTreeNode in = null;

      // second pass: inner includes
      for (int i = 0; cl != null && i < cl.getLength(); i ++)
      {
         cn = cl.item(i);
         if (cn.getNodeType() != Node.ELEMENT_NODE)
            continue;
         name = cn.getNodeName().toUpperCase();
         if (!name.equals(incTag))
            continue;
         in = createInclude((Element)cn);
         ht.addChild(in);
      }

      return ht;
   }

   /**
    * Creates a <code>ReferenceHint</code> instance from the information
    * in the passed DOM node.
    *
    * @param   elem
    *          the DOM element to analyze
    * @return  <code>ReferenceHint</code> representing the specified DOM node
    * @throws  PreprocessorException
    *          when the original XML document contains invalid nodes
    */
   private static ReferenceHint createReference(Element elem)
   throws PreprocessorException
   {
      // get values of all mandatory attributes
      String name  = XmlHelper.getAttribute(elem, ATTR_TAG_REF_NAME);
      String type  = XmlHelper.getAttribute(elem, ATTR_TAG_REF_TYPE);
      String value = XmlHelper.getAttribute(elem, ATTR_TAG_REF_VAL);
      String line  = XmlHelper.getAttribute(elem, ATTR_TAG_REF_LINE);
      String col   = XmlHelper.getAttribute(elem, ATTR_TAG_REF_COL);

      if (name == null || type == null || value == null || line == null ||
          col == null)
         throw new PreprocessorException("incomplete reference element");

      int lineNum = Integer.parseInt(line);
      int colNum  = Integer.parseInt(col);

      ReferenceHint rh = new ReferenceHint(name, type, value);
      rh.setLine(lineNum);
      rh.setColumn(colNum);

      return rh;
   }

   /**
    * Creates a <code>SymbolHint</code> instance from the information in the passed DOM node.
    *
    * @param   elem
    *          the DOM element to analyze
    * 
    * @return  <code>SymbolHint</code> representing the specified DOM node
    * 
    * @throws  PreprocessorException
    *          when the original XML document contains invalid nodes
    */
   private static SymbolHint createSymbol(Element elem)
   throws PreprocessorException
   {
      // get values of all mandatory attributes
      String name  = XmlHelper.getAttribute(elem, ATTR_TAG_SYM_NAME);
      String type  = XmlHelper.getAttribute(elem, ATTR_TAG_SYM_TYPE);
      String value = XmlHelper.getAttribute(elem, ATTR_TAG_SYM_VAL);

      if (name == null || type == null || value == null)
         throw new PreprocessorException("incomplete symbol element");

      SymbolHint sh = new SymbolHint(name, type, value);

      return sh;
   }

   /**
    * Creates an <code>ArgumentHint</code> instance from the information
    * in the passed DOM node.
    *
    * @param   elem
    *          the DOM element to analyze
    * @return  <code>ArgumentHint</code> representing the specified DOM node
    * @throws  PreprocessorException
    *          when the original XML document contains invalid nodes
    */
   private static ArgumentHint createArgument(Element elem)
   throws PreprocessorException
   {
      // get values of all mandatory attributes
      String name  = XmlHelper.getAttribute(elem, ATTR_TAG_ARG_NAME);
      String pos   = XmlHelper.getAttribute(elem, ATTR_TAG_ARG_POS);
      String value = XmlHelper.getAttribute(elem, ATTR_TAG_ARG_VAL);
      String used  = XmlHelper.getAttribute(elem, ATTR_TAG_ARG_USED);

      if (name == null || pos == null || used == null)
         throw new PreprocessorException("incomplete argument element");

      if (value == null && "yes".equalsIgnoreCase(used))
      {
         throw new PreprocessorException("incomplete argument element: used, with missing value");
      }
      
      int posNum = Integer.parseInt(pos);
      boolean usedFlag = false;
      
      used = used.toUpperCase();
      if (used.equals("YES"))
         usedFlag = true;

      return new ArgumentHint(name, posNum, value, usedFlag);
   }

   /**
    * Creates an <code>ArgumentHint</code> instance from the information
    * in the passed DOM node and then creates a HintsTreeNode from it.
    *
    * @param   elem
    *          the DOM element to analyze
    * @return  <code>HintsTreeNode</code> representing the specified DOM node
    * @throws  PreprocessorException
    *          when the original XML document contains invalid nodes
    */
   private HintsTreeNode createInclude(Element elem)
   throws PreprocessorException
   {
      // get values of all mandatory attributes
      String file = XmlHelper.getAttribute(elem, ATTR_TAG_FILE);
      String slin = XmlHelper.getAttribute(elem, ATTR_TAG_START_LINE);
      String scol = XmlHelper.getAttribute(elem, ATTR_TAG_START_COL);
      String elin = XmlHelper.getAttribute(elem, ATTR_TAG_END_LINE);
      String ecol = XmlHelper.getAttribute(elem, ATTR_TAG_END_COL);

      if (file == null || slin == null || scol == null || elin == null ||
          ecol == null)
         throw new PreprocessorException("incomplete include element");

      int sline = Integer.parseInt(slin);
      int scolm = Integer.parseInt(scol);
      int eline = Integer.parseInt(elin);
      int ecolm = Integer.parseInt(ecol);

      if (!Paths.get(file).isAbsolute())
      {
         // denormalize the file
         try
         {
            file = Paths.get(Configuration.home(), file).toString();
         }
         catch (ConfigurationException xcpt)
         {
            throw new PreprocessorException("Project home misconfigured", xcpt);
         }   
      }

      // create instance of IncludeHint
      IncludeHint ih = new IncludeHint(file);
      ih.setStartLine(sline);
      ih.setStartColumn(scolm);
      ih.setEndLine(eline);
      ih.setEndColumn(ecolm);

      // use this instance of IncludeHint to process the element recursively
      HintsTreeNode ht = createHintsNode(ih, elem);

      return ht;
   }

   /**
    * Locates all include files where specific line and column of the output
    * came from.
    *
    * @param  node
    *         starting node for this search
    * @param  line
    *         line of the output file under consideration
    * @param  column
    *         column of the output file under consideration. line and column
    *         together refer to the position in the output file that is under
    *         consideration
    * @return hints tree node contributed to the contents of the output 
    *         file at the specified location or the input node.
    */
   private HintsTreeNode getPathFor(HintsTreeNode node, int line, int column)
   {
      // enumerate immediate children of the given node
      Iterator thisIter = node.childIterator();

      // find a child node contributing to the specified location.
      // a node contributes to line, column if its range of output positions
      // covers the specified line and column
      HintsTreeNode current = null;
      IncludeHint ih = null;
      int startLine = 0;
      int endLine = 0;
      int startCol = 0;
      int endCol = 0;

      while (thisIter.hasNext())
      {
         current = (HintsTreeNode)thisIter.next();
         ih = current.getIncludeHint();
         startLine = ih.getStartLine();
         endLine = ih.getEndLine();
         startCol = ih.getStartColumn();
         endCol = ih.getEndColumn();

         // this include begins after the position - stop looking
         if (line < startLine || (line == startLine && column < startCol))
             return node;
             
         // the covering include? Note that the end column does NOT belong
         // to this include
         if ((line > startLine || (line == startLine && column >= startCol)) 
            && (line < endLine || (line == endLine   && column <  endCol)))
            return getPathFor(current, line, column);
      }

      // no covering include
      return node;
   }

   /**
    * Gets the number of references to the specified type variables in this
    * file.
    *
    * @param type
    *        type of reference to check
    * @param deep
    *        tells to look only at this node's hints (<code>false</code>) or
    *        everything
    *
    * @return number of references 
    */
   private int getNumRefs(String type, boolean deep)
   {
      int count = 0;
      List hints = getReferences(deep);      
      Iterator iter = hints.iterator();
      ReferenceHint hint = null;      

      while (iter.hasNext())
      {
         hint = (ReferenceHint)iter.next();
         if (hint.getType().equals(type))
            count ++;
      }

      return count;
   }

   /**
    * Command line driver used for testing.
    *
    * @param   cargs
    *          command line parameters. 
    *          Only one parameter is used and it is the base filename.
    *          
    * @throws ConfigurationException 
    * @throws IOException 
    */
   public static void main(String[] cargs)
   throws ConfigurationException,
          IOException
   {
      if (cargs.length != 3)
      {
         LOG.log(Level.SEVERE, "Correct syntax is 'PreprocessorHints file line# col#'");
         System.exit(1);
      }

      PreprocessorHints ph = null; 
      try
      {
         ph = new PreprocessorHints(cargs[0]);
      }
      catch (PreprocessorException excpt)
      {
         LOG.log(Level.SEVERE, "", excpt);
         System.exit(1);
      }
      LOG.log(Level.INFO,"Loaded hints file successfully");
      ph.dumpHintsTree();
 
      List args = ph.getArguments(false);
      if (args != null && args.size() != 0)
      {
         System.out.println("Arguments were:");
         Iterator iter = args.iterator();
         ArgumentHint arg = null;
         while (iter.hasNext())
         {
            arg = (ArgumentHint)iter.next();
            System.out.println("   name: " + arg.getName());
            System.out.println("    pos: " + arg.getPosition());
            System.out.println("  value: " + arg.getValue());
            System.out.println("   used: " + arg.getUsed());
         }
      }
      else
         System.out.println("No arguments were given");

      args = ph.getArguments(true);
      System.out.println("Deep arguments search: " + args.size());
      if (args != null && args.size() != 0)
      {
         Iterator iter = args.iterator();
         ArgumentHint arg = null;
         while (iter.hasNext())
         {
            arg = (ArgumentHint)iter.next();
            System.out.println("   name: " + arg.getName());
            System.out.println("    pos: " + arg.getPosition());
            System.out.println("  value: " + arg.getValue());
            System.out.println("   used: " + arg.getUsed());
         }
      }

      List refs = ph.getReferences(false);
      if (refs != null && refs.size() != 0)
      {
         System.out.println("References were:");
         Iterator iter = refs.iterator();
         ReferenceHint ref = null;
         while (iter.hasNext())
         {
            ref = (ReferenceHint)iter.next();
            System.out.println("   name: " + ref.getName());
            System.out.println("   type: " + ref.getType());
            System.out.println("  value: " + ref.getValue());
            System.out.println("   line: " + ref.getLine());
            System.out.println(" column: " + ref.getColumn());
         }
      }
      else
         System.out.println("No references were found");
      refs = ph.getReferences(true);
      System.out.println("Deep references search: " + refs.size());
      if (refs != null && refs.size() != 0)
      {
         Iterator iter = refs.iterator();
         ReferenceHint ref = null;
         while (iter.hasNext())
         {
            ref = (ReferenceHint)iter.next();
            System.out.println("   name: " + ref.getName());
            System.out.println("   type: " + ref.getType());
            System.out.println("  value: " + ref.getValue());
            System.out.println("   line: " + ref.getLine());
            System.out.println(" column: " + ref.getColumn());
         }
      }

      int line = Integer.parseInt(cargs[1]);
      int col  = Integer.parseInt(cargs[2]);
      String lastIncl = ph.getSourceFor(line, col);
      System.out.println("Contributing include file name for line " + cargs[1] +
                         " column " + cargs[2] + " is:");
      System.out.println("   " + lastIncl);

      List allIncl = ph.getSourcesFor(line, col);
      System.out.println("Contributing include file names for line " + cargs[1] +
                         " column " + cargs[2] + " are:");
      System.out.println("   " + allIncl.toString());

      System.out.println("Total  includes " + ph.getNumAllIncludes());
      System.out.println("Unique includes " + ph.getNumUniqueIncludes());

      List allNames = ph.getAllIncludes();
      System.out.println("All include file names:");
      System.out.println("   " + allNames.toString());

      Set uniqueNames = ph.getUniqueIncludes();
      System.out.println("Unique include file names:");
      System.out.println("   " + uniqueNames.toString());
      
      System.out.println("isIncluded(file): " + ph.isIncluded("file"));
      System.out.println("isIncluded(ile): " + ph.isIncluded("ile"));
      System.out.println("isIncluded(preproc/file): " + ph.isIncluded("preproc/file"));
      System.out.println("isIncluded(reproc/file): " + ph.isIncluded("reproc/file"));
      String fname = "/home/nvs/projects/p2j/testcases/preproc/file";
      System.out.println("isIncluded(" + fname + "): " + ph.isIncluded(fname));
      
      System.out.println("isIncluded(10, 10): " + ph.isIncluded(10, 10));
      System.out.println("isIncluded(60, 15): " + ph.isIncluded(60, 15));
      System.out.println("isIncluded(71, 11): " + ph.isIncluded(71, 11));

      System.out.println("isUnused(false): " + ph.isUnused(false));
      System.out.println("isUnused(true): " + ph.isUnused(true));
      System.out.println("isUnused(1,false): " + ph.isUnused(1,false));
      System.out.println("isUnused(2,false): " + ph.isUnused(2,false));
      System.out.println("isUnused(3,false): " + ph.isUnused(3,false));
      System.out.println("isUnused(4,false): " + ph.isUnused(4,false));
      System.out.println("isUnused(5,false): " + ph.isUnused(5,false));
      System.out.println("isUnused(6,false): " + ph.isUnused(6,false));
      System.out.println("isUnused(7,false): " + ph.isUnused(7,false));
      System.out.println("isUnused(8,false): " + ph.isUnused(8,false));
      System.out.println("isUnused(1,true): " + ph.isUnused(1,true));
      System.out.println("isUnused(2,true): " + ph.isUnused(2,true));
      System.out.println("isUnused(3,true): " + ph.isUnused(3,true));
      System.out.println("isUnused(4,true): " + ph.isUnused(4,true));
      System.out.println("isUnused(5,true): " + ph.isUnused(5,true));
      System.out.println("isUnused(6,true): " + ph.isUnused(6,true));
      System.out.println("isUnused(7,true): " + ph.isUnused(7,true));
      System.out.println("isUnused(8,true): " + ph.isUnused(8,true));

      System.out.println("isUnused(named, false): " + ph.isUnused("named", false));
      System.out.println("isUnused(named1, false): " + ph.isUnused("named1", false));
      System.out.println("isUnused(named2, false): " + ph.isUnused("named2", false));
      System.out.println("isUnused(named, true): " + ph.isUnused("named", true));
      System.out.println("isUnused(named1, true): " + ph.isUnused("named1", true));
      System.out.println("isUnused(named2, true): " + ph.isUnused("named2", true));

      System.out.println("isReferenced(opsys, false): " + ph.isReferenced("opsys", false));
      System.out.println("isReferenced(xyz, false): " + ph.isReferenced("xyz", false));
      System.out.println("isReferenced(opsys, true): " + ph.isReferenced("opsys", true));
      System.out.println("isReferenced(xyz, true): " + ph.isReferenced("xyz", true));

      System.out.println("getNumReferenced(opsys, false): " + ph.getNumReferenced("opsys", false));
      System.out.println("getNumReferenced(xyz, false): " + ph.getNumReferenced("xyz", false));
      System.out.println("getNumReferenced(opsys, true): " + ph.getNumReferenced("opsys", true));
      System.out.println("getNumReferenced(xyz, true): " + ph.getNumReferenced("xyz", true));

      System.out.println("getNumRefGlobal(false): " + ph.getNumRefGlobal(false));
      System.out.println("getNumRefGlobal(true): " + ph.getNumRefGlobal(true));
      System.out.println("getNumRefScoped(false): " + ph.getNumRefScoped(false));
      System.out.println("getNumRefScoped(true): " + ph.getNumRefScoped(true));
      System.out.println("getNumRefBuiltin(false): " + ph.getNumRefBuiltin(false));
      System.out.println("getNumRefBuiltin(true): " + ph.getNumRefBuiltin(true));
      System.out.println("getNumRefArgument(false): " + ph.getNumRefArgument(false));
      System.out.println("getNumRefArgument(true): " + ph.getNumRefArgument(true));
      System.out.println("getNumRefUndefined(false): " + ph.getNumRefUndefined(false));
      System.out.println("getNumRefUndefined(true): " + ph.getNumRefUndefined(true));

      System.out.println("Max include nesting is " + ph.getMaxIncludeDepth());
   }
}