# 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.");
       }
    }