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