ServiceSupport.java

/*
** Module   : ServiceSupport.java
** Abstract : Helper code and APIs to identify legacy services (REST, SOAP, WEB) from OpenEdge artifacts.
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description---------------------------------------
** 001 CA  20190611 First version.
** 002 CA  20190703 Added support for WebHandler services.
** 003 CA  20200427 If a REST parameter is not configured explicitly and is OUTPUT, assume the
**                  default as a JSON value.
** 004 CA  20200520 Added support for SOAP web services.
**     CA  20200528 The SOAP input are the .wsm files, not .xpxg (as these contain info configured when 
**                  running ProxyGen).
** 005 GES 20200722 Added multiple .paar file support. Added input-output mode support.
** 006 CA  20201008 Added generation for legacy open client proxy Java programs.
**     CA  20210816 Do not include internal entries (as operations) for non-persistent services.
**     CA  20211012 Fixed generated WSDL and reworked SOAP support to rely on namespace, when resolving an
**                  operation.
**     CA  20220427 Allow unknown arguments, default change tracking is done only for .NET.
**                  Legacy OpenClient proxy conversion must be done for each (Sub)AppObject, as a program may 
**                  appear in multiple sections.
**     CA  20220513 Optimized proxy generation, all programs belonging to multiple (Sub)AppObjects are 
**                  processed multiple times, in batches.
**     CA  20220516 The (Sub)AppObject JASTs must be tracked by Java package, too, as the same name may appear
**                  in multiple (Sub)AppObject configs, but for different packages. 
**     CA  20220526 Allow the proxy classes and methods to use the original legacy names, via the 
**                  'proxy-legacy-names' config parameter.
**     CA  20220503 Allow the generation of proxy Java programs in the folder specified by 'proxy-output-root'.
**     CA  20230125 Default the procType to '2' in case of 'isPersistent=true' programs.
** 007 CA  20230221 Javadoc fixes.
** 008 SBI 20230216 Fixed resolveRestAnnotation because defaultValue() was added to LegacyServiceParameter.
** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 010 CA  20230706 Added 'hasRestSubServices', to keep legacy REST annotations in the 4GL code, at the 
**                  external program, when the internal entries are exposed as REST.
** 011 CA  20240229 Fixed case when the ProcPath starts with './' or is empty.
**     CA  20240404 Address '..' by normalizing the path; any '..' at the beginning is not supported.
** 012 CA  20240319 Fixed CA/011 to add a separator between the path and filename.
*/

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

import com.goldencode.ast.*;
import com.goldencode.expr.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;

import java.io.*;
import java.lang.annotation.*;
import java.nio.file.Paths;
import java.util.*;
import java.util.logging.*;
import java.util.zip.*;

import org.w3c.dom.*;
import org.w3c.dom.Text;

/**
 * Interpret and create supporting data, to be able to create {@link LegacyService} annotations,
 * for any legacy service defined in 4GL code.
 * <p>
 * For REST services, this relies on the <code>.paar</code> archive, which includes:
 * <ul>
 *  <li><code>resourceModel.xml</code>, a file which defines the resources and the operation(s)
 *      on that resource.</li>
 *  <li><code>mapping.xml</code>, specifies how to read and write legacy parameters from/to HTTP 
 *      request or response.</li>
 *  <li><code>.restoe</code> file, which defines the details about the 4GL source code which is
 *      exposed as a service.</li>
 * </ul>
 * A single <code>.paar</code> archive can be specified via the <code>rest-cfg</code> configuration value
 * or that value can be a directory containing one or more {@code .paar} archives.
 * <p>
 * For SOAP services, a list of <code>.wsm</code> files must be specified, which will be used to
 * annotated the converted 4GL code (for each exported SOAP service), and also create WSDL files.
 * <p>
 * These files are read from the <code>soap-cfg</code> parameter, which represents a comma-separated
 * list of <code>.wsm</code> files or folders.  In case of folders, all <code>.wsm</code> files
 * will be read from it (non-recursive). 
 * <p>
 * For legacy proxy programs, one or more <code>.xpxg</code> files is provided.  For all these programs, a
 * new Java class will be generated, which will expose the metadata for the defined datasets and temp-tables,
 * plus APIs to invoke the internal procedures.
 */
public class ServiceSupport
extends AbstractPatternWorker
{
   /** Contains state which must survive for the entire run of the conversion. */
   private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
   {
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };
   
   /** Logger */
   private static final ConversionStatus LOG = ConversionStatus.get(ServiceSupport.class);

   /** The map of legacy configured REST operations. */
   private Map<String, RestOperation> operations = Collections.emptyMap();

   /** A map of WebHandler classes to their list of paths. */
   private Map<String, List<WebHandlerConfig>> webHandlers = Collections.emptyMap();
   
   /** A map of SOAP configurations, by their name (the .wsm file name, without extension). */
   private Map<String, SoapConfig> soapConfigs = Collections.emptyMap();
   
   /**
    * Default constructor which defines the symbol libraries to be registered.
    */
   public ServiceSupport()
   {
      super();
      
      setLibrary(new Library());
   }
   
   /**
    * Set the current proxy configuration for legacy OpenClient proxy generation.
    * 
    * @param    cfgs
    *           The proxy client configs.
    */
   public static void setCurrentProxyConfigs(List<LegacyProxyConfig> cfgs)
   {
      WorkArea wa = context.get();
      wa.proxyConfigs = cfgs;
   }
   
   /**
    * Get the list of proxy configs for legacy OpenClient.
    * 
    * @return   See above.
    */
   public static List<LegacyProxyConfig> getProxyConfigs()
   {
      WorkArea wa = context.get();
      if (wa.proxyConfigs == null)
      {
         return null;
      }
      
      List<LegacyProxyConfig> res = wa.proxyConfigs;
      
      return res == null ? Collections.emptyList() : new ArrayList<>(res);
   }
   
   /**
    * Get the list of JAST files which were generated for legay open client proxy programs.
    * 
    * @param    doneAppObjects
    *           The set of (Sub)AppObject class names (qualified with their packages), which can have their 
    *           JASTs discarded, as there are no more programs for them to generate.
    *           
    * @return   See above.
    */
   public static FileList getProxyClientPrograms(Set<String> doneAppObjects)
   {
      WorkArea wa = context.get();

      List<String> filenames = new ArrayList<>();
      
      for (LegacyProxyConfig lpc : wa.proxyConfigs)
      {
         filenames.addAll(lpc.getAstFiles(true));
      }
      
      for (Aast ast : wa.appObjectJast.values())
      {
         filenames.add(ast.getFilename());
      }
      
      wa.appObjectJast.keySet().removeAll(doneAppObjects);
      
      return new ExplicitFileList(filenames.toArray(new String[0]));
   }
   
   /**
    * Read and initialize the proxy program configurations, from {@code p2j.cfg.xml}. 
    */
   public static void initializeProxyConfigurations()
   {
      String proxyCfg = Configuration.getParameter("proxy-programs-cfg");
      WorkArea wa = context.get();
      if (proxyCfg != null && wa.proxyConfigs == null)
      {
         try
         {
            wa.proxyConfigs = new ArrayList<>();
            wa.proxyConfigs = resolveProxyProgramsConfiguration(proxyCfg);
         }
         catch (Exception e)
         {
            LOG.log(Level.SEVERE, "", e);
         }
      }
   }
   
   /**
    * Set the {@link WorkArea#brewProxy} flag.  This allows generation of the proxy Java programs in the 
    * folder specified in the {@code proxy-output-root} parameter from p2j.cfg.xml.
    * 
    * @param    state
    *           The state of the flag.
    */
   public static void setBrewProxy(boolean state)
   {
      context.get().brewProxy = state;
   }

   /**
    * Initialize, by interpreting (if specified) any REST, Web or SOAP services.
    */
   @Override
   public void initialize()
   {
      super.initialize();
      
      if (Configuration.isRuntimeConfig())
      {
         return;
      }
      
      initializeProxyConfigurations();
      
      String restCfg = Configuration.getParameter("rest-cfg");
      if (restCfg != null)
      {
         try
         {
            operations = resolveRestConfiguration(restCfg);
         }
         catch (Exception e)
         {
            LOG.log(Level.WARNING,"", e);
         }
      }
      
      String webHandlerCfg = Configuration.getParameter("webhandler-cfg");
      if (webHandlerCfg != null)
      {
         try
         {
            webHandlers = resolveWebHandlers(webHandlerCfg);
         }
         catch (Exception e)
         {
            LOG.log(Level.WARNING,"", e);
         }
      }
      
      String soapSrcs = Configuration.getParameter("soap-cfg");
      if (soapSrcs != null)
      {
         try
         {
            soapConfigs = new HashMap<>();
            String[] cfgs = soapSrcs.split(",");
            for (String cfg : cfgs)
            {
               File f = new File(cfg);
               File[] files = null;
               if (f.isDirectory())
               {
                  files = f.listFiles((dir, name) -> name.endsWith(".wsm"));
               }
               else if (cfg.endsWith(".wsm"))
               {
                  files = new File[] { f };
               }
               else
               {
                  LOG.log(Level.WARNING, "File " + cfg + " is not a .wsm file!");
               }
               
               for (File fcfg : files)
               {
                  SoapConfig soapCfg = createSoapConfig(fcfg);

                  String fname = fcfg.getName();
                  fname = fname.substring(0, fname.length() - ".wsm".length());
                  soapConfigs.put(fname, soapCfg);
               }
            }
         }
         catch (Exception e)
         {
            LOG.log(Level.WARNING,"", e);
         }
      }
   }
   
   /**
    * Parse the .xpxg file(s) and read the configurations for the external programs exposed as legacy open 
    * client proxy programs.
    * 
    * @param     proxyCfg
    *            The value of the <code>proxy-programs-cfg</code> configuration.
    *            
    * @return    The list of {@link LegacyProxyConfig} instances, one for each .xpxg file.
    * 
    * @throws    Exception
    *            In case of XML errors.
    */
   private static List<LegacyProxyConfig> resolveProxyProgramsConfiguration(String proxyCfg)
   throws Exception
   {
      List<LegacyProxyConfig> res = new ArrayList<>();
      
      File input = new File(proxyCfg);
      
      if (input.exists())
      {
         if (input.isFile())
         {
            res.addAll(parseProxyProgram(input));
         }
         else
         {
            File[] files = null;
            
            // must be a directory
            if (input.isDirectory())
            {
               FileSpecList list = new FileSpecList(input, "*.xpxg", true);
               files = list.list();
               for (File xpxg : files)
               {
                  res.addAll(parseProxyProgram(xpxg));
               }
            }
         }
      }
      
      return res;
   }
   
   /**
    * Parse the .xpxg file and read the configurations for the external programs exposed as legacy open client
    * proxy programs.
    * 
    * @param      file
    *             The .xpxg file name.
    * @return     The {@link LegacyProxyConfig configs} for this file's AppObject and SubAppObject, with all  
    *             registered programs for which a Java class will be generated.
    *             
    * @throws     Exception
    *             In case of XML errors.
    */
   private static List<LegacyProxyConfig> parseProxyProgram(File file)
   throws Exception
   {
      Document dom = XmlHelper.parse(new FileInputStream(file));
      
      Element root = dom.getDocumentElement();
      root = (Element) root.getElementsByTagName("AppObject").item(0);
      
      Map<String, String> configs = new HashMap<>();
      readRecursively(null, root, configs);
      
      String pkg = configs.get("PGGenInfo/DotNetNamespace");
      if (pkg == null || pkg.trim().isEmpty())
      {
         pkg = configs.get("PGGenInfo/Package");
      }
      if (pkg == null || pkg.trim().isEmpty())
      {
         pkg = Configuration.getParameter("pkgroot") + ".proxy";
      }
      
      // for Java, do not track changes by default
      String appObject = configs.get("Name");
      boolean trackChanges = !"true".equalsIgnoreCase(configs.get("PGGenInfo/JavaClient"));
      boolean allowUnknown = "true".equalsIgnoreCase(configs.get("AllowUnknown"));
      List<LegacyProxyConfig> res = new ArrayList<>();
      
      LegacyProxyConfig main = new LegacyProxyConfig(pkg, appObject, allowUnknown, trackChanges);
      res.add(main);
      
      // read all children recursively and process them
      NodeList children = root.getChildNodes();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node node  = children.item(i);
         if (node.getNodeType() == Node.ELEMENT_NODE)
         {
            Element el = (Element) node;
            String nodeName = el.getNodeName();
            
            if (nodeName.equals("Procedure"))
            {
               main.addProxyProgram(readProxyProgram(file, el, appObject, main.getPkg(), allowUnknown));
            }
            else if (nodeName.equals("SubAppObject"))
            {
               Map<String, String> subConfigs = new HashMap<>();
               readRecursively(null, el, subConfigs);
               
               boolean subAllowUnknown = "true".equalsIgnoreCase(subConfigs.get("AllowUnknown"));

               String subAppObject = subConfigs.get("Name");

               LegacyProxyConfig sub = new LegacyProxyConfig(pkg, subAppObject, subAllowUnknown, trackChanges);
               res.add(sub);
               
               NodeList subChildren = el.getChildNodes();
               for (int j = 0; j < subChildren.getLength(); j++)
               {
                  Node subNode  = subChildren.item(j);
                  if (subNode.getNodeType() == Node.ELEMENT_NODE)
                  {
                     Element subEl = (Element) subNode;
                     String subNodeName = subEl.getNodeName();

                     if (subNodeName.equals("Procedure"))
                     {
                        LegacyProxyClientConfig cfg = readProxyProgram(file,
                                                                       subEl, 
                                                                       subAppObject, 
                                                                       pkg, 
                                                                       subAllowUnknown);
                        sub.addProxyProgram(cfg);
                     }
                  }
               }
            }
         }
      }
      return res;
   }
   
   /**
    * Read the configuration about an external procedure exposed as legacy open client proxy program.
    * 
    * @param    file
    *           The .xpxg file name.
    * @param    el
    *           The 'Procedure' XML element.
    * @param    appObject
    *           The name of the (Sub)AppObject where this program is configured.
    * @param    pkg
    *           The Java package for the proxy program.
    * @param    allowUnknown
    *           Flag indicating if unknown arguments are allowed.
    *           
    * @return   The {@link LegacyProxyClientConfig} instance.
    */
   private static LegacyProxyClientConfig readProxyProgram(File   file,
                                                           Element el, 
                                                           String appObject, 
                                                           String pkg, 
                                                           boolean allowUnknown)
   {
      boolean useFullName = "true".equalsIgnoreCase(el.getAttribute("useFullName"));
      boolean isPersistent = "true".equalsIgnoreCase(el.getAttribute("isPersistent"));
      int procType = 1;
      if (isPersistent)
      {
         try
         {
            procType = Integer.parseInt(el.getAttribute("procType"));
         }
         catch (Exception e)
         {
            // default to 2, plain persistent program
            procType = 2;
         }
      }
      String name = getChildElementText(el, "Name");
      String progExt = getChildElementText(el, "ProcExt");
      if (progExt == null)
      {
         progExt = "p";
      }
      
      String path = getChildElementText(el, "ProcPath");
      path = path.replaceAll("\\\\", File.separator);
      path = Paths.get(path).normalize().toString();
      if (!path.isEmpty() && !path.endsWith(File.separator))
      {
         path = path + File.separatorChar;
      }
      String fullName = path + name;
      fullName = fullName.replaceAll("\\\\", File.separator);
      
      if (fullName.startsWith(".."))
      {
         String p = getChildElementText(el, "ProcPath");
         throw new RuntimeException("Error processing file " + file + 
                                    ". Relative paths " + p + " resolved to " + path + " are not allowed.");
      }
      
      if (useFullName)
      {
         throw new RuntimeException("useFullName attribute is not supported for " + fullName);
      }
      
      boolean legacyNames = Configuration.getParameter("proxy-legacy-names", false);
      
      NameConverter nc = new NameConverter();
      String javaName = legacyNames ? appObject : nc.convert(appObject, MatchPhraseConstants.TYPE_CLASS);

      return new LegacyProxyClientConfig(path, 
                                         name, 
                                         fullName, 
                                         progExt, 
                                         procType, 
                                         appObject, 
                                         javaName, 
                                         pkg, 
                                         allowUnknown);
   }
   
   /**
    * For a <code>Procedure</code> element in a .wsm file, associate it with one or more SOAP operations.
    * If the program is ran persistent, all its internal entries are exposed.
    * 
    * @param    cfg
    *           The parent SOAP configuration instance.
    * @param    el
    *           The <code>Procedure</code> XML element.
    *           
    * @return   The list of exported SOAP operations for this procedure.
    */
   private List<SoapOperation> readSoapOperations(SoapConfig cfg, Element el)
   {
      List<SoapOperation> res = new ArrayList<>();
      
      String name = getChildElementText(el, "Name");
      String folder = getChildElementText(el, "ProcPath");
      folder = folder.replace('\\', '/');
      String ext = getChildElementText(el, "ProcExt"); // .cls files are not supported 
      if (ext == null)
      {
         ext = "p";
      }
      String opName = getChildElementText(el, "Name");
      String operation = opName.toLowerCase();
      String procDetailOp = opName;
      String file = folder + 
                    (folder.isEmpty() || folder.endsWith("/") ? "" : "/") + name + 
                    (ext.isEmpty() ? "" : ".") + ext;
      
      String proctype = getChildElementText(el, "ProcType");
      boolean isPersistent = "true".equalsIgnoreCase(el.getAttribute("isPersistent"));
      // single-run or singleton are allowed only for .NET and Java clients
      boolean isSingleRun = isPersistent && "3".equals(proctype);
      boolean isSingleton = isPersistent && "4".equals(proctype);
      isPersistent = isPersistent && "2".equals(proctype);

      // TODO: other attributes?
      // IsMappedToSubmit="false"
      // IsTTResultSet="true" 
      // isCustomized="false"
      // usesBeforeImageDefault="true"
      // usesTTMappingDefault="true"
      // usesUnknownDefault="true"

      NodeList internalProcs = null;
      NodeList details = el.getElementsByTagName("ProcDetail");
      if (details != null && details.getLength() > 0)
      {
         Element procDetail = (Element) details.item(0);
         String procDetailName = getChildElementText(procDetail, "Name");
         if (procDetail != null && !procDetailName.isEmpty())
         {
            procDetailOp = procDetailName;
         }
         internalProcs = procDetail.getElementsByTagName("InternalProc");
      }
      SoapOperation rootOperation = new SoapOperation(cfg, file, operation, procDetailOp, isPersistent);
      rootOperation.createParameterMap(details, "ProcDetail");
      res.add(rootOperation);
      
      if (isPersistent && internalProcs != null)
      {
         // internal entries are processed only if the procedure is persistent
         
         for (int j = 0; j < internalProcs.getLength(); j++)
         {
            Element internalProc = (Element) internalProcs.item(j);
            String iename = getChildElementText(internalProc, "Name");
            // 1 - proc, 2 - func; track only if isExcluded="true"
            String ietype = getChildElementText(internalProc, "ProcType");
            String excluded = getChildElementText(internalProc, "isExcluded");
            
            // parameters and return value are computed from the internal entry definition
            SoapOperation subOperation = new SoapOperation(cfg, rootOperation, iename);
            NodeList subDetails = el.getElementsByTagName("MethodDetail");
            if (subDetails != null && subDetails.getLength() > 0)
            {
               subOperation.createParameterMap(subDetails, "MethodDetail");
            }
            subOperation.excluded = "true".equalsIgnoreCase(excluded);
            res.add(subOperation);
         }
      }
      
      return res;
   }
   
   /**
    * From the specified .wsm file, with the defining 4GL programs exported as SOAP operations, create a 
    * configuration.
    * 
    * @param    f
    *           The .wsm file with the exported 4GL programs for SOAP operations.
    * 
    * @return   The SOAP configuration to use during conversion.
    * 
    * @throws   Exception
    *           If there are any problems loading or analyzing the XML document.
    */
   private SoapConfig createSoapConfig(File f)
   throws Exception
   {
      SoapConfig cfg = new SoapConfig();
      SoapWsdl wsdl = cfg.getWsdl();
      
      Document dom = XmlHelper.parse(new FileInputStream(f));
      
      Element root = dom.getDocumentElement();
      root = (Element) root.getElementsByTagName("AppObject").item(0);
      
      {
         Map<String, String> configs = new HashMap<>();
         readRecursively(null, root, configs);
         cfg.putConfigs(configs);
         
         String author = cfg.getConfig("PGGenInfo/Author");
         String targetNamespace = cfg.getConfig("DeploymentWizard/WebServiceNamespace");
         wsdl.initialize(author, targetNamespace);
         
         wsdl.setHelpString(cfg.getConfig("HelpString"));
         wsdl.setEndpoint(cfg.getConfig("DeploymentWizard/SoapEndpointURL"));
         wsdl.setSoapAction(cfg.getConfig("DeploymentWizard/SoapAction"));
         if ("true".equals(cfg.getConfig("DeploymentWizard/SoapAction#appendToSoapAction")))
         {
            wsdl.setAppendObjNameToSoapAction(true);
         }
         if ("true".equals(cfg.getConfig("DeploymentWizard/PortTypeBindingSuffix#userDefined")))
         {
            wsdl.setPortTypeBindingSuffix(cfg.getConfig("DeploymentWizard/PortTypeBindingSuffix"));
         }
         if ("true".equals(cfg.getConfig("DeploymentWizard/ServiceSuffix#userDefined")))
         {
            wsdl.setServiceSuffix(cfg.getConfig("DeploymentWizard/ServiceSuffix"));
         }
      }

      String defaultService = cfg.getConfig("Name");
      wsdl.addService(defaultService, null);
      
      // read all children recursively and process them
      NodeList children = root.getChildNodes();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node node  = children.item(i);
         if (node.getNodeType() == Node.ELEMENT_NODE)
         {
            Element el = (Element) node;
            String nodeName = el.getNodeName();
            
            if (nodeName.equals("Procedure"))
            {
               List<SoapOperation> ops = readSoapOperations(cfg, el);
               
               cfg.addOperations(ops);

               for (SoapOperation subOp : ops)
               {
                  subOp.service = subOp.persistent ? subOp.operation : defaultService;
                  if (subOp.persistent)
                  {
                     subOp.parentService = defaultService;
                  }
               }
            }
            else if (nodeName.equals("SubAppObject"))
            {
               String subService = getChildElementText(el, "Name");
               wsdl.addService(subService, defaultService);
               
               NodeList subChildren = el.getElementsByTagName("Procedure");
               for (int j = 0; j < subChildren.getLength(); j++)
               {
                  Element subEl = (Element) subChildren.item(j);
                  List<SoapOperation> subOps = readSoapOperations(cfg, subEl);
                  cfg.addOperations(subOps);
                  
                  for (SoapOperation subOp : subOps)
                  {
                     subOp.service = subOp.persistent ? subOp.operation : subService;
                     if (subOp.persistent)
                     {
                        subOp.parentService = subService;
                     }
                  }
               }
            }
            else if (el.getChildNodes().getLength() > 0)
            {
               // get all nodes and add their configuration recursively
               Map<String, String> configs = new HashMap<>();
               readRecursively(nodeName, el, configs);
               
               if (configs.isEmpty())
               {
                  String val = getChildElementText(root, el.getNodeName());
                  if (!val.isEmpty())
                  {
                     cfg.putConfig(el.getNodeName(), val);
                  }
                  NamedNodeMap attrs = el.getAttributes();
                  for (int j = 0; j < attrs.getLength(); j++)
                  {
                     Node attr = attrs.item(j);
                     cfg.putConfig(el.getNodeName() + "#" + attr.getNodeName(), attr.getNodeValue());
                  }
               }
               else
               {
                  cfg.putConfigs(configs);
               }
            }
         }
      }
      
      return cfg;
   }
   
   /**
    * Resolve the web handlers.
    * 
    * @param    file
    *           The web handler configuration, a properties file with the format 
    *           <code>handler#=[qualifiedClassName] : /web/path/{p1}/{p2}</code>, where 
    *           <code>handler#</code> is handler1, handler2, etc.
    * 
    * @return   The map of WebHandler classes to their paths, in proper order.
    */
   private static Map<String, List<WebHandlerConfig>> resolveWebHandlers(String file)
   throws Exception
   {
      Map<String, List<WebHandlerConfig>> webHandlers = new HashMap<>();
      
      IniFile handlers = new IniFile(file);
      handlers.load();
      
      List<String> webSections = handlers.getSections("(.*).web");
      for (String section : webSections)
      {
         int num = 1;
         while (true)
         {
            String name = "handler" + num;
            String cfg = handlers.getString(section, name, null);
            if (cfg == null)
            {
               break;
            }
            
            String cls = cfg.substring(0, cfg.indexOf(':')).trim();
            String path = cfg.substring(cfg.indexOf(':') + 1).trim();
            
            cls = cls.toLowerCase();
            
            List<WebHandlerConfig> paths = webHandlers.get(cls);
            if (paths == null)
            {
               webHandlers.put(cls, paths = new ArrayList<>());
            }
            paths.add(new WebHandlerConfig(section, cls, num, path));

            num = num + 1;
         }
      }
      
      return webHandlers;
   }
   
   /**
    * Resolve the REST operations from one or more <code>.paar</code> archives.
    * 
    * @param    file
    *           A <code>.paar</code> archive or a directory in which to find one or more {@code .paar} files.
    *           
    * @return   A map of defined operations.
    */
   private static Map<String, RestOperation> resolveRestConfiguration(String file)
   throws Exception
   {
      Map<String, RestOperation> operations = new HashMap<>();
      
      File input = new File(file);
      
      if (input.exists())
      {
         if (input.isFile())
         {
            parsePaar(input, operations);
         }
         else
         {
            File[] files = null;
            
            // must be a directory
            if (input.isDirectory())
            {
               files = input.listFiles((dir, name) -> name.endsWith(".paar"));
               
               for (File paar : files)
               {
                  parsePaar(paar, operations);
               }
            }
         }
      }
      
      return operations;
   }
   
   /**
    * Resolve the REST operations from the given <code>.paar</code> archive.
    * 
    * @param    file
    *           A <code>.paar</code> archive.
    * @param    operations
    *           A map of defined operations.
    */
   private static void parsePaar(File file, Map<String, RestOperation> operations)
   throws Exception
   {
      ZipFile paar = new ZipFile(file);
      
      try
      {
         Enumeration<? extends ZipEntry> entries = paar.entries();
         Document resourceModel = null;
         Document mapping = null;
         Document service = null;
         Document spring  = null;
         
         while (entries.hasMoreElements())
         {
             ZipEntry entry = entries.nextElement();
             InputStream stream = paar.getInputStream(entry);
             
             if (entry.getName().equals("resourceModel.xml"))
             {
                resourceModel = XmlHelper.parse(stream);
             }
             else if (entry.getName().equals("mapping.xml"))
             {
                mapping = XmlHelper.parse(stream);
             }
             else if (entry.getName().endsWith(".restoe"))
             {
                service = XmlHelper.parse(stream);
             }
             else if (entry.getName().equals("spring.xml"))
             {
                spring = XmlHelper.parse(stream);
             }
         }
         
         List<RestParameterMapping> mappings  = loadMapping(mapping);
         String                     address   = loadSpring(spring);
         List<RestResource>         resources = loadResources(resourceModel, mappings, address);
         
         resolveServices(resources, service);
         
         for (RestResource rest : resources)
         {
            for (String op : rest.operations.keySet())
            {
               RestOperation restOper = rest.operations.get(op);
               restOper.rest = rest;
               
               if (operations.containsKey(op))
               {
                  throw new RuntimeException("Duplicate REST operation " + op);
               }
               
               operations.put(op, restOper);
            }
         }
      }
      
      catch (Exception e)
      {
         LOG.log(Level.WARNING,"", e);
      }
      
      finally
      {
         paar.close();
      }
   }
   
   /**
    * Given a root XML element, load the entire XML structure in a map, where the keys will be:
    * <ul>
    *    <li>the '/' separated path from the parent node</li>
    *    <li>the '/' separated path from the parent node followed by the '#' char and the attribute name,
    *        for element's attributes.</li>
    * </ul>
    * 
    * @param    parentNode
    *           The parent node name.
    * @param    root
    *           The root element.
    * @param    cfgs
    *           The map where to save the XML texts (from nodes or attributes).
    */
   static void readRecursively(String parentNode, Element root, Map<String, String> cfgs)
   {
      NodeList children = root.getChildNodes();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node child = children.item(i);
         if (child.getNodeType() == Node.ELEMENT_NODE)
         {
            String childNode = (parentNode == null ? "" : (parentNode + "/")) + child.getNodeName();
            Element elChild = (Element) child;
            
            String txt = getChildElementText(root, child.getNodeName());
            
            if (!txt.isEmpty())
            {
               cfgs.put(childNode, txt);
            }
            
            readRecursively(childNode, elChild, cfgs);
            
            NamedNodeMap attrs = elChild.getAttributes();
            for (int j = 0; j < attrs.getLength(); j++)
            {
               Node attr = attrs.item(j);
               cfgs.put(childNode + "#" + attr.getNodeName(), attr.getNodeValue());
            }
         }
      }
   }
   
   /**
    * Get the TEXT value, from the child with the given tag.
    * 
    * @param    parent
    *           The parent element.
    * @param    tag
    *           The child's tag.
    *           
    * @return   The child's text content.
    */
   private static String getChildElementText(Element parent, String tag)
   {
      NodeList children = parent.getElementsByTagName(tag);
      if (children == null || children.getLength() == 0)
      {
         return null;
      }
      
      Element el = (Element) children.item(0);
      NodeList nl = el.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++)
      {
         Node n = nl.item(i);
         if (n.getParentNode() != el)
         {
            continue;
         }
         
         if (n.getNodeType() == Node.TEXT_NODE)
         {
            return ((Text) n).getData();
         }
      }
      
      return "";
   }
   
   /**
    * Resolve the services from the <code>.restoe</code> file.
    * <p>
    * This associates the resources from <code>resourceModel.xml</code> with the legacy definitions.
    * 
    * @param    resources
    *           The defined REST resources.
    * @param    dom
    *           The DOM document.
    */
   private static void resolveServices(List<RestResource> resources, Document dom)
   {
      java.util.function.Function<Element, RestLegacyParameter> createLegacyParam = 
      (elParam) -> 
      {
         int ordinal = Integer.parseInt(elParam.getAttribute("ordinal"));
         String pname = getChildElementText(elParam, "wsad:Name");
         String pOriName = getChildElementText(elParam, "wsad:OrigName");
         int ptype = Integer.parseInt(getChildElementText(elParam, "wsad:Type"));
         int pmode = Integer.parseInt(getChildElementText(elParam, "wsad:Mode"));
         boolean useUnknown = "true".equalsIgnoreCase(elParam.getAttribute("allowUnknown"));
         
         return new RestLegacyParameter(ordinal, pname, pOriName, ptype, pmode, useUnknown);
      };

      Element root = dom.getDocumentElement();
      NodeList nl = root.getElementsByTagName("pidl:binding");
      Element el = (Element) nl.item(0);
      nl = el.getElementsByTagName("wsad:Operation");
      for (int i = 0; i < nl.getLength(); i++)
      {
         el = (Element) nl.item(i);
         
         String name = getChildElementText(el, "wsad:Name");
         String folder = getChildElementText(el, "wsad:ProcPath");
         folder = folder.replace('\\', '/');
         String ext = getChildElementText(el, "wsad:ProcExt");
         boolean isClass = "cls".equals(ext);
         Element procDetail = (Element) el.getElementsByTagName("wsad:ProcDetail").item(0);
         NodeList internalProcs = procDetail.getElementsByTagName("wsad:InternalProc");
         boolean extProgAsService = internalProcs.getLength() == 0;
         
         String opName = getChildElementText(procDetail, "wsad:Name");
         String operation = opName.toLowerCase();

         // TODO: other attributes?
         // IsMappedToSubmit="false"
         // IsTTResultSet="true" 
         // isCustomized="false"
         // usesBeforeImageDefault="true"
         // usesTTMappingDefault="true"
         // usesUnknownDefault="true"
         if (!isClass && extProgAsService)
         {
            String file = folder + 
                          (folder.isEmpty() || folder.endsWith("/") ? "" : "/") + name + 
                          (ext.isEmpty() ? "" : ".") + ext;
            
            boolean useRetVal = "true".equalsIgnoreCase(procDetail.getAttribute("useRetVal"));
            RestLegacyParameter retVal = null;
            if (useRetVal)
            {
               Element elRetVal = (Element) procDetail.getElementsByTagName("wsad:ReturnValue").item(0);
               retVal = createLegacyParam.apply(elRetVal);
            }
            NodeList params = procDetail.getElementsByTagName("wsad:Parameter");
            List<RestLegacyParameter> legacyParams = new ArrayList<>(params.getLength());
            for (int p = 0; p < params.getLength(); p++)
            {
               Element elParam = (Element) params.item(p);
               legacyParams.add(createLegacyParam.apply(elParam));
            }
            
            boolean found = false;
            for (RestResource rest : resources)
            {
               RestOperation restOp = rest.operations.get(operation);
               if (restOp != null)
               {
                  found = true;
                  
                  restOp.source = file;
                  restOp.isClass = false;
                  restOp.retVal = retVal;
                  restOp.legacyParams = legacyParams;
                  break;
               }
            }

            if (!found)
            {
               throw new RuntimeException("Can not find model operation for " + operation + "!");
            }
         }
         
         for (int j = 0; j < internalProcs.getLength(); j++)
         {
            Element internalProc = (Element) internalProcs.item(j);
            String iename = getChildElementText(internalProc, "wsad:Name");
            String proctype = getChildElementText(internalProc, "wsad:ProcType");
            
            Element methodDetail = 
              (Element) internalProc.getElementsByTagName("wsad:MethodDetail").item(0);
            
            String mname = getChildElementText(methodDetail, "wsad:Name");
            boolean useRetVal = "true".equalsIgnoreCase(methodDetail.getAttribute("useRetVal"));
            RestLegacyParameter retVal = null;
            if (useRetVal)
            {
               Element elRetVal = 
                  (Element) methodDetail.getElementsByTagName("wsad:ReturnValue").item(0);
               retVal = createLegacyParam.apply(elRetVal);
            }
            
            NodeList params = methodDetail.getElementsByTagName("wsad:Parameter");
            List<RestLegacyParameter> legacyParams = new ArrayList<>(params.getLength());
            for (int p = 0; p < params.getLength(); p++)
            {
               Element elParam = (Element) params.item(p);
               legacyParams.add(createLegacyParam.apply(elParam));
            }

            String ioperation = (opName + ".." + iename).toLowerCase();

            boolean found = false;
            for (RestResource rest : resources)
            {
               RestOperation restOp = rest.operations.get(ioperation);
               if (restOp != null)
               {
                  found = true;
                  
                  restOp.source = name;
                  restOp.isClass = true;
                  restOp.retVal = retVal;
                  restOp.legacyParams = legacyParams;
                  restOp.methodName = mname;
                  break;
               }
            }
            
            if (!found)
            {
               LOG.log(Level.WARNING, "Cannot find model operation for " + ioperation + "!");
            }
         }
      }
   }
   
   /**
    * Load the defined REST resources from the given DOM (a <code>resourceModel.xml</code> source).
    * <p>
    * It will resolve any argument mapping, from the attached list, built from 
    * <code>mapping.xml</code>.
    * 
    * @param    dom
    *           The DOM document.
    * @param    mappings
    *           The argument mappings, from <code>mapping.xml</code>.
    * @param    address
    *           The service-level path suffix or {@code null} if none is specified.
    *           
    * @return   The list of all defined REST resources (and their operations).
    */
   private static List<RestResource> loadResources(Document                   dom,
                                                   List<RestParameterMapping> mappings,
                                                   String                     address)
   {
      Scope testScope = new Scope()
      {
         public Scope getEnclosingScope()
         {
            return null;
         }
         public String toString()
         {
            return "test scope";
         }
      };

      SymbolResolver resolver = new SymbolResolver()
      {
         @Override
         public Scope getCurrentScope()
         {
            return testScope;
         }
      };
      Variable v = resolver.registerVariable(testScope, "rest", RestConditionVar.class, null, true);
      RestConditionVar var = new RestConditionVar();
      v.setValue(var);

      List<RestResource> res = new ArrayList<>();
      
      Element root = dom.getDocumentElement();
      NodeList nl = root.getElementsByTagName("prgs:resource");
      for (int i = 0; i < nl.getLength(); i++)
      {
         Element el = (Element) nl.item(i);
         
         RestResource rest = new RestResource(el.getAttribute("name"),
                                              el.getAttribute("path"),
                                              address,
                                              el.getAttribute("consumes"),
                                              el.getAttribute("produces"));
         res.add(rest);
         
         NodeList ops = el.getElementsByTagName("prgs:operation");
         for (int j = 0; j < ops.getLength(); j++)
         {
            el = (Element) ops.item(j);
            RestOperation op = new RestOperation(el.getAttribute("name"),
                                                 el.getAttribute("verb"),
                                                 el.getAttribute("consumes"),
                                                 el.getAttribute("produces"));
            rest.operations.put(op.name.toLowerCase(), op);
            
            NodeList params = el.getElementsByTagName("prgs:param");
            for (int k = 0; k < params.getLength(); k++)
            {
               el = (Element) params.item(k);
               RestParameter param = new RestParameter(el.getAttribute("name"), 
                                                       el.getAttribute("type"));
               op.parameters.put(param.name, param);
            }
            
            // find the mappings for this operation
            var.setOperationname(op.name);
            var.setResourcename(rest.name);
            var.setVerb(op.verb);
            
            for (RestParameterMapping m : mappings)
            {
               Expression expr = new Expression(resolver, m.condition);
               Object result = expr.execute();
               if (result instanceof Boolean && ((Boolean) result).booleanValue())
               {
                  m.operationname = var.getOperationname();
                  m.resourcename = var.getResourcename();
                  m.verb = var.getVerb();
                  op.mappings.add(m);
               }
            }
         }
      }
      return res;
   }

   /**
    * Load the mappings from a <code>mapping.xml</code> document.
    * <p>
    * The mapping expressions are not resolved here - they will be disambiguated when the resources
    * are loaded, in {@link #loadResources(Document, List, String)}, by evaluating the expression.
    * 
    * @param    dom
    *           The DOM document.
    *           
    * @return   The list of loaded parameter mappings.
    */
   private static List<RestParameterMapping> loadMapping(Document dom)
   throws Exception
   {
      List<RestParameterMapping> res = new ArrayList<>();
      
      Element root = dom.getDocumentElement();
      NodeList nl = root.getElementsByTagName("mapping:MappingBean");
      Node n = nl.item(0);
      for (int i = 0; i < n.getChildNodes().getLength(); i++)
      {
         Node cdata = n.getChildNodes().item(i);
         if (cdata.getNodeType() == Node.CDATA_SECTION_NODE)
         {
            String txt = cdata.getTextContent();
            dom = XmlHelper.parse(new ByteArrayInputStream(txt.getBytes()));
            break;
         }
      }
      
      root = dom.getDocumentElement();
      for (int i = 0; i < root.getChildNodes().getLength(); i++)
      {
         n = root.getChildNodes().item(i);
         
         if (n.getNodeType() == Node.ELEMENT_NODE)
         {
            Element el = (Element) n;
            boolean input = el.getTagName().equals("mapping:mapInput");
            NodeList conditions = el.getElementsByTagName("conditional:conditionalRuleSet");
            for (int j = 0; j < conditions.getLength(); j++)
            {
               el = (Element) conditions.item(j);
               
               Element elif = (Element) el.getElementsByTagName("conditional:if").item(0);
               NodeList rules = elif.getElementsByTagName("mapping:rule");
               
               RestParameterMapping mapping = new RestParameterMapping(elif.getAttribute("condition"));
               mapping.input = input;
               for (int k = 0; k < rules.getLength(); k++)
               {
                  el = (Element) rules.item(k);
                  RestParameterMappingRule rule = new RestParameterMappingRule(el.getAttribute("source"), 
                                                             el.getAttribute("target"));
                  
                  mapping.rules.add(rule);
               }
               
               res.add(mapping);
            }
         }
      }
      
      return res;
   }

   /**
    * Load the configuration from a <code>spring.xml</code> document.
    * <p>
    * The only value processed is the "address" attribute of element "prgrs:prgRsServer". 
    * 
    * @param    dom
    *           The DOM document.
    *           
    * @return   The service-level path suffix or {@code null} if none exists.
    */
   private static String loadSpring(Document dom)
   throws Exception
   {
      if (dom == null)
      {
         return null;
      }
      
      Element root = dom.getDocumentElement();
      
      NodeList nl = root.getElementsByTagName("prgrs:prgRsServer");
      
      String address = null;
      
      if (nl != null)
      {
         Node n = nl.item(0);
         
         if (n != null && n.getNodeType() == Node.ELEMENT_NODE)
         {
            Element el = (Element) n;
            address = el.getAttribute("address");
         }
      }
      
      return address;
   }

   /**
    * Find all SOAP operations configured in the .wsm document(s), for this external program and internal
    * entry, or class and method.
    * 
    * @param    program
    *           The program or class name.
    * @param    internalEntry
    *           The internal entry or method name.  May be <code>null</code>.
    *           
    * @return   The list of SOAP operations exporting this program and internal entry.
    */
   private List<SoapOperation> getSoapOperation(String program, String internalEntry)
   {
      return getSoapOperation(null, program, internalEntry);
   }

   /**
    * Find all SOAP operations configured in the .wsm document(s), for this external program and internal
    * entry, or class and method.
    * 
    * @param    file
    *           The file name.
    * @param    program
    *           The program or class name.
    * @param    internalEntry
    *           The internal entry or method name.  May be <code>null</code>.
    *           
    * @return   The list of SOAP operations exporting this program and internal entry.
    */
   private List<SoapOperation> getSoapOperation(String file, String program, String internalEntry)
   {
      List<SoapOperation> found = new ArrayList<>();
      
      for (String cfg : soapConfigs.keySet())
      {
         SoapConfig soap = soapConfigs.get(cfg);
         List<SoapOperation> operation = soap.getSoapOperation(file, program, internalEntry);
         
         found.addAll(operation);
      }
      
      return found;
   }
   
   /**
    * Register the specified internal entry as a SOAP operation in the given persistent program.
    * 
    * @param    program
    *           The program name.
    * @param    internalEntry
    *           The internal entry name.
    */
   private void registerSoapOperation(String program, String internalEntry)
   {
      List<SoapOperation> children = getSoapOperation(program, internalEntry);
      List<SoapOperation> parents = getSoapOperation(program, null);

      parents.forEach(parOp ->
      {
         if (!parOp.persistent)
         {
            return;
         }
         
         boolean[] found = new boolean[1];
         children.forEach(chOp -> found[0] = found[0] || chOp.parent == parOp);
         
         if (!found[0])
         {
            SoapOperation child = new SoapOperation(parOp.soap, parOp, internalEntry);
            child.soap.addOperation(child);
            children.add(child);
         }
         
         children.forEach(child->
         {
            child.service = parOp.service;
            child.soap.getWsdl().addService(child.service, null);
         });
      });
   }

   /**
    * Resolve the proxy program configuration for the given file.
    * 
    * @param    file
    *           The proxy program file.
    *           
    * @return   See above.
    */
   private LegacyProxyClientConfig getProxyProgramConfig(String file)
   {
      LegacyProxyClientConfig found = null;
      for (LegacyProxyConfig lpc : context.get().proxyConfigs)
      {
         LegacyProxyClientConfig cfg = lpc.getProgram(file);
         if (cfg != null)
         {
            if (found != null)
            {
               throw new IllegalStateException("More than one proxy config found for " + file);
            }
            found = cfg;
         }
      }
      
      if (found == null)
      {
         throw new NullPointerException("Proxy config not found for file " + file);
      }
      
      return found;
   }

   /**
    * Resolve the proxy configuration for the given file.
    * 
    * @param    file
    *           The proxy program file.
    *           
    * @return   See above.
    */
   private LegacyProxyConfig getProxyConfig(String file)
   {
      LegacyProxyConfig found = null;
      for (LegacyProxyConfig lpc : context.get().proxyConfigs)
      {
         LegacyProxyClientConfig cfg = lpc.getProgram(file);
         if (cfg != null)
         {
            if (found != null)
            {
               throw new IllegalStateException("More than one proxy config found for " + file);
            }
            found = lpc;
         }
      }
      
      if (found == null)
      {
         throw new NullPointerException("Proxy config not found for file " + file);
      }
      
      return found;
   }
   
   /**
    * Exposes APIs to resolve services (REST, etc) for a certain program or OO class.
    */
   public class Library
   {
      /**
       * Get the state of the {@link WorkArea#brewProxy} flag.
       * 
       * @return   See above.
       */
      public boolean isBrewProxy()
      {
         return context.get().brewProxy;
      }
      
      /**
       * Get the JAST for the specified (Sub)AppObject.
       * 
       * @param    appObject
       *           The (Sub)AppObject name.
       *           
       * @return   The AST or <code>null</code> if it was not yet registered.
       */
      public Aast getAppObjectJast(String appObject)
      {
         return context.get().appObjectJast.get(appObject);
      }
      
      /**
       * Register the JAST for the specified (Sub)AppObject.
       * 
       * @param    appObject
       *           The (Sub)AppObject name.
       * @param    ast
       *           The Java AST.
       */
      public void registerAppObjectJast(String appObject, Aast ast)
      {
         context.get().appObjectJast.put(appObject, ast);
      }
      
      /**
       * Check if the given proxy program is ran PERSISTENT.
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public boolean isProxyPersistentProgram(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg != null && cfg.isPersistent();
      }
      
      /**
       * Check if the given proxy program is ran SINGLETON.
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public boolean isProxySingletonProgram(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg != null && cfg.isSingleton();
      }
      
      /**
       * Check if the given proxy program is ran SINGLE-RUN.
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public boolean isProxySingleRunProgram(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg != null && cfg.isSingleRun();
      }
      
      /**
       * Check if the given proxy is tracking datagraph changes by default.
       * 
       * @param    file
       *           The proxy program file.
       *
       * @return   See above.
       */
      public boolean isProxyTrackChanges(String file)
      {
         LegacyProxyConfig lpc = getProxyConfig(file);
         return lpc.isTrackChanges();
      }
      
      /**
       * Check if the given proxy program allows unknown arguments.
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public boolean isProxyAllowUnknown(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg != null && cfg.isAllowUnknown();
      }
      
      /**
       * Get the Java name of the (Sub)AppObject where this program is configured
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public String getProxyAppObjectName(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg == null ? null : cfg.getAppObject();
      }
      
      /**
       * Get the legacy name of the (Sub)AppObject where this program is configured
       * 
       * @param    file
       *           The proxy program file.
       *           
       * @return   See above.
       */
      public String getProxyLegacyAppObjectName(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg == null ? null : cfg.getLegacyName();
      }
      
      /**
       * Get the Java package under which the legacy open client proxy Java class will be generated.
       * 
       * @param    file
       *           The full name for the external program.
       *           
       * @return   The package name.
       */
      public String getProxyPackage(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         return cfg == null ? null : cfg.getPkg();
      }
      
      /**
       * Get the folder for Java package under which the legacy open client proxy Java class will be generated.
       * 
       * @param    file
       *           The full name for the external program.
       *           
       * @return   The package folder.
       */
      public String getProxyPackageFolder(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         String pkg = cfg.getPkg() + "." + cfg.getAppObject();
         pkg = pkg.replace('.', File.separatorChar);
         
         return pkg;
      }
      
      /**
       * Get the folder under which this external program was initially configured as being defined.
       * 
       * @param    file
       *           The full name for the external program.
       *           
       * @return   The folder name from the .xpxg configuration for this external program.
       */
      public String getProxyFolder(String file)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         
         if (cfg == null)
         {
            return null;
         }

         String res = cfg.getFolder();
         
         while (res.endsWith(File.separator) || res.endsWith("/") || res.endsWith("\\"))
         {
            res = res.substring(0, res.length() - 1);
         }
         
         return res;
      }
      
      /**
       * Register the given JAST file name with its associated external program (which is configured as a 
       * legacy open client proxy).
       * 
       * @param    file
       *           The full name for the external program.
       * @param    proxyAstFilename
       *           The full JAST file name.
       */
      public void registerProxyJavaAst(String file, String proxyAstFilename)
      {
         LegacyProxyClientConfig cfg = getProxyProgramConfig(file);
         cfg.setAstFilename(proxyAstFilename);
      }
      
      /**
       * Register the generated proxy class name.
       * 
       * @param    file
       *           The proxy file.
       * @param    pkg
       *           The package.
       * @param    className
       *           The class name.
       */
      public void registerProxyClass(String file, String pkg, String className)
      {
         WorkArea wa = context.get();
         
         className = (pkg.isEmpty() ? "" : (pkg + ".")) + className;
         
         String prev = wa.proxyClasses.get(className);
         if (prev != null)
         {
            System.out.println("WARNING: proxy class " + className + " already exists for " + file + 
                               ", first found at " + prev);
         }
         else
         {
            wa.proxyClasses.put(className, file);
         }
      }
      
      /**
       * Check if the given external program file (as a full path) is configured as a legacy open client proxy
       * program.
       * 
       * @param     file
       *            The full path for the program.
       *            
       * @return    <code>true</code> if the program exists in any of the .xpxg configurations in 
       *            {@link WorkArea#proxyConfigs}.
       */
      public boolean isLegacyProxyClient(String file)
      {
         WorkArea wa = context.get();
         if (wa.proxyConfigs == null)
         {
            return false;
         }
         
         for (LegacyProxyConfig cfg : wa.proxyConfigs)
         {
            if (cfg.getProgram(file) != null)
            {
               return true;
            }
         }
         
         return false;
      }
      
      /**
       * Generate all WSDL files and save the in the root Java package. 
       */
      public void generateSoapWsdl()
      {
         String pkgroot = Configuration.getParameter("pkgroot");
         pkgroot = pkgroot.replace('.', File.separatorChar);
         String output = Configuration.getParameter("output-root");
         String path = String.format("%s%s%s%s", output, File.separator, pkgroot, File.separator);

         for (SoapConfig cfg : soapConfigs.values())
         {
            cfg.generateWsdl(path);
            
            cfg.checkOperations();
         }
         
         // cleanup
         soapConfigs.clear();
      }
      
      /**
       * For this function or method, set its return type.
       * 
       * @param    program
       *           The program defining the internal entry or method.
       * @param    internalEntry
       *           The internal entry or method.
       * @param    type
       *           The return token type.
       */
      public void setSoapReturnType(String program, String internalEntry, long type)
      {
         List<SoapOperation> operation = getSoapOperation(program, internalEntry);
         operation.forEach(op -> op.retValType = (int) type);
      }
      
      /**
       * Add a SOAP operation for the specified program and internal entry.
       * 
       * @param    file
       *           The program file.
       * @param    program
       *           The program name.
       * @param    internalEntry
       *           The internal entry name.
       */
      public void addSoapOperation(String file, String program, String internalEntry)
      {
         List<SoapOperation> operation = getSoapOperation(file, program, internalEntry);
         operation.forEach(op -> 
         {
            op.registered = true;
            op.soap.getWsdl().addOperation(op);
         });
      }
      
      /**
       * Add a SOAP parameter for the specified program and internal entry.
       * 
       * @param    program
       *           The program name.
       * @param    internalEntry
       *           The internal entry name.
       * @param    parameter
       *           The parameter AST.
       */
      public void addSoapParameter(String program, String internalEntry, Aast parameter)
      {
         List<SoapOperation> operation = getSoapOperation(program, internalEntry);
         operation.forEach(op -> op.addParameter(parameter));
      }
      
      /**
       * Add a SOAP table parameter for the specified program and internal entry.
       * 
       * @param    program
       *           The program name.
       * @param    internalEntry
       *           The internal entry name.
       * @param    parameter
       *           The parameter AST.
       * @param    dictionary
       *           The schema dictionary to resolve the table fields.
       */
      public void addSoapTableParameter(String           program, 
                                        String           internalEntry, 
                                        Aast             parameter, 
                                        SchemaDictionary dictionary)
      {
         List<SoapOperation> operation = getSoapOperation(program, internalEntry);
         operation.forEach(op -> op.addTableParameter(parameter, dictionary));
      }
      
      /**
       * Add a SOAP dataset parameter for the specified program and internal entry.
       * 
       * @param    program
       *           The program name.
       * @param    internalEntry
       *           The internal entry name.
       * @param    parameter
       *           The parameter AST.
       * @param    dictionary
       *           The schema dictionary to resolve the table fields.
       */
      public void addSoapDataSetParameter(String           program, 
                                          String           internalEntry, 
                                          Aast             parameter, 
                                          SchemaDictionary dictionary)
      {
         List<SoapOperation> operation = getSoapOperation(program, internalEntry);
         operation.forEach(op -> op.addDataSetParameter(parameter, dictionary));
      }

      /**
       * Get all the defined SOAP operations.
       * 
       * @param    file
       *           The file name.
       * @param    program
       *           The program name.
       * @param    internalEntry
       *           The internal entry name.
       *           
       * @return   The list of SOAP operations exported by this program.
       */
      public List<SoapOperation> getSoapOperations(String file, String program, String internalEntry)
      {
         if (internalEntry != null)
         {
            registerSoapOperation(program, internalEntry);
         }
         
         List<SoapOperation> operation = getSoapOperation(file, program, internalEntry);
         
         // if an internal entry was specified, check if any parent is persistent and the operation
         // is not excluded
         if (internalEntry != null)
         {
            Set<SoapOperation> parentOps = new HashSet<>();
            Iterator<SoapOperation> iter = operation.iterator();
            while (iter.hasNext())
            {
               SoapOperation op = iter.next();
               
               // the program must be persistent and the internal entry must not be ignored
               if (op.parent.persistent && !op.excluded)
               {
                  parentOps.add(op.parent);
               }
               else
               {
                  // remove anything else
                  iter.remove();
               }
            }
            
            // get all persistent external programs which have this op
            List<SoapOperation> progOpers = getSoapOperation(file, program, null);
            iter = progOpers.iterator();
            while (iter.hasNext())
            {
               SoapOperation progOp = iter.next();
               
               if (progOp.persistent && !parentOps.contains(progOp))
               {
                  // if the internal entry is not found as a customized operation, then is included
                  // only if the external program is persistent
                  operation.add(progOp);
               }
            }
         }
         else
         {
            // for each persistent operation, we need CreatePO_ and Release_ versions
            List<SoapOperation> others = new ArrayList<>();
            Iterator<SoapOperation> iter = operation.iterator();
            while (iter.hasNext())
            {
               SoapOperation op = iter.next();
               if (op.persistent)
               {
                  SoapOperation createPO = new SoapOperation(op);
                  createPO.operation = "CreatePO_" + createPO.operation;
                  others.add(createPO);

                  SoapOperation releasePO = new SoapOperation(op);
                  releasePO.operation = "Release_" + releasePO.operation;
                  releasePO.parentService = releasePO.service; // this needs to be the same.
                  others.add(releasePO);
                  
                  iter.remove();
               }
            }
            
            operation.addAll(others);
         }
         
         return operation;
      }
      
      /**
       * Check if the given qualified legacy class name is configured as a WebHandler.\
       * 
       * @param    cls
       *           The qualified class name.
       *           
       * @return   See above.
       */
      public boolean isWebHandler(String cls)
      {
         return webHandlers.containsKey(cls.toLowerCase());
      }
      
      /**
       * Get the section where this legacy class is configured as a web handler.
       * 
       * @param    cls
       *           The legacy qualified class name.
       *           
       * @return   The section name.
       */
      public String getSection(String cls)
      {
         String section = null;
         for (WebHandlerConfig cfg : webHandlers.get(cls.toLowerCase()))
         {
            if (section == null)
            {
               section = cfg.getSection();
            }
            else if (!section.equals(cfg.getSection()))
            {
               throw new RuntimeException("Class " + cls + " is part of multiple WebHandler " +
                                          "configurations: " + section + " and " + 
                                          cfg.getSection());
            }
         }
         
         return section;
      }
      
      /**
       * Get the paths served by this web handler.
       * 
       * @param    cls
       *           The qualified legacy class name, which has handler configurations.
       *           
       * @return   See above.
       */
      public List<LegacyWebPath> getPaths(String cls)
      {
         List<LegacyWebPath> res = new ArrayList<>();
         
         List<WebHandlerConfig> paths = webHandlers.get(cls.toLowerCase());
         for (WebHandlerConfig cfg : paths)
         {
            res.add(new LegacyWebPath()
            {
               @Override
               public Class<? extends Annotation> annotationType()
               {
                  return LegacyWebPath.class;
               }
               
               @Override
               public String path()
               {
                  return cfg.getPath();
               }
               
               @Override
               public int order()
               {
                  return cfg.getIndex();
               }
            });
         }
         
         return res;
      }
      
      /**
       * Get the parameters associated with a service call.  Not all parameters (as at the 
       * definition) are necessarily mapped here.
       * 
       * @param    srv
       *           The service annotaiton.
       *           
       * @return   The list of parameters.
       */
      public List<LegacyServiceParameter> getParameters(LegacyService srv)
      {
         return Arrays.asList(srv.parameters());
      }
      
      /**
       * Check if there is a service with the given name.
       * 
       * @param    serviceName
       *           The service name.
       *           
       * @return   See above.
       */
      public boolean isRestService(String serviceName)
      {
         return operations.containsKey(serviceName.toLowerCase());
      }
      
      /**
       * Check if the service is already registered or it has sub-services.
       * 
       * @param    serviceName
       *           The service name.
       *           
       * @return   See above.
       */
      public boolean hasRestSubServices(String serviceName)
      {
         serviceName = serviceName.toLowerCase();
         if (operations.containsKey(serviceName))
         {
            return true;
         }
         else if (serviceName.indexOf("..") >= 0)
         {
            // already a sub-service
            return false;
         }
         
         serviceName = serviceName + "..";
         // check for sub-services (i.e. internal entries)
         for (String key : operations.keySet())
         {
            if (key.startsWith(serviceName))
            {
               return true;
            }
         }
         
         return false;
      }
      
      /**
       * Computes the {@link LegacyService} annotation for the given service.  This will be used
       * by the TRPL code to emit a 4GL-style <code>LegacyService</code> annotation.
       * 
       * @param    serviceName
       *           The REST service.
       *           
       * @return   See above.
       */
      public LegacyService resolveRestAnnotation(String serviceName)
      {
         RestOperation service = operations.get(serviceName.toLowerCase());
         if (service == null)
         {
            return null;
         }
         
         return new LegacyService()
         {
            @Override
            public Class<? extends Annotation> annotationType()
            {
               return LegacyService.class;
            }
            
            @Override
            public String executionMode()
            {
               return "";
            }
            
            @Override
            public String verb()
            {
               return service.verb;
            }
            
            @Override
            public String type()
            {
               return "REST";
            }
            
            @Override
            public String produces()
            {
               return service.produces;
            }
            
            @Override
            public String path()
            {
               return service.rest.path;
            }

            @Override
            public String address()
            {
               return (service.rest.address == null) ? "" : service.rest.address;
            }

            @Override
            public LegacyWebPath[] paths()
            {
               return new LegacyWebPath[0];
            }

            @Override
            public int order()
            {
               return 0;
            }
            
            @Override
            public String binding()
            {
               return "";
            }
            
            @Override
            public String namespace()
            {
               return "";
            }

            @Override
            public LegacyServiceParameter[] parameters()
            {
               boolean hasReturnValue = service.retVal != null;
               int psize = service.legacyParams.size() + (hasReturnValue ? 1 : 0);
               List<LegacyServiceParameter> parameters = new ArrayList<>();

               for (int i = 0; i < psize; i++)
               {
                  boolean isRetVal = hasReturnValue && i == service.legacyParams.size();
                  RestLegacyParameter p =  isRetVal ? service.retVal 
                                                    : service.legacyParams.get(i);
                  
                  RestParameterMappingRule rule = service.findParameterMapping(p);
                  String source;
                  String target;
                  
                  if (rule == null)
                  {
                     if (p.mode.equals("O") || p.mode.equals("U"))
                     {
                        // assume the default 
                        source = null;
                        String mthd = "object";
                        switch (p.type.toLowerCase())
                        {
                           case "logical":
                              mthd = "boolean";
                              break;
                           case "integer":
                              mthd = "integerValue";
                              break;
                           case "int64":
                              mthd = "longValue";
                              break;
                           case "decimal":
                              mthd = "decimalValue";
                              break;
                           case "date":
                           case "datetime":
                           case "datetimetz":
                           case "datetime-tz":
                           case "character":
                              mthd = "string";
                              break;
                        }
                        target = String.format("json.object['response'].%s['%s']", mthd, p.name);
                     }
                     else
                     {
                        LOG.log(Level.WARNING,
                                "Parameter " + p.name + " is not mapped for service " + service.name);
                        continue;
                     }
                  }
                  else
                  {
                     source = rule.source;
                     target = rule.target;
                  }
                  
                  LegacyServiceParameter parameter = new LegacyServiceParameter()
                  {
                     @Override
                     public Class<? extends Annotation> annotationType()
                     {
                        return LegacyServiceParameter.class;
                     }
                     
                     public int ordinal()
                     {
                        return p.ordinal;
                     };
                     
                     @Override
                     public String name()
                     {
                        return p.name;
                     }
                     
                     @Override
                     public String type()
                     {
                        return p.type;
                     };

                     @Override
                     public boolean allowUnknown()
                     {
                        return p.allowUnknown;
                     }
                     
                     @Override
                     public String target()
                     {
                        return target;
                     }
                     
                     @Override
                     public String source()
                     {
                        return source;
                     }
                     
                     @Override
                     public boolean returnValue()
                     {
                        return isRetVal;
                     }
                     
                     @Override
                     public boolean output()
                     {
                        return "O".equals(p.mode) || "U".equals(p.mode);
                     }
                     
                     @Override
                     public boolean input()
                     {
                        return "I".equals(p.mode) || "U".equals(p.mode);
                     }
                     
                     @Override
                     public int extent()
                     {
                        return SourceNameMapper.NO_EXTENT;
                     }

                     public String defaultValue()
                     {
                        return "";
                     }
                  };
                  
                  if (isRetVal)
                  {
                     parameters.add(0, parameter);
                  }
                  else
                  {
                     parameters.add(parameter);
                  }
               }
               return parameters.toArray(new LegacyServiceParameter[0]);
            }
            
            @Override
            public String name()
            {
               return service.name;
            }
            
            @Override
            public String consumes()
            {
               return service.consumes;
            }
         };
      }
   }
   
   /** State which must survive for the duration of the conversion. */
   private static class WorkArea
   {
      /** The list of .xpxg configuration(s), with legacy open client programs. */
      private List<LegacyProxyConfig> proxyConfigs = null;
      
      /** The map of generated proxy class names, including their package, to the proxy file */
      private Map<String, String> proxyClasses = new HashMap<>();
      
      /** The map of JAva ASTs created for (Sub)AppObject. */
      private Map<String, Aast> appObjectJast = new HashMap<>();
      
      /**
       * Flag indicating if the proxy programs are being brewed, so they can be written to 'proxy-output-root'.
       */
      private boolean brewProxy = false;
   }
   
   /**
    * Given a file or folder with .xpxg proxy program configurations, get the list of target programs.
    * 
    * @param    args
    *           A single argument with the folder's or file's location.
    */
   public static void main(String[] args) 
   throws Exception
   {
      ServiceSupport srv = new ServiceSupport();
      
      List<LegacyProxyConfig> proxies = srv.resolveProxyProgramsConfiguration(args[0]);
      
      int count = 0;
      for (LegacyProxyConfig proxy : proxies)
      {
         for (String program : proxy.getProxyProgramFiles())
         {
            count++;
            System.out.println(program);
         }
      }
      
      System.out.println("Found " + count + " programs.");
   }
}