Преглед изворни кода

HPCC-25002 ESDL Transform Script enhancements

1. Refactor away from ptree result handling.. allowing access to
   any part of the script context.  Preserving order.  And allowing
   more powerful xpath processing.
2. Refactor away from PTree based script parsing / loading.
   Preserving order.
3. Support changing the target and source locations on the fly.
4. Support builing of structured output.
5. Support calling other web services from within scripts.
6. Support dynamically tokenizing strings.  Iterating over order
   accessing the result.
7. Support building structured variables.
8. Support dynamically adding namespaces to target content.

Signed-off-by: Anthony Fishbeck <anthony.fishbeck@lexisnexisrisk.com>
Anthony Fishbeck пре 4 година
родитељ
комит
a1211e17c5

+ 10 - 2
esp/bindings/SOAP/xpp/xpp/XmlPullParser.h

@@ -174,7 +174,8 @@ namespace xpp {
     }
 
 
-    int skipSubTree()  {
+    bool skipSubTreeEx()  {
+      bool hasChildren = false;
       int level = 1;
       StartTag stag;
       int type = XmlPullParser::END_TAG;
@@ -184,13 +185,20 @@ namespace xpp {
         case XmlPullParser::START_TAG:
           readStartTag(stag);
           ++level;
+          hasChildren = true;
           break;
         case XmlPullParser::END_TAG:
           --level;
           break;
         }
       }
-      return type;
+      return hasChildren;
+    }
+
+    //backward compatability
+    int skipSubTree() {
+      skipSubTreeEx();
+      return XmlPullParser::END_TAG;
     }
 
     const SXT_STRING getPosDesc() const {

+ 25 - 17
esp/bindings/http/client/httpclient.cpp

@@ -640,13 +640,13 @@ int CHttpClient::sendRequest(IProperties *headers, const char* method, const cha
     return static_cast<int>(ret);
 }
 
-HttpClientErrCode CHttpClient::sendRequest(IProperties *headers, const char* method, const char* contenttype, StringBuffer& content, StringBuffer& responseContent, StringBuffer& responseStatus, bool alwaysReadContent, bool forceNewConnection)
+IHttpMessage *CHttpClient::sendRequestEx(const char* method, const char* contenttype, StringBuffer& content, HttpClientErrCode &code, StringBuffer &errmsg, IProperties *headers, bool alwaysReadContent, bool forceNewConnection)
 {
-    StringBuffer errmsg;
     if (connect(errmsg, forceNewConnection) < 0)
     {
-        responseContent.append(errmsg);
-        return HttpClientErrCode::Error;
+        errmsg.append(errmsg);
+        code = HttpClientErrCode::Error;
+        return nullptr;
     }
 
     Owned<CHttpRequest> httprequest;
@@ -655,21 +655,15 @@ HttpClientErrCode CHttpClient::sendRequest(IProperties *headers, const char* met
     httprequest.setown(new CHttpRequest(*m_socket));
     httpresponse.setown(new CHttpResponse(*m_socket));
 
-    httprequest->setMethod(method);
-    httprequest->setVersion("HTTP/1.1");
-
     if(m_proxy.length() <= 0)
-    {
         httprequest->setPath(m_path.get());
-    }
     else
-    {
         httprequest->setPath(m_url.get());
-    }
 
+    httprequest->setMethod(method);
+    httprequest->setVersion("HTTP/1.1");
     httprequest->setHost(m_host.get());
     httprequest->setPort(m_port);
-
     httprequest->setContentType(contenttype);
 
     bool alreadyEncoded = false;
@@ -739,7 +733,10 @@ HttpClientErrCode CHttpClient::sendRequest(IProperties *headers, const char* met
     m_exceptions.setown(MakeMultiException());
     int ret = httpresponse->receive(alwaysReadContent, m_exceptions);
     if (ret < 0 && m_isPersistentSocket && httpresponse->getPeerClosed())
-        return HttpClientErrCode::PeerClosed;
+    {
+        code = HttpClientErrCode::PeerClosed;
+        return nullptr;
+    }
 
 #ifdef COOKIE_HANDLING
     if(m_context)
@@ -756,16 +753,27 @@ HttpClientErrCode CHttpClient::sendRequest(IProperties *headers, const char* met
     }
 #endif
 
-    httpresponse->getContent(responseContent);
-    httpresponse->getStatus(responseStatus);
-
     m_persistable = httpresponse->getPersistentEligible();
     m_numRequests++;
 
+    code = HttpClientErrCode::OK;
+    return httpresponse.getClear();
+}
+
+HttpClientErrCode CHttpClient::sendRequest(IProperties *headers, const char* method, const char* contenttype, StringBuffer& content, StringBuffer& responseContent, StringBuffer& responseStatus, bool alwaysReadContent, bool forceNewConnection)
+{
+    HttpClientErrCode code = HttpClientErrCode::OK;
+    Owned<IHttpMessage> resp = sendRequestEx(method, contenttype, content, code, responseContent, headers, alwaysReadContent, forceNewConnection);
+    if (!resp || code != HttpClientErrCode::OK)
+        return code;
+
+    resp->getContent(responseContent);
+    resp->getStatus(responseStatus);
+
     if (getEspLogLevel()>LogNormal)
         DBGLOG("Response content: %s", responseContent.str());
 
-    return HttpClientErrCode::OK;
+    return code;
 }
 
 int CHttpClient::sendRequest(const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response, StringBuffer& responseStatus, bool alwaysReadContent)

+ 9 - 1
esp/bindings/http/client/httpclient.hpp

@@ -23,6 +23,13 @@
 
 #define HTTP_CLIENT_DEFAULT_CONNECT_TIMEOUT 3000
 
+enum class HttpClientErrCode
+{
+    PeerClosed = -2,
+    Error = -1,
+    OK = 0
+};
+
 interface IHttpClient : extends ITransportClient
 {
     virtual void setProxy(const char* proxy) = 0;
@@ -39,8 +46,9 @@ interface IHttpClient : extends ITransportClient
     virtual int sendRequest(IProperties *headers, const char* method, const char* contenttype, StringBuffer& content, StringBuffer &response, StringBuffer& responseStatus, bool alwaysReadContent = false) = 0;
     virtual int proxyRequest(IHttpMessage *request, IHttpMessage *response, bool resetForwardedFor) = 0;
 
-    virtual int postRequest(ISoapMessage & request, ISoapMessage & response) = 0;
+    virtual IHttpMessage *sendRequestEx(const char* method, const char* contenttype, StringBuffer& content, HttpClientErrCode &code, StringBuffer &errmsg, IProperties *headers = nullptr, bool alwaysReadContent = false, bool forceNewConnection = false) = 0;
 
+    virtual int postRequest(ISoapMessage & request, ISoapMessage & response) = 0;
 };
 
 interface IHttpClientContext : extends IInterface

+ 2 - 7
esp/bindings/http/client/httpclient.ipp

@@ -75,13 +75,6 @@ public:
     virtual IHttpClient* createHttpClient(const char* proxy, const char* url);
 };
 
-enum class HttpClientErrCode
-{
-    PeerClosed = -2,
-    Error = -1,
-    OK = 0
-};
-
 class CHttpClient : implements IHttpClient, public CInterface
 {
 
@@ -122,6 +115,7 @@ private:
 
     HttpClientErrCode sendRequest(const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response, bool forceNewConnection);
     HttpClientErrCode sendRequest(IProperties *headers, const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response, StringBuffer& responseStatus, bool alwaysReadContent, bool forceNewConnection);
+
     HttpClientErrCode proxyRequest(IHttpMessage *request, IHttpMessage *response,  bool forceNewConnection, bool resetForwardedFor);
     HttpClientErrCode postRequest(ISoapMessage &req, ISoapMessage& resp, bool forceNewConnection);
 
@@ -136,6 +130,7 @@ public:
     virtual int sendRequest(const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response);
     virtual int sendRequest(const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response, StringBuffer& responseStatus, bool alwaysReadContent = false);
     virtual int sendRequest(IProperties *headers, const char* method, const char* contenttype, StringBuffer& request, StringBuffer& response, StringBuffer& responseStatus, bool alwaysReadContent = false);
+    virtual IHttpMessage *sendRequestEx(const char* method, const char* contenttype, StringBuffer& content, HttpClientErrCode &code, StringBuffer &errmsg, IProperties *headers = nullptr, bool alwaysReadContent = false, bool forceNewConnection = false) override ;
     virtual int proxyRequest(IHttpMessage *request, IHttpMessage *response, bool resetForwardedFor) override;
 
     virtual int postRequest(ISoapMessage &req, ISoapMessage& resp);

+ 4 - 2
esp/bindings/http/platform/httptransport.ipp

@@ -162,7 +162,7 @@ public:
     virtual const char *queryParamStr(){return m_paramstr.get();}
     virtual void setHeader(const char* headername, const char* headerval);
     virtual void addHeader(const char* headername, const char* headerval);
-    virtual StringBuffer& getHeader(const char* headername, StringBuffer& headerval);
+    virtual StringBuffer &getHeader(const char* headername, StringBuffer& headerval);
     virtual bool hasHeader(const char* headername);
     virtual void removeHeader(const char* headername);
     virtual int getParameterCount(){return m_paramCount;}
@@ -279,6 +279,8 @@ public:
     virtual bool decompressContent(StringBuffer* originalContent, int compressType) { return false; }
     virtual void setSocketReturner(ISocketReturner* returner) { m_socketReturner = returner; }
     virtual ISocketReturner* querySocketReturner() { return m_socketReturner; }
+    virtual StringBuffer& getStatus(StringBuffer& status) { return status; }
+
 };
 
 
@@ -393,7 +395,7 @@ public:
     CHttpResponse(ISocket& socket);
     virtual ~CHttpResponse();
     virtual void setStatus(const char* status);
-    virtual StringBuffer& getStatus(StringBuffer& status);
+    virtual StringBuffer& getStatus(StringBuffer& status) override;
     virtual void sendBasicChallenge(const char* realm, bool includeContent);
     virtual void sendBasicChallenge(const char* realm, const char* content);
 

+ 3 - 1
esp/esdllib/CMakeLists.txt

@@ -18,9 +18,10 @@
 project( esdllib )
 
 include_directories (
-    ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/xpp
     ${HPCC_SOURCE_DIR}/system/xmllib
     ${HPCC_SOURCE_DIR}/esp/bindings
+    ${HPCC_SOURCE_DIR}/esp/bindings/http/client
+    ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/xpp
     ${HPCC_SOURCE_DIR}/esp/platform
     ${HPCC_SOURCE_DIR}/system/include
     ${HPCC_SOURCE_DIR}/esp/esdllib
@@ -67,6 +68,7 @@ target_link_libraries ( esdllib
     jlib
     xmllib
     thorhelper
+    esphttp
 )
 
 IF (USE_LIBXSLT)

Разлика између датотеке није приказан због своје велике величине
+ 1150 - 393
esp/esdllib/esdl_script.cpp


+ 38 - 3
esp/esdllib/esdl_script.hpp

@@ -47,14 +47,49 @@
 interface IEsdlCustomTransform : extends IInterface
 {
     virtual void processTransform(IEsdlScriptContext * context, const char *srcSection, const char *tgtSection) = 0;
-    virtual void appendEsdlURIPrefixes(StringArray &prefixes) = 0;
+    virtual void processTransformImpl(IEsdlScriptContext * scriptContext, const char *srcSection, const char *tgtSection, IXpathContext *xpathContext, const char *target) = 0;
+    virtual void appendPrefixes(StringArray &prefixes) = 0;
     virtual void toDBGLog() = 0;
 };
 
-esdl_decl void processServiceAndMethodTransforms(IEsdlScriptContext * scriptCtx, std::initializer_list<IEsdlCustomTransform *> const &transforms, const char *srcSection, const char *tgtSection);
+interface IEsdlTransformSet : extends IInterface
+{
+    virtual void processTransformImpl(IEsdlScriptContext * scriptContext, const char *srcSection, const char *tgtSection, IXpathContext *xpathContext, const char *target) = 0;
+    virtual void appendPrefixes(StringArray &prefixes) = 0;
+    virtual aindex_t length() = 0;
+};
+
+inline bool isEmptyTransformSet(IEsdlTransformSet *set)
+{
+    if (!set)
+        return true;
+    return (set->length()==0);
+}
+
+#define ESDLScriptEntryPoint_Legacy "CustomRequestTransform"
+#define ESDLScriptEntryPoint_BackendRequest "BackendRequest"
+#define ESDLScriptEntryPoint_BackendResponse "BackendResponse"
+#define ESDLScriptEntryPoint_PreLogging "PreLogging"
+
+interface IEsdlTransformEntryPointMap : extends IInterface
+{
+    virtual IEsdlTransformSet *queryEntryPoint(const char *name) = 0;
+    virtual void removeEntryPoint(const char *name) = 0;
+};
+
+interface IEsdlTransformMethodMap : extends IInterface
+{
+    virtual IEsdlTransformEntryPointMap *queryMethod(const char *method) = 0;
+    virtual IEsdlTransformSet *queryMethodEntryPoint(const char *method, const char *name) = 0;
+    virtual void removeMethod(const char *method) = 0;
+    virtual void addMethodTransforms(const char *method, const char *script, bool &foundNonLegacyTransforms) = 0;
+};
+
+esdl_decl IEsdlTransformMethodMap *createEsdlTransformMethodMap();
 
-esdl_decl IEsdlCustomTransform *createEsdlCustomTransform(IPropertyTree &customRequestTransform, const char *ns_prefix);
+esdl_decl IEsdlCustomTransform *createEsdlCustomTransform(const char *scriptXml, const char *ns_prefix);
 
+esdl_decl void processServiceAndMethodTransforms(IEsdlScriptContext * scriptCtx, std::initializer_list<IEsdlTransformSet *> const &transforms, const char *srcSection, const char *tgtSection);
 esdl_decl void registerEsdlXPathExtensions(IXpathContext *xpathCtx, IEsdlScriptContext *scriptCtx, const StringArray &prefixes);
 
 #endif /* ESDL_SCRIPT_HPP_ */

+ 64 - 0
esp/esdllib/esdl_xpath_extensions_libxml.cpp

@@ -345,6 +345,69 @@ static void storedValueExistsFunction (xmlXPathParserContextPtr ctxt, int nargs)
     xmlXPathReturnBoolean(ctxt, (!value) ? 0 : 1);
 }
 
+//esdl tokenize function will create temporaries in the root/temp section/node of the document
+//so this is not a general purpose function in that sense
+//this section should be cleared after every script runs
+//we may allow overriding the storage location in the future
+//
+static void strTokenizeFunction(xmlXPathParserContextPtr ctxt, int nargs)
+{
+    IEsdlScriptContext *scriptContext = getEsdlScriptContext(ctxt);
+    if (!scriptContext)
+    {
+        xmlXPathSetError((ctxt), XPATH_INVALID_CTXT);
+        return;
+    }
+
+    if ((nargs < 1) || (nargs > 2))
+    {
+        xmlXPathSetArityError(ctxt);
+        return;
+    }
+
+    xmlChar *delimiters;
+    if (nargs == 2)
+    {
+        delimiters = xmlXPathPopString(ctxt);
+        if (xmlXPathCheckError(ctxt))
+            return;
+    }
+    else
+    {
+        delimiters = xmlStrdup((const xmlChar *) "\t\r\n ");
+    }
+
+    if (delimiters == NULL)
+        return;
+
+    xmlChar *str = xmlXPathPopString(ctxt);
+    if (xmlXPathCheckError(ctxt) || (str == NULL))
+    {
+        if (str)
+            xmlFree(str);
+        xmlFree(delimiters);
+        return;
+    }
+
+    StringBuffer resultPath;
+    if (!scriptContext->tokenize((const char *)str, (const char *)delimiters, resultPath))
+    {
+        xmlFree(str);
+        xmlFree(delimiters);
+        xmlXPathSetError((ctxt), XPATH_EXPR_ERROR);
+        return;
+    }
+
+    xmlXPathObjectPtr ret = xmlXPathEval((const xmlChar *) resultPath.str(), ctxt->context);
+    if (ret != NULL)
+        valuePush(ctxt, ret);
+    else
+        valuePush(ctxt, xmlXPathNewNodeSet(NULL));
+
+    xmlFree(str);
+    xmlFree(delimiters);
+}
+
 void registerEsdlXPathExtensionsForURI(IXpathContext *xpathContext, const char *uri)
 {
     xpathContext->registerFunction(uri, "validateFeaturesAccess", (void *)validateFeaturesAccessFunction);
@@ -355,6 +418,7 @@ void registerEsdlXPathExtensionsForURI(IXpathContext *xpathContext, const char *
     xpathContext->registerFunction(uri, "getLogProfile", (void *)getLogProfileFunction);
     xpathContext->registerFunction(uri, "getLogOption", (void *)getLogOptionFunction);
     xpathContext->registerFunction(uri, "logOptionExists", (void *)logOptionExistsFunction);
+    xpathContext->registerFunction(uri, "tokenize", (void *)strTokenizeFunction);
 }
 
 void registerEsdlXPathExtensions(IXpathContext *xpathContext, IEsdlScriptContext *context, const StringArray &prefixes)

+ 3 - 0
esp/scm/esp.ecm

@@ -45,6 +45,9 @@ SCMinterface IHttpMessage (IInterface)
     int receive(bool alwaysReadContent, IMultiException *me);
     int send();
     StringBuffer& getContent(StringBuffer& buf);
+    StringBuffer& getContentType(StringBuffer& contenttype);
+    StringBuffer& getHeader(const char* headername, StringBuffer& headerval);
+    StringBuffer& getStatus(StringBuffer& status);
 };
 
 typedef enum ESPSerializationFormat_

+ 259 - 167
esp/services/esdl_svc_engine/esdl_binding.cpp

@@ -385,74 +385,148 @@ void EsdlServiceImpl::configureUrlMethod(const char *method, IPropertyTree &entr
     }
 }
 
-void EsdlServiceImpl::addServiceLevelRequestTransform(IPropertyTree *customRequestTransform)
+void EsdlServiceImpl::handleTransformError(StringAttr &serviceError, MapStringTo<StringAttr, const char *> &methodErrors, IException *e, const char *service, const char *method)
 {
-    if (!customRequestTransform)
-        return;
+    VStringBuffer msg("Encountered error while fetching transforms for service '%s'", service);
+    if (!isEmptyString(method))
+        msg.appendf(" method '%s'", method);
+    msg.append(": ");
+    if (e)
+        e->errorMessage(msg);
+    IERRLOG("%s", msg.str());
 
-    try
+    if (!isEmptyString(method))
+        methodErrors.setValue(method, msg.str());
+    else
+        serviceError.set(msg.str());
+}
+
+enum class scriptXmlChildMode { raised, lowered, kept };
+
+StringBuffer &toScriptXML(IPropertyTree *tree, const StringArray &raise, const StringArray &lower, StringBuffer &xml, int indent);
+
+StringBuffer &toScriptXMLNamedChildren(IPropertyTree *tree, const char *name, const StringArray &raise, const StringArray &lower, bool excludeNotKept, StringBuffer &xml, int indent)
+{
+    Owned<IPropertyTreeIterator> children = tree->getElements(name);
+    ForEach(*children)
     {
-        m_serviceLevelRequestTransform.setown(createEsdlCustomTransform(*customRequestTransform, nullptr));
+        IPropertyTree &child = children->query();
+        const char *name = child.queryName();
+        if (!excludeNotKept || (!raise.contains(name) && !lower.contains(name)))
+            toScriptXML(&child, raise, lower, xml, indent + 2);
     }
-    catch(IException* e)
+    return xml;
+}
+
+StringBuffer &toScriptXMLChildren(IPropertyTree *tree, scriptXmlChildMode mode, const StringArray &raise, const StringArray &lower, StringBuffer &xml, int indent)
+{
+    switch (mode)
     {
-        m_serviceLevelCrtFail = true;
-        StringBuffer msg;
-        e->errorMessage(msg);
-        IERRLOG("Service Level Custom Request Transform could not be processed!!: \n\t%s", msg.str());
-        e->Release();
+    case scriptXmlChildMode::kept:
+        return toScriptXMLNamedChildren(tree, "*", raise, lower, true, xml, indent);
+    case scriptXmlChildMode::raised:
+    {
+        ForEachItemIn(i, raise)
+            toScriptXMLNamedChildren(tree, raise.item(i), raise, lower, false, xml, indent);
+        return xml;
     }
-    catch (...)
+    case scriptXmlChildMode::lowered:
     {
-        m_serviceLevelCrtFail = true;
-        IERRLOG("Service Level Custom Request Transform could not be processed!!");
+        ForEachItemIn(i, lower)
+            toScriptXMLNamedChildren(tree, lower.item(i), raise, lower, false, xml, indent);
+        return xml;
+    }
+    default:
+        return xml;
     }
 }
 
-void EsdlServiceImpl::addMethodLevelRequestTransform(const char *method, IPropertyTree &methodCfg, IPropertyTree *customRequestTransform)
+StringBuffer &toScriptXML(IPropertyTree *tree, const StringArray &raise, const StringArray &lower, StringBuffer &xml, int indent)
 {
-    if (!method || !*method || !customRequestTransform)
-        return;
+    const char *name = tree->queryName();
+    if (!name)
+        name = "__unnamed__";
+
+    appendXMLOpenTag(xml.pad(indent), name, nullptr, false);
+
+    Owned<IAttributeIterator> it = tree->getAttributes(true);
+    ForEach(*it)
+        appendXMLAttr(xml, it->queryName()+1, it->queryValue(), nullptr, true);
+
+    if (!tree->hasChildren())
+        return xml.append("/>\n");
+
+    xml.append(">\n");
+
+    toScriptXMLChildren(tree, scriptXmlChildMode::raised, raise, lower, xml, indent);
+    toScriptXMLChildren(tree, scriptXmlChildMode::kept, raise, lower, xml, indent);
+    toScriptXMLChildren(tree, scriptXmlChildMode::lowered, raise, lower, xml, indent);
+
+    return appendXMLCloseTag(xml.pad(indent), name).append("\n");
+}
+
+//Need to fix up serialized PTREE order for backward compatability of scripts
+
+StringBuffer &toScriptXML(IPropertyTree *tree, StringBuffer &xml, int indent)
+{
+    StringArray raise;
+    raise.append("xsdl:param");
+    raise.append("xsdl:variable");
+
+    StringArray lower;
+    lower.append("xsdl:otherwise");
+
+    return toScriptXML(tree, raise, lower, xml, indent);
 
+}
+
+StringBuffer &buildScriptXml(IPropertyTree *cfgParent, StringBuffer &xml)
+{
+    if (!cfgParent)
+        return xml;
+    appendXMLOpenTag(xml, "Scripts", nullptr, false);
+    appendXMLAttr(xml, "xmlns:xsdl", "urn:hpcc:esdl:script", nullptr, true);
+    appendXMLAttr(xml, "xmlns:es", "urn:hpcc:esdl:script", nullptr, true);
+    xml.append('>');
+    IPropertyTree *crt = cfgParent->queryPropTree("xsdl:CustomRequestTransform");
+    if (crt)
+        toScriptXML(crt, xml, 2);
+    IPropertyTree *transforms = cfgParent->queryPropTree("Transforms");
+    if (transforms)
+        toScriptXML(transforms, xml, 2);
+    appendXMLCloseTag(xml, "Scripts");
+    xml.replaceString("&apos;", "'");
+    return xml;
+}
+
+void EsdlServiceImpl::addTransforms(IPropertyTree *cfgParent, const char *service, const char *method, bool removeCfgEntries)
+{
     try
     {
-        Owned<IEsdlCustomTransform> crt = createEsdlCustomTransform(*customRequestTransform, nullptr);
-        m_customRequestTransformMap.setValue(method, crt.get());
+        StringBuffer xml;
+        const char *scriptXml = nullptr;
+        if (cfgParent->hasProp("Scripts"))
+            scriptXml = cfgParent->queryProp("Scripts");
+        else
+            scriptXml = buildScriptXml(cfgParent, xml).str();
+        if (!isEmptyString(scriptXml))
+            m_transforms->addMethodTransforms(method ? method : "", scriptXml, nonLegacyTransforms);
+        if (removeCfgEntries)
+        {
+            cfgParent->removeProp("Scripts");
+            cfgParent->removeProp("Transforms");
+            cfgParent->removeProp("xsdl:CustomRequestTransform");
+        }
     }
-    catch(IException* e)
+    catch (IException *e)
     {
-        StringBuffer msg;
-        e->errorMessage(msg);
-        VStringBuffer errmsg("Custom Request Transform for method %s could not be processed!!: %s", method, msg.str());
-
-        m_methodCRTransformErrors.setValue(method, errmsg.str());
-        IERRLOG("%s", errmsg.str());
+        handleTransformError(m_serviceScriptError, m_methodScriptErrors, e, service, method);
         e->Release();
     }
     catch (...)
     {
-        VStringBuffer errmsg("Custom Request Transform for method %s could not be processed!!", method);
-        m_methodCRTransformErrors.setValue(method, errmsg.str());
-        IERRLOG("%s", errmsg.str());
-    }
-}
-
-static void ensureMergeOrderedEsdlTransform(Owned<IPropertyTree> &dest, IPropertyTree *src)
-{
-    if (!src)
-        return;
-    if (!dest)
-        dest.setown(createPTree(src->queryName(), ipt_ordered));
-    //copy so we can make changes, like adding calculated targets
-    Owned<IPropertyTree> copy = createPTreeFromIPT(src, ipt_ordered);
-    const char *target = copy->queryProp("@target");
-    if (target && *target)
-    {
-        Owned<IPropertyTreeIterator> children = copy->getElements("*");
-        ForEach(*children)
-            children->query().setProp("@_crtTarget", target); //internal use only attribute
+        handleTransformError(m_serviceScriptError, m_methodScriptErrors, nullptr, service, method);
     }
-    mergePTree(dest, copy);
 }
 
 void EsdlServiceImpl::configureTargets(IPropertyTree *cfg, const char *service)
@@ -469,26 +543,7 @@ void EsdlServiceImpl::configureTargets(IPropertyTree *cfg, const char *service)
 
     if (target_cfg)
     {
-        Owned<IPropertyTree> serviceCrt;
-        try
-        {
-            ensureMergeOrderedEsdlTransform(serviceCrt, target_cfg->queryPropTree("xsdl:CustomRequestTransform"));
-            Owned<IPropertyTreeIterator> transforms =  target_cfg->getElements("Transforms/xsdl:CustomRequestTransform");
-            ForEach(*transforms)
-                ensureMergeOrderedEsdlTransform(serviceCrt, &transforms->query());
-        }
-        catch (IPTreeException *e)
-        {
-            StringBuffer msg;
-            e->errorMessage(msg);
-            VStringBuffer errmsg("Encountered error while fetching \"xsdl:CustomRequestTransform\" from service \"%s\" ESDL configuration: %s ", service, msg.str());
-
-            m_serviceLevelCrtFail = true;
-            OERRLOG("%s", errmsg.str());
-            e->Release();
-        }
-
-        addServiceLevelRequestTransform(serviceCrt);
+        addTransforms(target_cfg, service, nullptr, true);
 
         m_pServiceMethodTargets.setown(createPTree(ipt_caseInsensitive));
         Owned<IPropertyTreeIterator> itns = target_cfg->getElements("Method");
@@ -496,7 +551,6 @@ void EsdlServiceImpl::configureTargets(IPropertyTree *cfg, const char *service)
         ForEach(*itns)
             m_pServiceMethodTargets->addPropTree("Target", createPTreeFromIPT(&itns->query()));
 
-
         StringBuffer classPath;
         const IProperties &envConf = queryEnvironmentConf();
         if (envConf.hasProp("classpath"))
@@ -538,29 +592,10 @@ void EsdlServiceImpl::configureTargets(IPropertyTree *cfg, const char *service)
             if (generator.generateMap())
                 m_methodAccessMaps.setValue(mthDef->queryMethodName(), authMap.getLink());
 
-            m_methodCRTransformErrors.remove(method);
-            m_customRequestTransformMap.remove(method);
+            m_methodScriptErrors.remove(method);
+            m_transforms->removeMethod(method);
 
-            Owned<IPropertyTree> methodCrt;
-            try
-            {
-                ensureMergeOrderedEsdlTransform(methodCrt, methodCfg.queryPropTree("xsdl:CustomRequestTransform"));
-                Owned<IPropertyTreeIterator> transforms =  methodCfg.getElements("Transforms/xsdl:CustomRequestTransform");
-                ForEach(*transforms)
-                    ensureMergeOrderedEsdlTransform(methodCrt, &transforms->query());
-            }
-            catch (IException *e)
-            {
-                StringBuffer msg;
-                e->errorMessage(msg);
-                VStringBuffer errmsg("Encountered error while fetching 'xsdl:CustomRequestTransform' from service '%s', method '%s' ESDL configuration: %s ", service, method, msg.str());
-
-                m_methodCRTransformErrors.setValue(method, errmsg.str());
-                OERRLOG("%s", errmsg.str());
-                e->Release();
-            }
-
-            addMethodLevelRequestTransform(method, methodCfg, methodCrt);
+            addTransforms(&methodCfg, service, method, true);
 
             const char *type = methodCfg.queryProp("@querytype");
             if (type && strieq(type, "java"))
@@ -632,8 +667,9 @@ static inline bool isPublishedQuery(EsdlMethodImplType implType)
 
 IEsdlScriptContext* EsdlServiceImpl::checkCreateEsdlServiceScriptContext(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, IPropertyTree *tgtcfg)
 {
-    IEsdlCustomTransform *methodCrt = m_customRequestTransformMap.getValue(mthdef.queryMethodName());
-    if (!m_serviceLevelRequestTransform && !methodCrt)
+    IEsdlTransformEntryPointMap *serviceEPm = m_transforms->queryMethod("");
+    IEsdlTransformEntryPointMap *methodEPm = m_transforms->queryMethod(mthdef.queryMethodName());
+    if (!serviceEPm && !methodEPm)
         return nullptr;
 
     Owned<IEsdlScriptContext> scriptContext = createEsdlScriptContext(&context);
@@ -651,6 +687,7 @@ IEsdlScriptContext* EsdlServiceImpl::checkCreateEsdlServiceScriptContext(IEspCon
 }
 
 void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
+                                           Owned<IEsdlScriptContext> &scriptContext,
                                            IEsdlDefService &srvdef,
                                            IEsdlDefMethod &mthdef,
                                            Owned<IPropertyTree> &tgtcfg,
@@ -660,16 +697,20 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
                                            IPropertyTree *req,
                                            StringBuffer &out,
                                            StringBuffer &logdata,
+                                           StringBuffer &origResp,
+                                           StringBuffer &soapmsg,
                                            unsigned int flags)
 {
-    Owned<IEsdlScriptContext> scriptContext;
-
     const char *mthName = mthdef.queryName();
     context.addTraceSummaryValue(LogMin, "method", mthName);
     const char* srvName = srvdef.queryName();
 
-    if (m_serviceLevelCrtFail) //checked further along in shared code, but might as well avoid extra overhead
-        throw MakeStringException(-1, "%s::%s disabled due to Custom Transform errors. Review transform template in configuration.", srvdef.queryName(), mthName);
+    if (m_serviceScriptError.length()) //checked further along in shared code, but might as well avoid extra overhead
+    {
+        VStringBuffer msg("%s::%s disabled due to ESDL Script error(s). [%s]. Review transform template in configuration.", srvdef.queryName(), mthName, m_serviceScriptError.str());
+        throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_CLIENT, "ESDL", "%s", msg.str());
+
+    }
 
     Owned<MethodAccessMap>* authMap = m_methodAccessMaps.getValue(mthdef.queryMethodName());
     if (authMap != nullptr && authMap->get() != nullptr)
@@ -688,7 +729,6 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
             }
             const char * user = context.queryUserId();
             throw MakeStringException(401, "Insufficient priviledge to run function (%s) access denied for user (%s) - %s", mthName, (user && *user) ? user : "Anonymous", features.str());
-
         }
     }
 
@@ -728,7 +768,6 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
     else
         ESPLOG(LogMin,"DESDL: Transaction ID could not be generated!");
 
-    StringBuffer origResp, soapmsg;
     EsdlMethodImplType implType = EsdlMethodImplUnknown;
 
     if(stricmp(mthName, "echotest")==0 || mthdef.hasProp("EchoTest"))
@@ -753,9 +792,12 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
         if (!tgtcfg)
             throw makeWsException( ERR_ESDL_BINDING_BADREQUEST, WSERR_CLIENT, "ESDL", "Target not configured for method: %s", mthName );
 
-        StringAttr *crtErrorMessage = m_methodCRTransformErrors.getValue(mthName);
+        StringAttr *crtErrorMessage = m_methodScriptErrors.getValue(mthName);
         if (crtErrorMessage && !crtErrorMessage->isEmpty())
-            throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_CLIENT, "ESDL", "%s", crtErrorMessage->str());
+        {
+            VStringBuffer msg("%s::%s disabled due to ESDL Script error(s). [%s]. Review transform template in configuration.", srvName, mthName, crtErrorMessage->str());
+            throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_CLIENT, "ESDL", "%s", msg.str());
+        }
 
         implType = getEsdlMethodImplType(tgtcfg->queryProp("@querytype"));
 
@@ -883,6 +925,8 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
             handleFinalRequest(context, scriptContext, tgtcfg, tgtctx, srvdef, mthdef, ns, reqcontent, origResp, isPublishedQuery(implType), implType==EsdlMethodImplProxy, soapmsg);
             context.addTraceSummaryTimeStamp(LogNormal, "end-HFReq");
 
+
+
             if (isPublishedQuery(implType))
             {
                 context.addTraceSummaryTimeStamp(LogNormal, "srt-procres");
@@ -899,15 +943,29 @@ void EsdlServiceImpl::handleServiceRequest(IEspContext &context,
         }
     }
 
-    context.addTraceSummaryTimeStamp(LogNormal, "srt-resLogging");
-    handleResultLogging(context, scriptContext, tgtctx.get(), req, soapmsg.str(), origResp.str(), out.str(), logdata.str());
-    context.addTraceSummaryTimeStamp(LogNormal, "end-resLogging");
     ESPLOG(LogMax,"Customer Response: %s", out.str());
 }
 
-bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptContext *scriptContext, IPropertyTree * reqcontext, IPropertyTree * request,const char *rawreq, const char * rawresp, const char * finalresp, const char * logdata)
+bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptContext *scriptContext, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, IPropertyTree * reqcontext, IPropertyTree * request,const char *rawreq, const char * rawresp, const char * finalresp, const char * logdata)
 {
+    StringBuffer temp;
+    if (scriptContext)
+    {
+        IEsdlTransformSet *servicePLTs = m_transforms->queryMethodEntryPoint("", ESDLScriptEntryPoint_PreLogging);
+        IEsdlTransformSet *methodPLTs = m_transforms->queryMethodEntryPoint(mthdef.queryName(), ESDLScriptEntryPoint_PreLogging);
+
+        scriptContext->appendContent(ESDLScriptCtxSection_LogData, "LogDatasets", logdata);
+
+        if (servicePLTs || methodPLTs)
+        {
+            processServiceAndMethodTransforms(scriptContext, {servicePLTs, methodPLTs}, ESDLScriptCtxSection_LogData, nullptr);
+            scriptContext->toXML(temp, ESDLScriptCtxSection_LogData);
+            logdata = temp.str();
+        }
+    }
+
     bool success = true;
+    espcontext.addTraceSummaryTimeStamp(LogNormal, "srt-resLogging");
     if (loggingManager())
     {
         Owned<IEspLogEntry> entry = loggingManager()->createLogEntry();
@@ -925,7 +983,7 @@ bool EsdlServiceImpl::handleResultLogging(IEspContext &espcontext, IEsdlScriptCo
         success = loggingManager()->updateLog(entry, logresp);
         ESPLOG(LogMin,"ESDLService: Attempted to log ESP transaction: %s", logresp.str());
     }
-
+    espcontext.addTraceSummaryTimeStamp(LogNormal, "end-resLogging");
     return success;
 }
 
@@ -1077,10 +1135,11 @@ void EsdlServiceImpl::handleFinalRequest(IEspContext &context,
     {
         try
         {
-            auto queryname = (isroxie ? tgtcfg->queryProp("@queryname") : nullptr);
-            auto requestname = mthdef.queryRequestType();
-            const char* qname = nullptr;
-            const char* content = nullptr;
+            const char *queryname = (isroxie ? tgtcfg->queryProp("@queryname") : nullptr);
+            const char *requestname = mthdef.queryRequestType();
+            const char *qname = nullptr;
+            const char *content = nullptr;
+
             StringBuffer encodedContent;
             StringBuffer echoDataset;
 
@@ -1161,7 +1220,22 @@ void EsdlServiceImpl::handleFinalRequest(IEspContext &context,
         }
     }
 
-    processResponse(context,srvdef,mthdef,ns,out);
+    if (scriptContext)
+    {
+        IEsdlTransformSet *serviceIRTs = m_transforms->queryMethodEntryPoint("", ESDLScriptEntryPoint_BackendResponse);
+        IEsdlTransformSet *methodIRTs = m_transforms->queryMethodEntryPoint(mthdef.queryName(), ESDLScriptEntryPoint_BackendResponse);
+
+        context.addTraceSummaryTimeStamp(LogNormal, "srt-resptrans");
+
+        scriptContext->setContent(ESDLScriptCtxSection_InitialResponse, out.str());
+        if (serviceIRTs || methodIRTs)
+        {
+            processServiceAndMethodTransforms(scriptContext, {serviceIRTs, methodIRTs}, ESDLScriptCtxSection_InitialResponse, ESDLScriptCtxSection_PreESDLResponse);
+            scriptContext->toXML(out.clear(), ESDLScriptCtxSection_PreESDLResponse);
+        }
+
+        context.addTraceSummaryTimeStamp(LogNormal, "end-resptrans");
+    }
 }
 
 void EsdlServiceImpl::handleEchoTest(const char *mthName,
@@ -1431,22 +1505,25 @@ void EsdlServiceImpl::prepareFinalRequest(IEspContext &context,
 
     // Process Custom Request Transforms
 
-    if (m_serviceLevelCrtFail)
-        throw MakeStringException(-1, "%s::%s disabled due to service-level Custom Transform errors. Review transform template in configuration.", srvdef.queryName(), mthName);
+    if (m_serviceScriptError.length())
+        throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_CLIENT, "ESDL", "%s::%s disabled due to service-level Custom Transform errors. [%s]. Review transform template in configuration.", srvdef.queryName(), mthName, m_serviceScriptError.str());
 
-    StringAttr *crtErrorMessage = m_methodCRTransformErrors.getValue(mthName);
+    StringAttr *crtErrorMessage = m_methodScriptErrors.getValue(mthName);
     if (crtErrorMessage && !crtErrorMessage->isEmpty())
-        throw MakeStringException(-1, "%s::%s disabled due to method-level Custom Transform errors: %s. Review transform template in configuration.", srvdef.queryName(), mthName, crtErrorMessage->str());
+        throw makeWsException( ERR_ESDL_BINDING_INTERNERR, WSERR_CLIENT, "ESDL", "%s::%s disabled due to ESDL Script error(s). [%s]. Review transform template in configuration.", srvdef.queryName(), mthName, crtErrorMessage->str());
 
     if (scriptContext)
     {
-        IEsdlCustomTransform *methodCrt = m_customRequestTransformMap.getValue(mthName);
+        IEsdlTransformSet *serviceBRTs = m_transforms->queryMethodEntryPoint("", ESDLScriptEntryPoint_BackendRequest);
+        IEsdlTransformSet *methodBRTs = m_transforms->queryMethodEntryPoint(mthName, ESDLScriptEntryPoint_BackendRequest);
 
         context.addTraceSummaryTimeStamp(LogNormal, "srt-custreqtrans");
         scriptContext->setContent(ESDLScriptCtxSection_ESDLRequest, reqProcessed.str());
-        if (m_serviceLevelRequestTransform || methodCrt) //slightly redundant, but less fragile to future changes to check again here
-            processServiceAndMethodTransforms(scriptContext, {m_serviceLevelRequestTransform, methodCrt}, ESDLScriptCtxSection_ESDLRequest, ESDLScriptCtxSection_FinalRequest);
-        scriptContext->toXML(reqProcessed.clear(), ESDLScriptCtxSection_FinalRequest);
+        if (serviceBRTs || methodBRTs)
+        {
+            processServiceAndMethodTransforms(scriptContext, {serviceBRTs, methodBRTs}, ESDLScriptCtxSection_ESDLRequest, ESDLScriptCtxSection_FinalRequest);
+            scriptContext->toXML(reqProcessed.clear(), ESDLScriptCtxSection_FinalRequest);
+        }
 
         context.addTraceSummaryTimeStamp(LogNormal, "end-custreqtrans");
     }
@@ -1992,6 +2069,12 @@ void EsdlBindingImpl::getSoapMessage(StringBuffer& soapmsg,
     }
 }
 
+static void returnSocket(CHttpResponse *response)
+{
+    if (response && response->querySocketReturner())
+        response->querySocketReturner()->returnSocket();
+}
+
 int EsdlBindingImpl::onGetInstantQuery(IEspContext &context,
                                        CHttpRequest* request,
                                        CHttpResponse* response,
@@ -2040,7 +2123,11 @@ int EsdlBindingImpl::onGetInstantQuery(IEspContext &context,
 
                     getSchemaLocation(context, request, schemaLocation);
                     context.setESDLBindingID(m_bindingId.get());
-                    m_pESDLService->handleServiceRequest(context, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), req_pt.get(), out, logdata, 0);
+
+                    StringBuffer origResp;
+                    StringBuffer soapmsg;
+                    Owned<IEsdlScriptContext> scriptContext;
+                    m_pESDLService->handleServiceRequest(context, scriptContext, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), req_pt.get(), out, logdata, origResp, soapmsg, 0);
 
                     response->setContent(out.str());
 
@@ -2050,13 +2137,14 @@ int EsdlBindingImpl::onGetInstantQuery(IEspContext &context,
                       response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);
                     response->setStatus(HTTP_STATUS_OK);
                     response->send();
+                    returnSocket(response);
 
                     unsigned timetaken = msTick() - context.queryCreationTime();
                     context.addTraceSummaryTimeStamp(LogMin, "respSent");
 
                     ESPLOG(LogMax,"EsdlBindingImpl:onGetInstantQuery response: %s", out.str());
 
-                    m_pESDLService->esdl_log(context, *srvdef, *mthdef, tgtcfg.get(), tgtctx.get(), req_pt.get(), out.str(), logdata.str(), timetaken);
+                    m_pESDLService->handleResultLogging(context, scriptContext, *srvdef, *mthdef, tgtctx.get(), req_pt.get(), soapmsg.str(), origResp.str(), out.str(), logdata.str());
                     context.addTraceSummaryTimeStamp(LogMin, "respLogged");
 
                     return 0;
@@ -2347,7 +2435,11 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
             getSchemaLocation(*ctx, request, schemaLocation);
 
             ctx->setESDLBindingID(m_bindingId.get());
-            m_pESDLService->handleServiceRequest(*ctx, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), pt, baseout, logdata, 0);
+
+            StringBuffer origResp;
+            StringBuffer soapmsg;
+            Owned<IEsdlScriptContext> scriptContext;
+            m_pESDLService->handleServiceRequest(*ctx, scriptContext, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), pt, baseout, logdata, origResp, soapmsg, 0);
 
             StringBuffer out;
             out.append(
@@ -2364,11 +2456,12 @@ int EsdlBindingImpl::HandleSoapRequest(CHttpRequest* request,
             response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);
             response->setStatus(HTTP_STATUS_OK);
             response->send();
+            returnSocket(response);
 
             unsigned timetaken = msTick() - ctx->queryCreationTime();
             ctx->addTraceSummaryTimeStamp(LogMin, "respSent");
 
-             m_pESDLService->esdl_log(*ctx, *srvdef, *mthdef, tgtcfg.get(), tgtctx.get(), pt, baseout.str(), logdata.str(), timetaken);
+            m_pESDLService->handleResultLogging(*ctx, scriptContext, *srvdef, *mthdef, tgtctx.get(), pt, soapmsg.str(), origResp.str(), out.str(), logdata.str());
 
             ESPLOG(LogMax,"EsdlBindingImpl:HandleSoapRequest response: %s", xmlout.str());
         }
@@ -3044,57 +3137,56 @@ void EsdlBindingImpl::handleJSONPost(CHttpRequest *request, CHttpResponse *respo
             DBGLOG("EsdlBinding::%s::%s: JSON request: %s", serviceName, methodName, content.str());
 
         Owned<IPropertyTree> contentTree = createPTreeFromJSONString(content.str());
-        if (contentTree)
-        {
-            StringBuffer requestName;
-            if (stricmp(methodName, "ping") == 0)
-                requestName.append(serviceName);
-            requestName.append(methodName).append("Request");
-
-            Owned<IPropertyTree> reqTree = contentTree->getBranch(requestName.str());
-            if (!reqTree)
-                throw MakeStringException(-1, "EsdlBinding::%s::%s: Could not find \"%s\" section in JSON request", serviceName, methodName, requestName.str());
-
-            if (!m_esdl)
-            {
-                throw MakeStringException(-1, "EsdlBinding::%s: Service definition has not been loaded", serviceName);
-            }
-            else
-            {
-                IEsdlDefService *srvdef = m_esdl->queryService(serviceName);
+        if (!contentTree)
+            throw MakeStringException(-1, "EsdlBinding::%s::%s: Could not process JSON request", serviceName, methodName);
 
-                if (!srvdef)
-                    throw MakeStringException(-1, "EsdlBinding::%s: Service definition not found", serviceName);
-                else
-                {
-                    IEsdlDefMethod *mthdef = srvdef->queryMethodByName(methodName);
-                    if (!mthdef)
-                        throw MakeStringException(-1, "EsdlBinding::%s::%s: Method definition not found", serviceName, methodName);
-                    else
-                    {
-                        jsonresp.append("{");
-                        StringBuffer logdata; //RODRIGO: What are we doing w/ the logdata?
+        StringBuffer requestName;
+        if (stricmp(methodName, "ping") == 0)
+            requestName.append(serviceName);
+        requestName.append(methodName).append("Request");
 
-                        Owned<IPropertyTree> tgtcfg;
-                        Owned<IPropertyTree> tgtctx;
+        Owned<IPropertyTree> reqTree = contentTree->getBranch(requestName.str());
+        if (!reqTree)
+            throw MakeStringException(-1, "EsdlBinding::%s::%s: Could not find \"%s\" section in JSON request", serviceName, methodName, requestName.str());
 
-                        StringBuffer ns, schemaLocation;
-                        generateNamespace(*ctx, request, serviceName, methodName, ns);
-                        getSchemaLocation(*ctx, request, schemaLocation);
+        if (!m_esdl)
+            throw MakeStringException(-1, "EsdlBinding::%s: Service definition has not been loaded", serviceName);
+        IEsdlDefService *srvdef = m_esdl->queryService(serviceName);
 
-                        ctx->setESDLBindingID(m_bindingId.get());
-                        m_pESDLService->handleServiceRequest(*ctx, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), reqTree.get(), jsonresp, logdata, ESDL_BINDING_RESPONSE_JSON);
+        if (!srvdef)
+            throw MakeStringException(-1, "EsdlBinding::%s: Service definition not found", serviceName);
+        IEsdlDefMethod *mthdef = srvdef->queryMethodByName(methodName);
+        if (!mthdef)
+            throw MakeStringException(-1, "EsdlBinding::%s::%s: Method definition not found", serviceName, methodName);
+        jsonresp.append("{");
+        StringBuffer logdata; //RODRIGO: What are we doing w/ the logdata?
+
+        Owned<IPropertyTree> tgtcfg;
+        Owned<IPropertyTree> tgtctx;
+
+        StringBuffer ns, schemaLocation;
+        generateNamespace(*ctx, request, serviceName, methodName, ns);
+        getSchemaLocation(*ctx, request, schemaLocation);
+
+        ctx->setESDLBindingID(m_bindingId.get());
+        StringBuffer origResp;
+        StringBuffer soapmsg;
+        Owned<IEsdlScriptContext> scriptContext;
+        m_pESDLService->handleServiceRequest(*ctx, scriptContext, *srvdef, *mthdef, tgtcfg, tgtctx, ns.str(), schemaLocation.str(), reqTree.get(), jsonresp, logdata, origResp, soapmsg, ESDL_BINDING_RESPONSE_JSON);
+
+        jsonresp.append("}");
+
+        response->setContent(jsonresp.str());
+        response->setContentType("application/json");
+        response->setStatus("200 OK");
+        response->send();
+        returnSocket(response);
 
-                        jsonresp.append("}");
-                    }
-                }
-            }
+        m_pESDLService->handleResultLogging(*ctx, scriptContext, *srvdef, *mthdef, tgtctx.get(), reqTree, soapmsg.str(), origResp.str(), jsonresp.str(), logdata.str());
 
-            if (getEspLogLevel()>LogNormal)
-                DBGLOG("json response: %s", jsonresp.str());
-        }
-        else
-            throw MakeStringException(-1, "EsdlBinding::%s::%s: Could not process JSON request", serviceName, methodName);
+        if (getEspLogLevel()>LogNormal)
+            DBGLOG("json response: %s", jsonresp.str());
+        return;
     }
     catch (IWsException * iwse)
     {

+ 16 - 11
esp/services/esdl_svc_engine/esdl_binding.hpp

@@ -73,6 +73,9 @@ class EsdlServiceImpl : public CInterface, implements IEspService
 private:
     inline Owned<ILoggingManager>& loggingManager() { return m_oDynamicLoggingManager ? m_oDynamicLoggingManager : m_oStaticLoggingManager; }
     IEspContainer *container;
+    Owned<IEsdlTransformMethodMap> m_transforms = createEsdlTransformMethodMap();
+    bool nonLegacyTransforms = false;
+
     MapStringToMyClass<ISmartSocketFactory> connMap;
     MapStringToMyClass<IEmbedServiceContext> javaServiceMap;
     MapStringToMyClass<IException> javaExceptionMap;
@@ -81,9 +84,7 @@ private:
     Owned<ILoggingManager> m_oDynamicLoggingManager;
     Owned<ILoggingManager> m_oStaticLoggingManager;
     bool m_bGenerateLocalTrxId;
-    MapStringToMyClass<IEsdlCustomTransform> m_customRequestTransformMap;
-    Owned<IEsdlCustomTransform> m_serviceLevelRequestTransform;
-    bool m_serviceLevelCrtFail = false;
+    StringAttr m_serviceScriptError;
     using MethodAccessMap = MapStringTo<SecAccessFlags>;
     using MethodAccessMaps = MapStringTo<Owned<MethodAccessMap> >;
     MethodAccessMaps            m_methodAccessMaps;
@@ -105,11 +106,15 @@ public:
     StringBuffer                m_serviceNameSpaceBase;
     StringAttr                  m_namespaceScheme;
     bool                        m_usesURLNameSpace;
-    MapStringTo<StringAttr, const char *> m_methodCRTransformErrors;
+
+    using TransformErrorMap = MapStringTo<StringAttr, const char *>;
+    TransformErrorMap m_methodScriptErrors;
 
 public:
     IMPLEMENT_IINTERFACE;
-    EsdlServiceImpl(){}
+    EsdlServiceImpl()
+    {
+    }
 
     virtual ~EsdlServiceImpl();
 
@@ -158,7 +163,7 @@ public:
             m_pEsdlTransformer.clear();
         if(m_pServiceMethodTargets)
             m_pServiceMethodTargets.clear();
-        m_methodCRTransformErrors.kill();
+        m_methodScriptErrors.kill();
     }
 
     virtual bool loadLoggingManager(Owned<ILoggingManager>& manager, IPTree* configuration);
@@ -168,12 +173,13 @@ public:
     void configureJavaMethod(const char *method, IPropertyTree &entry, const char *classPath);
     void configureCppMethod(const char *method, IPropertyTree &entry, IEspPlugin*& plugin);
     void configureUrlMethod(const char *method, IPropertyTree &entry);
-    void addServiceLevelRequestTransform(IPropertyTree *customRequestTransform);
-    void addMethodLevelRequestTransform(const char *method, IPropertyTree &methodCfg, IPropertyTree *customRequestTransform);
+
+    void handleTransformError(StringAttr &serviceError, TransformErrorMap &methodErrors, IException *e, const char *service, const char *method);
+    void addTransforms(IPropertyTree *cfgParent, const char *service, const char *method, bool removeCfgIEntries);
 
     IEsdlScriptContext* checkCreateEsdlServiceScriptContext(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, IPropertyTree *tgtcfg);
 
-    virtual void handleServiceRequest(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, Owned<IPropertyTree> &tgtcfg, Owned<IPropertyTree> &tgtctx, const char *ns, const char *schema_location, IPropertyTree *req, StringBuffer &out, StringBuffer &logdata, unsigned int flags);
+    virtual void handleServiceRequest(IEspContext &context, Owned<IEsdlScriptContext> &scriptContext, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, Owned<IPropertyTree> &tgtcfg, Owned<IPropertyTree> &tgtctx, const char *ns, const char *schema_location, IPropertyTree *req, StringBuffer &out, StringBuffer &logdata, StringBuffer &origResp, StringBuffer &soapmsg, unsigned int flags);
     virtual void generateTransactionId(IEspContext & context, StringBuffer & trxid)=0;
     void generateTargetURL(IEspContext & context, IPropertyTree *srvinfo, StringBuffer & url, bool isproxy);
     void sendTargetSOAP(IEspContext & context, IPropertyTree *srvinfo, const char * req, StringBuffer &resp, bool isproxy,const char * targeturl);
@@ -183,10 +189,9 @@ public:
     virtual void esdl_log(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, IPropertyTree *tgtcfg, IPropertyTree *tgtctx, IPropertyTree *req_pt, const char *xmlresp, const char *logdata, unsigned int timetaken){}
     virtual void processHeaders(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, const char *ns, StringBuffer &req, StringBuffer &headers){};
     virtual void processRequest(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, const char *ns, StringBuffer &req) {};
-    virtual void processResponse(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, const char *ns, StringBuffer &resp) {};
     void prepareFinalRequest(IEspContext &context, IEsdlScriptContext *scriptContext, Owned<IPropertyTree> &tgtcfg, Owned<IPropertyTree> &tgtctx, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, bool isroxie, const char* ns, StringBuffer &reqcontent, StringBuffer &reqProcessed);
     virtual void createServersList(IEspContext &context, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, StringBuffer &servers) {};
-    virtual bool handleResultLogging(IEspContext &espcontext, IEsdlScriptContext *scriptContext, IPropertyTree * reqcontext, IPropertyTree * request,  const char * rawreq, const char * rawresp, const char * finalresp, const char * logdata);
+    virtual bool handleResultLogging(IEspContext &espcontext, IEsdlScriptContext *scriptContext, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, IPropertyTree * reqcontext, IPropertyTree * request,  const char * rawreq, const char * rawresp, const char * finalresp, const char * logdata);
     void handleEchoTest(const char *mthName, IPropertyTree *req, StringBuffer &soapResp, ESPSerializationFormat format);
     void handlePingRequest(const char *srvName, StringBuffer &out, unsigned int flags);
     virtual void handleFinalRequest(IEspContext &context, IEsdlScriptContext *scriptContext, Owned<IPropertyTree> &tgtcfg, Owned<IPropertyTree> &tgtctx, IEsdlDefService &srvdef, IEsdlDefMethod &mthdef, const char *ns, StringBuffer& req, StringBuffer &out, bool isroxie, bool isproxy, StringBuffer &rawreq);

+ 1 - 1
esp/services/esdl_svc_engine/esdl_svc_engine.cpp

@@ -44,7 +44,7 @@ IPropertyTree *createContextMethodConfig(IPropertyTree *methodConfig)
     const char *include = "*";
     if (methodConfig->hasProp("@contextInclude"))
         include = methodConfig->queryProp("@contextInclude");
-    const char *remove = "Transforms|xsdl:CustomRequestTransform";
+    const char *remove = "Transforms|xsdl:CustomRequestTransform|Scripts";
     if (methodConfig->hasProp("@contextRemove"))
         remove = methodConfig->queryProp("@contextRemove");
     const char *removeAttrs = "@contextInclude|@contextRemove|@contextAttRemove";

+ 37 - 20
initfiles/examples/EsdlExample/esdl_binding.xml

@@ -1,29 +1,46 @@
- <Methods>
-       <xsdl:CustomRequestTransform>
-          <xsdl:choose>
-             <xsdl:when test="$clientversion=1.9">
-                <xsdl:SetValue target="Row/Name/First"  value="'service'" />
-             </xsdl:when>
-             <xsdl:otherwise>
-                <xsdl:SetValue target="Row/Name/Last" value="'zz'"/>
-             </xsdl:otherwise>
-          </xsdl:choose>
-       </xsdl:CustomRequestTransform>
-    <Method name="JavaEchoPersonInfo" querytype="java" javamethod="EsdlExample.EsdlExampleService.JavaEchoPersonInfo"/>
-    <Method name="CppEchoPersonInfo" querytype="cpp" method="onEsdlExampleCppEchoPersonInfo" plugin="libEsdlExampleService.so"/>
-    <Method name="RoxieEchoPersonInfo" querytype="roxie" url="http://localhost:9876/roxie" queryname="RoxieEchoPersonInfo">
-         <xsdl:CustomRequestTransform target="soap:Body/{$query}">
+<Methods>
+   <Scripts><![CDATA[
+      <Transforms xmlns:xsdl='urn:hpcc:esdl:script'>
+         <xsdl:CustomRequestTransform>
             <xsdl:choose>
                <xsdl:when test="$clientversion=1.9">
-                  <xsdl:SetValue target="vertest"  value="'v1.9'"/>
-                  <xsdl:SetValue target="RoxieEchoPersonInfoRequest/Row/Name/First"  value="'v1.9'"/>
+                  <xsdl:SetValue target="Row/Name/First" value="'service'" />
                </xsdl:when>
                <xsdl:otherwise>
-                  <xsdl:SetValue target="vertest"  value="concat('v', $clientversion)"/>
-                  <xsdl:SetValue target="RoxieEchoPersonInfoRequest/Row/Name/Last" value="concat('v', $clientversion)"/>
+                  <xsdl:SetValue target="Row/Name/Last" value="'zz'"/>
                </xsdl:otherwise>
             </xsdl:choose>
+            </xsdl:CustomRequestTransform>
+            <xsdl:CustomRequestTransform>
+               <xsdl:element name="www">
+               <xsdl:SetValue target="yyy" value="'111'"/>
+               </xsdl:element>
+         </xsdl:CustomRequestTransform>
+      </Transforms>
+      ]]>
+   </Scripts>
+   <Method name="RoxieEchoPersonInfo" querytype="roxie" url="http://localhost:9876/roxie" queryname="RoxieEchoPersonInfo">
+      <Scripts><![CDATA[
+         <Transforms xmlns:xsdl='urn:hpcc:esdl:script'>
+         <xsdl:CustomRequestTransform target="soap:Body/{$query}">
+         <xsdl:choose>
+            <xsdl:when test="$clientversion=1.9">
+            <xsdl:SetValue target="vertest" value="'v1.9'"/>
+            <xsdl:SetValue target="RoxieEchoPersonInfoRequest/Row/Name/First" value="'v1.9'"/>
+            </xsdl:when>
+            <xsdl:otherwise>
+            <xsdl:SetValue target="vertest" value="concat('v', $clientversion)"/>
+            <xsdl:SetValue target="RoxieEchoPersonInfoRequest/Row/Name/Last" value="concat('v', $clientversion)"/>
+            </xsdl:otherwise>
+         </xsdl:choose>
+         </xsdl:CustomRequestTransform>
+         <xsdl:CustomRequestTransform>
+         <xsdl:element name="abc">
+            <xsdl:SetValue target="xyz" value="'000'"/>
+         </xsdl:element>
          </xsdl:CustomRequestTransform>
+         </Transforms>
+      ]]>
+      </Scripts>
    </Method>
 </Methods>
-

+ 147 - 0
initfiles/examples/EsdlExample/esdl_binding_entrypoints.xml

@@ -0,0 +1,147 @@
+<Methods>
+   <Scripts><![CDATA[
+      <Transforms xmlns:es='urn:hpcc:esdl:script'>
+         <es:BackendRequest>
+            <es:set-value target="Row/Name/First" value="'modified-request-at-service'" />
+         </es:BackendRequest>
+         <es:BackendRequest>
+            <es:set-value target="BRSRV2" value="'s2'" />
+         </es:BackendRequest>
+         <es:BackendRequest>
+            <es:set-value target="BRSRV3" value="'s3'" />
+         </es:BackendRequest>
+         <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+            <es:target xpath="soap:Body">
+              <es:target xpath="resp:RoxieEchoPersonInfoResponse">
+                <es:target xpath="resp:Results/resp:Result">
+                  <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+                   <es:set-value target="ds1:Row/ds1:Name/ds1:Last" value="'modified-response-at-service'" />
+                  </es:target>
+                </es:target>
+              </es:target>
+            </es:target>
+         </es:BackendResponse>
+         <es:BackendResponse>
+            <es:set-value target="BRESPSRV2" value="'s22'" />
+         </es:BackendResponse>
+         <es:BackendResponse>
+            <es:set-value target="BRESPSRV3" value="'s33'" />
+         </es:BackendResponse>
+         <es:PreLogging>
+            <es:set-value target="PLSRV1" value="'s111'" />
+         </es:PreLogging>
+         <es:PreLogging>
+            <es:set-value target="PLSRV2" value="'s222'" />
+         </es:PreLogging>
+         <es:PreLogging>
+            <es:set-value target="PLSRV3" value="'s333'" />
+         </es:PreLogging>
+      </Transforms>
+      ]]>
+   </Scripts>
+   <Method name="RoxieEchoPersonInfo" querytype="roxie" url="http://localhost:9876/roxie" queryname="RoxieEchoPersonInfo">
+      <Scripts><![CDATA[
+         <Transforms xmlns:es='urn:hpcc:esdl:script'>
+         <es:BackendRequest>
+            <es:append-to-value target="Row/Name/First" value="'-and-method'" />
+         </es:BackendRequest>
+         <es:BackendRequest>
+            <es:set-value target="BRMTH2" value="'m2'" />
+         </es:BackendRequest>
+         <es:BackendRequest>
+            <es:set-value target="BRMTH3" value="'m3'" />
+         </es:BackendRequest>
+         <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+            <es:target xpath="soap:Body">
+              <es:target xpath="resp:RoxieEchoPersonInfoResponse">
+                <es:target xpath="resp:Results/resp:Result">
+                  <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+                    <es:append-to-value target="ds1:Row/ds1:Name/ds1:Last" value="'-and-method'" />
+                  </es:target>
+                </es:target>
+              </es:target>
+            </es:target>
+         </es:BackendResponse>
+         <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+            <es:http-post-xml url="'http://127.0.0.1:9876'" section="logdata/LogDataset" name="roxie_call_success">
+              <es:content>
+                <es:element name="Envelope">
+                  <es:namespace prefix="soap" uri="http://schemas.xmlsoap.org/soap/envelope/" current="true" />
+                  <es:element name="Body">
+                    <es:element name="roxieechopersoninfoRequest">
+                      <es:namespace uri="urn:hpccsystems:ecl:roxieechopersoninfo" current="true" />
+                      <es:element name="roxieechopersoninforequest">
+                        <es:element name="Row">
+                          <es:element name="Name">
+                            <es:set-value target="First" value="'echoFirst'"/>
+                            <es:set-value target="Last" value="'echoLast'"/>
+                            <es:element name="Aliases">
+                              <es:set-value target="Alias" value="'echoA1'"/>
+                              <es:add-value target="Alias" value="'echoA2'"/>
+                            </es:element>
+                          </es:element>
+                        </es:element>
+                      </es:element>
+                    </es:element>
+                  </es:element>
+                </es:element>
+              </es:content>
+            </es:http-post-xml>
+            <es:target xpath="soap:Body">
+              <es:target xpath="resp:RoxieEchoPersonInfoResponse">
+                <es:target xpath="resp:Results/resp:Result">
+                  <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+                    <es:source xpath="$roxie_call_success/response/content">
+                      <es:source xpath="soap:Envelope/soap:Body">
+                        <es:source xpath="resp:roxieechopersoninfoResponse/resp:Results/resp:Result">
+                          <es:source xpath="ds1:Dataset/ds1:Row">
+                            <es:append-to-value target="ds1:Row/ds1:Name/ds1:Last" value="concat('-plus-echoed-alias-', ds1:Name/ds1:Aliases/ds1:Alias[2])" />
+                          </es:source>
+                        </es:source>
+                      </es:source>
+                    </es:source>
+                  </es:target>
+                </es:target>
+              </es:target>
+            </es:target>
+         </es:BackendResponse>
+         <es:BackendResponse>
+            <es:set-value target="BRESPMTH3" value="'m33'" />
+         </es:BackendResponse>
+         <es:PreLogging>
+            <es:http-post-xml url="'http://127.0.0.1:9876'" section="logdata/LogDatasets" name="roxie_call_exception">
+              <es:content>
+                <es:element name="Envelope">
+                  <es:namespace prefix="soap" uri="http://schemas.xmlsoap.org/soap/envelope/" current="true" />
+                  <es:element name="Body">
+                    <es:element name="nonexistent_query">
+                      <es:namespace uri="urn:hpccsystems:ecl:roxieechopersoninfo" current="true" />
+                      <es:element name="nonexistent_queryrequest">
+                        <es:element name="Row">
+                          <es:element name="Name">
+                            <es:set-value target="First" value="'aaa'"/>
+                            <es:set-value target="Last" value="'bbb'"/>
+                            <es:element name="Aliases">
+                              <es:set-value target="Alias" value="'ccc'"/>
+                              <es:add-value target="Alias" value="'ddd'"/>
+                            </es:element>
+                          </es:element>
+                        </es:element>
+                      </es:element>
+                    </es:element>
+                  </es:element>
+                </es:element>
+              </es:content>
+            </es:http-post-xml>
+         </es:PreLogging>
+         <es:PreLogging>
+            <es:set-value target="PLMTH2" value="'m222'" />
+         </es:PreLogging>
+         <es:PreLogging>
+            <es:set-value target="PLMTH3" value="'m333'" />
+         </es:PreLogging>
+         </Transforms>
+      ]]>
+      </Scripts>
+   </Method>
+</Methods>

+ 22 - 5
system/jlib/jstring.cpp

@@ -957,7 +957,7 @@ StringBuffer &replaceString(StringBuffer & result, size_t lenSource, const char
     return result;
 }
 
-StringBuffer &replaceEnvVariables(StringBuffer & result, const char *source, bool exceptions, const char* delim, const char* term)
+StringBuffer &replaceVariables(StringBuffer & result, const char *source, bool exceptions, IVariableSubstitutionHelper *helper, const char* delim, const char* term)
 {
     if (isEmptyString(source) || isEmptyString(delim) || isEmptyString(term))
         return result;
@@ -974,17 +974,15 @@ StringBuffer &replaceEnvVariables(StringBuffer & result, const char *source, boo
             if (thumb)
             {
                 StringAttr name(finger, (size_t)(thumb - finger));
-                const char *value = getenv(name);
-                if (value)
+                if (helper->findVariable(name, result))
                 {
-                    result.append(value);
                     size_t replaced = (thumb - source) + lenTerm;
                     source = thumb + lenTerm;
                     left -= replaced;
                     continue;
                 }
                 if (exceptions)
-                    throw MakeStringException(-1, "Environment variable %s not set", name.str());
+                    throw MakeStringException(-1, "string substitution variable %s not set", name.str());
             }
         }
         result.append(*source);
@@ -997,6 +995,25 @@ StringBuffer &replaceEnvVariables(StringBuffer & result, const char *source, boo
     return result;
 }
 
+class CEnvVariableSubstitutionHelper : public CInterfaceOf<IVariableSubstitutionHelper>
+{
+public:
+    CEnvVariableSubstitutionHelper(){}
+    virtual bool findVariable(const char *name, StringBuffer &value) override
+    {
+        const char *s = getenv(name);
+        if (s)
+            value.append(s);
+        return s!=nullptr;
+    }
+};
+
+StringBuffer &replaceEnvVariables(StringBuffer & result, const char *source, bool exceptions, const char* delim, const char* term)
+{
+    CEnvVariableSubstitutionHelper helper;
+    return replaceVariables(result, source, exceptions, &helper, delim, term);
+}
+
 StringBuffer &replaceStringNoCase(StringBuffer & result, size_t lenSource, const char *source, size_t lenOldStr, const char* oldStr, size_t lenNewStr, const char* newStr)
 {
     if (lenSource)

+ 7 - 0
system/jlib/jstring.hpp

@@ -404,6 +404,13 @@ extern jlib_decl int utf8CharLen(unsigned char ch);
 extern jlib_decl int utf8CharLen(const unsigned char *ch, unsigned maxsize = (unsigned)-1);
 
 extern jlib_decl StringBuffer &replaceString(StringBuffer & result, size_t lenSource, const char *source, size_t lenOldStr, const char* oldStr, size_t lenNewStr, const char* newStr);
+
+interface IVariableSubstitutionHelper
+{
+    virtual bool findVariable(const char *name, StringBuffer &value) = 0;
+};
+
+extern jlib_decl StringBuffer &replaceVariables(StringBuffer & result, const char *source, bool exceptions, IVariableSubstitutionHelper *helper, const char* delim = "${", const char* term = "}");
 extern jlib_decl StringBuffer &replaceEnvVariables(StringBuffer & result, const char *source, bool exceptions, const char* delim = "${env.", const char* term = "}");
 
 inline const char *encodeUtf8XML(const char *x, StringBuffer &ret, unsigned flags=false, unsigned len=(unsigned)-1)

+ 580 - 38
system/xmllib/libxml_xpathprocessor.cpp

@@ -195,7 +195,7 @@ public:
 
 typedef std::vector<XpathContextState> XPathContextStateVector;
 
-class CLibXpathContext : public CInterfaceOf<IXpathContext>
+class CLibXpathContext : public CInterfaceOf<IXpathContext>, implements IVariableSubstitutionHelper
 {
 public:
     XPathInputMap provided;
@@ -203,6 +203,8 @@ public:
     xmlXPathContextPtr m_xpathContext = nullptr;
     ReadWriteLock m_rwlock;
     XPathScopeVector scopes;
+    Owned<CLibXpathContext> primaryContext; //if set will lookup variables from primaryContext (secondary context is generally target not source)
+
     bool strictParameterDeclaration = true;
     bool removeDocNamespaces = false;
     bool ownedDoc = false;
@@ -211,15 +213,20 @@ public:
     XPathContextStateVector saved;
 
 public:
+    IMPLEMENT_IINTERFACE_USING(CInterfaceOf<IXpathContext>)
+
     CLibXpathContext(const char * xmldoc, bool _strictParameterDeclaration, bool removeDocNs) : strictParameterDeclaration(_strictParameterDeclaration), removeDocNamespaces(removeDocNs)
     {
+        xmlKeepBlanksDefault(0);
         beginScope("/");
         setXmlDoc(xmldoc);
         registerExslt();
     }
 
-    CLibXpathContext(xmlDocPtr doc, xmlNodePtr node, bool _strictParameterDeclaration) : strictParameterDeclaration(_strictParameterDeclaration)
+    CLibXpathContext(CLibXpathContext *_primary, xmlDocPtr doc, xmlNodePtr node, bool _strictParameterDeclaration) : strictParameterDeclaration(_strictParameterDeclaration)
     {
+        xmlKeepBlanksDefault(0);
+        primaryContext.set(_primary);
         beginScope("/");
         setContextDocument(doc, node);
         registerExslt();
@@ -241,7 +248,7 @@ public:
         exsltSetsXpathCtxtRegister(m_xpathContext, (xmlChar*)"set");
         exsltStrXpathCtxtRegister(m_xpathContext, (xmlChar*)"str");
     }
-    void pushLocation()
+    void pushLocation() override
     {
         WriteLockBlock wblock(m_rwlock);
         saved.emplace_back(XpathContextState(m_xpathContext));
@@ -265,7 +272,7 @@ public:
         m_xpathContext->proximityPosition = ctx->proximityPosition;
     }
 
-    void popLocation()
+    void popLocation() override
     {
         WriteLockBlock wblock(m_rwlock);
         saved.back().restore(m_xpathContext);
@@ -341,15 +348,29 @@ public:
         assertex(scopes.size());
         return scopes.back().get();
     }
+
+    bool findVariable(const char *name, StringBuffer &s) override
+    {
+        xmlXPathObjectPtr obj = findVariable(name, nullptr, nullptr);
+        return append(s, obj);
+    }
+
     xmlXPathObjectPtr findVariable(const char *name, const char *ns_uri, CLibXpathScope *scope)
     {
+        xmlXPathObjectPtr obj = nullptr;
+        if (primaryContext)
+        {
+            //could always return from here, but may find a use for secondary variables, they currently can't be defined from esdl script, so can't hurt there
+            obj = primaryContext->findVariable(name, ns_uri, scope);
+            if (obj)
+                return obj;
+        }
         const char *fullname = name;
         StringBuffer s;
         if (!isEmptyString(ns_uri))
             fullname = s.append(ns_uri).append(':').append(name).str();
 
         ReadLockBlock wblock(m_rwlock);
-        xmlXPathObjectPtr obj = nullptr;
         if (scope)
             return scope->getObject(fullname);
 
@@ -372,6 +393,396 @@ public:
         return (findVariable(name, ns_uri, scope)!=nullptr);
     }
 
+    void setLocation(xmlNodePtr node)
+    {
+        WriteLockBlock wblock(m_rwlock);
+        m_xpathContext->doc = node->doc;
+        m_xpathContext->node = node;
+        m_xpathContext->contextSize = 0;
+        m_xpathContext->proximityPosition = 0;
+    }
+
+    bool setLocation(xmlXPathObjectPtr obj, bool required, const char *xpath)
+    {
+        if (!obj)
+        {
+            if (required)
+                throw MakeStringException(XPATHERR_InvalidInput,"XpathContext:setLocation: Error: Syntax error XPATH '%s'", xpath);
+            return false;
+        }
+        if (obj->type!=XPATH_NODESET || !obj->nodesetval || obj->nodesetval->nodeNr==0)
+        {
+            xmlXPathFreeObject(obj);
+            if (required)
+                throw MakeStringException(XPATHERR_EvaluationFailed,"XpathContext:setLocation: Error: Could not evaluate XPATH '%s'", xpath);
+            return false;
+        }
+        if (obj->nodesetval->nodeNr>1)
+        {
+            xmlXPathFreeObject(obj);
+            if (required)
+                throw MakeStringException(XPATHERR_EvaluationFailed,"XpathContext:setLocation: Error: ambiguous XPATH '%s'", xpath);
+            return false;
+        }
+        xmlNodePtr location = obj->nodesetval->nodeTab[0];
+        xmlXPathFreeObject(obj);
+        setLocation(location);
+        return true;
+    }
+
+    virtual bool setLocation(ICompiledXpath * compiledXpath, bool required) override
+    {
+        if (!compiledXpath)
+            return false;
+
+        CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
+        if (!ccXpath)
+            throw MakeStringException(XPATHERR_InvalidState,"XpathProcessor:setLocation: Error: BAD compiled XPATH");
+
+        xmlXPathObjectPtr obj = evaluate(ccXpath->getCompiledXPathExpression(), compiledXpath->getXpath());
+        return setLocation(obj, required, compiledXpath->getXpath());
+    }
+
+    virtual bool setLocation(const char *s, bool required) override
+    {
+        StringBuffer xpath;
+        replaceVariables(xpath, s, false, this, "{$", "}");
+
+        xmlXPathObjectPtr obj = evaluate(xpath);
+        return setLocation(obj, required, xpath.str());
+    }
+
+    virtual void addElementToLocation(const char *name) override
+    {
+        if (!validateXMLTag(name))
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:addElementToLocation: invalid name '%s'", name);
+        xmlNodePtr location = m_xpathContext->node;
+        if (!location)
+            return;
+        location = xmlNewChild(location, nullptr, (const xmlChar *) name, nullptr);
+        if (!location)
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:addElementToLocation: failed to add element '%s'", name);
+        setLocation(location);
+    }
+
+    virtual void addXmlContent(const char *xml) override
+    {
+        xmlNodePtr location = m_xpathContext->node;
+        xmlParserCtxtPtr parserCtx = xmlCreateDocParserCtxt((const xmlChar *)xml);
+        if (!parserCtx)
+            throw MakeStringException(-1, "CLibXpathContext:addXmlContent: Unable to parse xml");
+        parserCtx->node = location;
+        xmlParseDocument(parserCtx);
+        int wellFormed = parserCtx->wellFormed;
+        xmlFreeDoc(parserCtx->myDoc); //dummy document
+        xmlFreeParserCtxt(parserCtx);
+        if (!wellFormed)
+            throw MakeStringException(-1, "XpathContext:addXmlContent: Unable to parse %s XML content", xml);
+
+    }
+    virtual bool ensureLocation(const char *xpath, bool required) override
+    {
+        if (isEmptyString(xpath))
+            return true;
+        if (strstr(xpath, "//")) //maybe in the future, scripting error rather than not found, required==false has no effect
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: '//' not currently allowed '%s'", xpath);
+
+        xmlNodePtr current = m_xpathContext->node;
+        xmlNodePtr node = current;
+        if (*xpath=='/')
+        {
+            xpath++;
+            if (strncmp(xpath, "esdl_script_context/", 20)!=0)
+                throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: incorect first absolute path node '%s'", xpath);
+            xpath+=20;
+            node = xmlDocGetRootElement(m_xmlDoc);
+        }
+
+        StringArray xpathnodes;
+        xpathnodes.appendList(xpath, "/");
+        ForEachItemIn(i, xpathnodes)
+        {
+            const char *finger = xpathnodes.item(i);
+            if (isEmptyString(finger)) //shouldn't happen, scripting error rather than not found, required==false has no effect
+                throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: '//' not allowed '%s'", xpath);
+            if (*finger=='@')
+                throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: attribute cannot be selected '%s'", xpath);
+
+            xmlXPathObjectPtr obj = xmlXPathNodeEval(node, (const xmlChar*) finger, m_xpathContext);
+            xmlXPathSetContextNode(current, m_xpathContext); //restore
+
+            if (!obj)
+                throw MakeStringException(XPATHERR_InvalidInput,"XpathContext:ensureLocation: invalid xpath '%s' at node '%s'", xpath, finger);
+
+            if (obj->type!=xmlXPathObjectType::XPATH_NODESET || !obj->nodesetval || obj->nodesetval->nodeNr==0)
+            {
+                if (*finger=='.' || strpbrk(finger, "[()]*") || strstr(finger, "::")) //complex nodes must already exist
+                {
+                    if (required)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: xpath node not found '%s' in xpath '%s'", finger, xpath);
+                    return false;
+                }
+                node = xmlNewChild(node, nullptr, (const xmlChar *) finger, nullptr);
+                if (!node)
+                {
+                    if (required)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: error creating node '%s' in xpath '%s'", finger, xpath);
+                    return false;
+                }
+            }
+            else
+            {
+                xmlNodePtr *nodes = obj->nodesetval->nodeTab;
+                if (obj->nodesetval->nodeNr>1)
+                {
+                    if (required)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureLocation: ambiguous xpath '%s' at node '%s'", xpath, finger);
+                    return false;
+                }
+                node = *nodes;
+            }
+            xmlXPathFreeObject(obj);
+        }
+        setLocation(node);
+        return true;
+    }
+
+    virtual void setLocationNamespace(const char *prefix, const char * uri, bool current) override
+    {
+        xmlNodePtr node = m_xpathContext->node;
+        if (!node)
+            return;
+        xmlNsPtr ns = nullptr;
+        if (isEmptyString(uri))
+            ns = xmlSearchNs(node->doc, node, (const xmlChar *) prefix);
+        else
+            ns = xmlNewNs(node, (const xmlChar *) uri, (const xmlChar *) prefix);
+        if (current && ns)
+            xmlSetNs(node, ns);
+    }
+
+    enum class ensureValueMode { set, add, appendto };
+    virtual void ensureValue(const char *xpath, const char *value, ensureValueMode action, bool required)
+    {
+        if (isEmptyString(xpath))
+            return;
+        if (strstr(xpath, "//")) //maybe in the future, scripting error rather than not found, required==false has no effect
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: '//' not currently allowed for set operations '%s'", xpath);
+
+        xmlNodePtr current = m_xpathContext->node;
+        if (!current || *xpath=='/')
+        {
+            current = xmlDocGetRootElement(m_xmlDoc); //valuable, but very dangerous
+            if (*xpath=='/')
+                xpath++;
+        }
+        xmlNodePtr node = current;
+
+        StringArray xpathnodes;
+        xpathnodes.appendList(xpath, "/");
+        ForEachItemIn(i, xpathnodes)
+        {
+            const char *finger = xpathnodes.item(i);
+            if (isEmptyString(finger)) //shouldn't happen, scripting error rather than not found, required==false has no effect
+                throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: empty xpath node unexpected '%s'", xpath);
+            bool last = (i == (xpathnodes.ordinality()-1));
+            if (*finger=='@')
+            {
+                if (!last) //scripting error
+                    throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: invalid xpath attribute node '%s' in xpath '%s'", finger, xpath);
+                if (action == ensureValueMode::appendto)
+                {
+                    const char *existing = (const char *) xmlGetProp(node, (const xmlChar *) finger+1);
+                    if (!isEmptyString(existing))
+                    {
+                        StringBuffer appendValue(existing);
+                        appendValue.append(value);
+                        xmlSetProp(node, (const xmlChar *) finger+1, (const xmlChar *) appendValue.str());
+                        return;
+                    }
+                }
+                xmlSetProp(node, (const xmlChar *) finger+1, (const xmlChar *) value);
+                return;
+            }
+            xmlXPathObjectPtr obj = xmlXPathNodeEval(node, (const xmlChar*) finger, m_xpathContext);
+            xmlXPathSetContextNode(current, m_xpathContext);
+
+            if (!obj)
+                throw MakeStringException(XPATHERR_InvalidInput,"XpathContext:ensureValue: invalid xpath '%s' at node '%s'", xpath, finger);
+
+            if (obj->type!=xmlXPathObjectType::XPATH_NODESET || !obj->nodesetval || obj->nodesetval->nodeNr==0)
+            {
+                if (*finger=='.' || strpbrk(finger, "[()]*") || strstr(finger, "::")) //complex nodes must already exist
+                {
+                    if (required)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: xpath node not found '%s' in xpath '%s'", finger, xpath);
+                    return;
+                }
+                const char *nsx = strchr(finger, ':');
+                if (nsx)
+                {
+                    StringAttr prefix(finger, nsx-finger);
+                    finger = nsx+1;
+                    //translate the xpath xmlns prefix into the one defined in the content xml (exception if not found)
+                    const char *uri = queryNamespace(prefix.str());
+                    if (isEmptyString(uri))
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: xmlns prefix '%s' not defined in script used in xpath '%s'", prefix.str(), xpath);
+                    xmlNsPtr ns = xmlSearchNsByHref(m_xmlDoc, node, (const xmlChar *) uri);
+                    if (!ns)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: namespace uri '%s' (referenced as xmlns prefix '%s') not declared in xml ", uri, prefix.str());
+                    node = xmlNewChild(node, ns, (const xmlChar *) finger, last ? (const xmlChar *) value : nullptr);
+                }
+                else
+                {
+                    node = xmlNewChild(node, nullptr, (const xmlChar *) finger, last ? (const xmlChar *) value : nullptr);
+                    //default libxml2 behavior here would be very confusing for scripts.  After adding a tag with no prefix you would need a tag on the xpath to read it.
+                    //Instead when there is no prefix, create a node with no namespace. To match the parent default namespace, define an xpath prefix for it.
+                    //You can always add the namespace you want by declaring it both in the xml (<es:namespace>) and the script (xmlns:prefix on the root node of the script) and then using it on the xpath.
+                    xmlSetNs(node, nullptr);
+                }
+
+                if (!node)
+                {
+                    if (required)
+                        throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: error creating node '%s' in xpath '%s'", finger, xpath);
+                    return;
+                }
+            }
+            else
+            {
+                xmlNodePtr *nodes = obj->nodesetval->nodeTab;
+                if (!last)
+                {
+                    if (obj->nodesetval->nodeNr>1)
+                    {
+                        if (required)
+                            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: ambiguous xpath '%s' at node '%s'", xpath, finger);
+                        return;
+                    }
+                    node = *nodes;
+                }
+                else
+                {
+                    if (obj->nodesetval->nodeNr>1 && action != ensureValueMode::add)
+                    {
+                        if (required)
+                            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:ensureValue: ambiguous xpath '%s'", xpath);
+                        return;
+                    }
+
+                    xmlNodePtr parent = node;
+                    node = *nodes;
+                    switch (action)
+                    {
+                        case ensureValueMode::add:
+                        {
+                            StringBuffer name((const char *)node->name);
+                            xmlNewChild(parent, node->ns, (const xmlChar *) name.str(), (const xmlChar *) value);
+                            break;
+                        }
+                        case ensureValueMode::appendto:
+                        {
+                            xmlChar *existing = xmlNodeGetContent(node);
+                            StringBuffer appendValue((const char *)existing);
+                            xmlNodeSetContent(node, (const xmlChar *) appendValue.append(value).str());
+                            xmlFree(existing);
+                            break;
+                        }
+                        case ensureValueMode::set:
+                        {
+                            xmlNodeSetContent(node, (const xmlChar *) value);
+                            break;
+                        }
+                    }
+                }
+            }
+            xmlXPathFreeObject(obj);
+        }
+    }
+
+    virtual void ensureSetValue(const char *xpath, const char *value, bool required) override
+    {
+        ensureValue(xpath, value, ensureValueMode::set, required);
+    }
+    virtual void ensureAddValue(const char *xpath, const char *value, bool reqired) override
+    {
+        ensureValue(xpath, value, ensureValueMode::add, reqired);
+    }
+    virtual void ensureAppendToValue(const char *xpath, const char *value, bool required) override
+    {
+        ensureValue(xpath, value, ensureValueMode::appendto, required);
+    }
+
+    virtual void rename(const char *xpath, const char *name, bool all) override
+    {
+        if (isEmptyString(xpath) || isEmptyString(name))
+            return;
+        xmlXPathObjectPtr obj = xmlXPathEval((const xmlChar*) xpath, m_xpathContext);
+        if (!obj || obj->type!=xmlXPathObjectType::XPATH_NODESET || obj->nodesetval->nodeNr==0)
+            return;
+        if (obj->nodesetval->nodeNr>1 && !all)
+        {
+            xmlXPathFreeObject(obj);
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:rename: ambiguous xpath '%s' to rename all set 'all'", xpath);
+        }
+        xmlNodePtr *nodes = obj->nodesetval->nodeTab;
+        for (int i = 0; i < obj->nodesetval->nodeNr; i++)
+            xmlNodeSetName(nodes[i], (const xmlChar *)name);
+        xmlXPathFreeObject(obj);
+    }
+
+    virtual void remove(const char *xpath, bool all) override
+    {
+        if (isEmptyString(xpath))
+            return;
+        xmlXPathObjectPtr obj = xmlXPathEval((const xmlChar*) xpath, m_xpathContext);
+        if (!obj || obj->type!=xmlXPathObjectType::XPATH_NODESET || !obj->nodesetval || obj->nodesetval->nodeNr==0)
+            return;
+        if (obj->nodesetval->nodeNr>1 && !all)
+        {
+            xmlXPathFreeObject(obj);
+            throw MakeStringException(XPATHERR_InvalidState,"XpathContext:remove: ambiguous xpath '%s' to remove all set 'all'", xpath);
+        }
+        xmlNodePtr *nodes = obj->nodesetval->nodeTab;
+        for (int i = obj->nodesetval->nodeNr - 1; i >= 0; i--)
+        {
+            xmlUnlinkNode(nodes[i]);
+            xmlFreeNode(nodes[i]);
+            nodes[i]=nullptr;
+        }
+        xmlXPathFreeObject(obj);
+    }
+
+    virtual void copyFromPrimaryContext(ICompiledXpath *select, const char *newname) override
+    {
+        if (!primaryContext)
+            return;
+        xmlNodePtr current = m_xpathContext->node;
+        if (!current || !select)
+            return;
+        CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(select);
+        xmlXPathObjectPtr obj = primaryContext->evaluate(ccXpath->getCompiledXPathExpression(), ccXpath->getXpath());
+        if (!obj)
+            throw MakeStringException(XPATHERR_InvalidState, "XpathContext:copyof xpath syntax error '%s'", ccXpath->getXpath());
+        if (obj->type!=xmlXPathObjectType::XPATH_NODESET || !obj->nodesetval || obj->nodesetval->nodeNr==0)
+        {
+            xmlXPathFreeObject(obj);
+            return;
+        }
+        xmlNodePtr *nodes = obj->nodesetval->nodeTab;
+        for (int i = 0; i < obj->nodesetval->nodeNr; i++)
+        {
+            xmlNodePtr copy = xmlCopyNode(nodes[i], 1);
+            if (!copy)
+                continue;
+            if (!isEmptyString(newname))
+                xmlNodeSetName(copy, (const xmlChar *)newname);
+            xmlAddChild(current, copy);
+        }
+        xmlXPathFreeObject(obj);
+    }
+
+
     virtual bool addObjectVariable(const char * name, xmlXPathObjectPtr obj, CLibXpathScope *scope)
     {
         if (isEmptyString(name))
@@ -417,10 +828,10 @@ public:
             addVariable(name, "");
         if (m_xpathContext)
         {
-            CLibCompiledXpath * clibCompiledXpath = static_cast<CLibCompiledXpath *>(compiled);
-            xmlXPathObjectPtr obj = evaluate(clibCompiledXpath->getCompiledXPathExpression(), clibCompiledXpath->getXpath());
+            CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(compiled);
+            xmlXPathObjectPtr obj = evaluate(ccXpath->getCompiledXPathExpression(), ccXpath->getXpath());
             if (!obj)
-                throw MakeStringException(-1, "addEvaluateVariable xpath error %s", clibCompiledXpath->getXpath());
+                throw MakeStringException(-1, "addEvaluateVariable xpath error %s", ccXpath->getXpath());
             return addObjectVariable(name, obj, scope);
         }
 
@@ -538,6 +949,26 @@ public:
         return evaluateAsBoolean(evaluate(xpath), xpath);
     }
 
+    virtual StringBuffer &toXml(const char *xpath, StringBuffer & xml) override
+    {
+        xmlXPathObjectPtr obj = evaluate(xpath);
+        if (!obj || !obj->type==XPATH_NODESET || !obj->nodesetval || !obj->nodesetval->nodeTab || obj->nodesetval->nodeNr!=1)
+            return xml;
+        xmlNodePtr node = obj->nodesetval->nodeTab[0];
+        if (!node)
+            return xml;
+
+        xmlOutputBufferPtr xmlOut = xmlAllocOutputBuffer(nullptr);
+        xmlNodeDumpOutput(xmlOut, node->doc, node, 0, 1, nullptr);
+        xmlOutputBufferFlush(xmlOut);
+        xmlBufPtr buf = (xmlOut->conv != nullptr) ? xmlOut->conv : xmlOut->buffer;
+        if (xmlBufUse(buf))
+            xml.append(xmlBufUse(buf), (const char *)xmlBufContent(buf));
+        xmlOutputBufferClose(xmlOut);
+        xmlXPathFreeObject(obj);
+        return xml;
+    }
+
     virtual bool evaluateAsString(const char * xpath, StringBuffer & evaluated) override
     {
         if (!xpath || !*xpath)
@@ -547,26 +978,26 @@ public:
 
     virtual bool evaluateAsBoolean(ICompiledXpath * compiledXpath) override
     {
-        CLibCompiledXpath * clibCompiledXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
-        if (!clibCompiledXpath)
+        CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
+        if (!ccXpath)
             throw MakeStringException(XPATHERR_MissingInput,"XpathProcessor:evaluateAsBoolean: Error: Missing compiled XPATH");
-        return evaluateAsBoolean(evaluate(clibCompiledXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), compiledXpath->getXpath());
+        return evaluateAsBoolean(evaluate(ccXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), compiledXpath->getXpath());
     }
 
     virtual const char * evaluateAsString(ICompiledXpath * compiledXpath, StringBuffer & evaluated) override
     {
-        CLibCompiledXpath * clibCompiledXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
-        if (!clibCompiledXpath)
+        CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
+        if (!ccXpath)
             throw MakeStringException(XPATHERR_MissingInput,"XpathProcessor:evaluateAsString: Error: Missing compiled XPATH");
-        return evaluateAsString(evaluate(clibCompiledXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), evaluated, compiledXpath->getXpath());
+        return evaluateAsString(evaluate(ccXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), evaluated, compiledXpath->getXpath());
     }
 
     virtual double evaluateAsNumber(ICompiledXpath * compiledXpath) override
     {
-        CLibCompiledXpath * clibCompiledXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
-        if (!clibCompiledXpath)
+        CLibCompiledXpath * ccXpath = static_cast<CLibCompiledXpath *>(compiledXpath);
+        if (!ccXpath)
             throw MakeStringException(XPATHERR_MissingInput,"XpathProcessor:evaluateAsNumber: Error: Missing compiled XPATH");
-        return evaluateAsNumber(evaluate(clibCompiledXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), compiledXpath->getXpath());
+        return evaluateAsNumber(evaluate(ccXpath->getCompiledXPathExpression(), compiledXpath->getXpath()), compiledXpath->getXpath());
     }
 
     virtual bool setXmlDoc(const char * xmldoc) override
@@ -716,6 +1147,18 @@ private:
         return evaluated.str();
     }
 
+    bool append(StringBuffer &s, xmlXPathObjectPtr obj)
+    {
+        if (!obj)
+            return false;
+        xmlChar *value = xmlXPathCastToString(obj);
+        if (!value || !*value)
+            return false;
+        s.append((const char *) value);
+        xmlFree(value);
+        return true;
+    }
+
     double evaluateAsNumber(xmlXPathObjectPtr evaluatedXpathObj, const char* xpath)
     {
         if (!evaluatedXpathObj)
@@ -746,21 +1189,12 @@ private:
 
     virtual xmlXPathObjectPtr evaluate(const char * xpath)
     {
-        xmlXPathObjectPtr evaluatedXpathObj = nullptr;
-        if (xpath && *xpath)
-        {
-            ReadLockBlock rlock(m_rwlock);
-            if ( m_xpathContext)
-            {
-                evaluatedXpathObj = xmlXPathEval((const xmlChar *)xpath, m_xpathContext);
-            }
-            else
-            {
-                throw MakeStringException(XPATHERR_InvalidState,"XpathProcessor:evaluate: Error: Could not evaluate XPATH '%s'; ensure xmldoc has been set", xpath);
-            }
-        }
-
-        return evaluatedXpathObj;
+        if (isEmptyString(xpath))
+            return nullptr;
+        if (!m_xpathContext)
+            throw MakeStringException(XPATHERR_InvalidState,"XpathProcessor:evaluate: Error: Could not evaluate XPATH '%s'; ensure xmldoc has been set", xpath);
+        ReadLockBlock rlock(m_rwlock);
+        return xmlXPathEval((const xmlChar *)xpath, m_xpathContext);
     }
 };
 
@@ -988,6 +1422,7 @@ private:
         xmlXPathFreeObject(eval);
         return sect;
     }
+
     void addXpathCtxConfigInputs(IXpathContext *tgtXpathCtx)
     {
         xmlXPathObjectPtr eval = xmlXPathEval((const xmlChar *) "config/*/Transform/Param", xpathCtx);
@@ -1064,6 +1499,36 @@ private:
     {
         return getSectionNode(name, nullptr);
     }
+
+    xmlNodePtr ensureCopiedSection(const char *target, const char *source, bool replace)
+    {
+        if (isEmptyString(source)||isEmptyString(target))
+            return nullptr;
+        sanityCheckSectionName(target);
+        sanityCheckSectionName(source);
+
+        xmlNodePtr targetNode = getSectionNode(target, nullptr);
+        if (targetNode && !replace)
+            return targetNode;
+
+        xmlNodePtr sourceNode = getSectionNode(source, nullptr);
+        if (!sourceNode)
+            return nullptr;
+
+        xmlNodePtr copyNode = xmlCopyNode(sourceNode, 1);
+        xmlNodeSetName(copyNode, (const xmlChar *) target);
+
+        if (targetNode)
+        {
+            xmlNodePtr oldNode = xmlReplaceNode(targetNode, copyNode);
+            xmlFreeNode(oldNode);
+            return copyNode;
+        }
+
+        xmlAddChild(root, copyNode);
+        return copyNode;
+    }
+
     xmlNodePtr ensureSection(const char *name)
     {
         sanityCheckSectionName(name);
@@ -1117,6 +1582,47 @@ private:
         if (!wellFormed)
            throw MakeStringException(-1, "CEsdlScriptContext:setContent xml string: Unable to parse %s XML content", section);
     }
+    virtual void appendContent(const char *section, const char *name, const char *xml) override
+    {
+        if (xml==nullptr)
+            return;
+
+        xmlNodePtr sect = ensureSection(section);
+        xmlNodePtr sectNode = getSectionNode(section, name);
+
+        xmlDocPtr doc = xmlParseDoc((const xmlChar *)xml);
+        if (!doc)
+            return;
+        xmlNodePtr content = xmlDocGetRootElement(doc);
+        if (!content)
+        {
+            xmlFreeDoc(doc);
+            return;
+        }
+        if (!sectNode)
+        {
+            xmlUnlinkNode(content);
+            if (!isEmptyString(name))
+                xmlNodeSetName(content, (const xmlChar *) name);
+            xmlAddChild(sect, content);
+            xmlFree(doc);
+            return;
+        }
+        xmlAttrPtr prop = content->properties;
+        while (prop != nullptr)
+        {
+            xmlChar *value = xmlGetProp(content, prop->name);
+            xmlSetProp(sectNode, prop->name, value);
+            xmlFree(value);
+            prop = prop->next;
+        }
+        for (xmlNodePtr node = xmlFirstElementChild(content); node != nullptr; node = xmlFirstElementChild(content))
+        {
+            xmlUnlinkNode(node);
+            xmlAddChild(sectNode, node);
+        }
+        xmlFreeDoc(doc);
+    }
     virtual void setContent(const char *section, IPropertyTree *tree) override
     {
         xmlNodePtr sect = replaceSection(section);
@@ -1152,6 +1658,30 @@ private:
         xmlFree(val);
         return s;
     }
+    virtual bool tokenize(const char *str, const char *delimeters, StringBuffer &resultPath)
+    {
+        xmlNodePtr sect = ensureSection("temporaries");
+        if (!sect)
+            return false;
+
+        StringBuffer unique("T");
+        appendGloballyUniqueId(unique);
+        xmlNodePtr container = xmlNewChild(sect, nullptr, (const xmlChar *) unique.str(), nullptr);
+        if (!container)
+            return false;
+
+        resultPath.set("/esdl_script_context/temporaries/").append(unique.str()).append("/token");
+
+        StringArray tkns;
+        tkns.appendList(str, delimeters);
+        ForEachItemIn(i, tkns)
+        {
+            const char *tkn = tkns.item(i);
+            xmlNewChild(container, nullptr, (const xmlChar *) "token", (const xmlChar *) tkn);
+        }
+        return true;
+    }
+
 
     virtual void toXML(StringBuffer &xml, const char *section, bool includeParentNode=false) override
     {
@@ -1170,7 +1700,6 @@ private:
         if (xmlBufUse(buf))
             xml.append(xmlBufUse(buf), (const char *)xmlBufContent(buf));
         xmlOutputBufferClose(xmlOut);
-
     }
     virtual IPropertyTree *createPTreeFromSection(const char *section) override
     {
@@ -1186,12 +1715,16 @@ private:
     {
         toXML(xml, nullptr, true);
     }
-    IXpathContext* createXpathContext(const char *section, bool strictParameterDeclaration) override
+    IXpathContext* createXpathContext(IXpathContext *primaryContext, const char *section, bool strictParameterDeclaration) override
     {
-        xmlNodePtr sect = getSectionNode(section);
-        if (!sect)
-            throw MakeStringException(-1, "CEsdlScriptContext:createXpathContext: section not found %s", section);
-        CLibXpathContext *xpathContext = new CLibXpathContext(doc, sect, strictParameterDeclaration);
+        xmlNodePtr sect = nullptr;
+        if (!isEmptyString(section))
+        {
+            sect = getSectionNode(section);
+            if (!sect)
+                throw MakeStringException(-1, "CEsdlScriptContext:createXpathContext: section not found %s", section);
+        }
+        CLibXpathContext *xpathContext = new CLibXpathContext(static_cast<CLibXpathContext *>(primaryContext), doc, sect, strictParameterDeclaration);
         StringBuffer val;
         xpathContext->addVariable("method", getAttribute("esdl", "method", val));
         xpathContext->addVariable("service", getAttribute("esdl", "service", val.clear()));
@@ -1204,6 +1737,15 @@ private:
 
         return xpathContext;
     }
+    IXpathContext *getCopiedSectionXpathContext(IXpathContext *primaryContext, const char *tgtSection, const char *srcSection, bool strictParameterDeclaration) override
+    {
+        ensureCopiedSection(tgtSection, srcSection, false);
+        return createXpathContext(primaryContext, tgtSection, strictParameterDeclaration);
+    }
+    virtual void cleanupBetweenScripts() override
+    {
+        removeSection("temporaries");
+    }
 };
 
 IEsdlScriptContext *createEsdlScriptContext(void * espCtx)

+ 5 - 0
system/xmllib/xalan_processor.cpp

@@ -703,3 +703,8 @@ extern IXpathContext* getXpathContext(const char * xmldoc, bool strictParameterD
 {
     UNIMPLEMENTED;
 }
+
+extern IXpathContext *createChildXpathContext(IXpathContext *parent, IEsdlScriptContext *scriptContext, const char *section, bool strictParameterDeclaration, bool removeDocNamespaces)
+{
+    UNIMPLEMENTED;
+}

+ 1 - 0
system/xmllib/xmlerror.hpp

@@ -47,6 +47,7 @@
 #define XPATHERR_MissingInput                           5681
 #define XPATHERR_InvalidInput                           5682
 #define XPATHERR_UnexpectedInput                        5683
+#define XPATHERR_EvaluationFailed                       5684
 
 
 #endif

+ 56 - 16
system/xmllib/xpathprocessor.hpp

@@ -31,19 +31,6 @@ interface IXpathContextIterator;
 
 interface XMLLIB_API IXpathContext : public IInterface
 {
-    virtual bool addVariable(const char * name, const char * val) = 0;
-    virtual bool addXpathVariable(const char * name, const char * xpath) = 0;
-    virtual bool addCompiledVariable(const char * name, ICompiledXpath * compiled) = 0;
-
-    virtual const char * getVariable(const char * name, StringBuffer & variable) = 0;
-
-    virtual bool evaluateAsBoolean(const char * xpath) = 0;
-    virtual bool evaluateAsString(const char * xpath, StringBuffer & evaluated) = 0;
-    virtual bool evaluateAsBoolean(ICompiledXpath * compiledXpath) = 0;
-    virtual const char * evaluateAsString(ICompiledXpath * compiledXpath, StringBuffer & evaluated) = 0;
-    virtual double evaluateAsNumber(ICompiledXpath * compiledXpath) = 0;
-    virtual  IXpathContextIterator *evaluateAsNodeSet(ICompiledXpath * compiledXpath) = 0;
-
     virtual bool setXmlDoc(const char * xmldoc) = 0;
     virtual void setUserData(void *) = 0;
     virtual void *getUserData() = 0;
@@ -51,14 +38,42 @@ interface XMLLIB_API IXpathContext : public IInterface
     virtual void registerFunction(const char *xmlns, const char * name, void *f) = 0;
     virtual void registerNamespace(const char *prefix, const char *uri) = 0;
     virtual const char *queryNamespace(const char *prefix) = 0;
+
     virtual void beginScope(const char *name) = 0;
     virtual void endScope() = 0;
 
+    virtual bool addVariable(const char * name, const char * val) = 0;
+    virtual bool addXpathVariable(const char * name, const char * xpath) = 0;
+    virtual bool addCompiledVariable(const char * name, ICompiledXpath * compiled) = 0;
+    virtual const char * getVariable(const char * name, StringBuffer & variable) = 0;
     virtual bool addInputXpath(const char * name, const char * xpath) = 0; //values should be declared as parameters before use, "strict parameter mode" requires it
     virtual bool addInputValue(const char * name, const char * value) = 0; //values should be declared as parameters before use, "strict parameter mode" requires it
     virtual bool declareParameter(const char * name, const char *value) = 0;
     virtual bool declareCompiledParameter(const char * name, ICompiledXpath * compiled) = 0;
     virtual void declareRemainingInputs() = 0;
+
+    virtual void pushLocation() = 0;
+    virtual void popLocation() = 0;
+    virtual bool setLocation(const char *xpath, bool required) = 0;
+    virtual bool setLocation(ICompiledXpath * compiledXpath, bool required) = 0;
+    virtual bool ensureLocation(const char *xpath, bool required) = 0;
+    virtual void addElementToLocation(const char *name) = 0;
+    virtual void setLocationNamespace(const char *prefix, const char *uri, bool current) = 0;
+    virtual void ensureSetValue(const char *xpath, const char *value, bool required) = 0;
+    virtual void ensureAddValue(const char *xpath, const char *value, bool required) = 0;
+    virtual void ensureAppendToValue(const char *xpath, const char *value, bool required) = 0;
+    virtual void rename(const char *xpath, const char *name, bool all) = 0;
+    virtual void remove(const char *xpath, bool all) = 0;
+    virtual void copyFromPrimaryContext(ICompiledXpath *select, const char *newname) = 0;
+
+    virtual bool evaluateAsBoolean(const char * xpath) = 0;
+    virtual bool evaluateAsString(const char * xpath, StringBuffer & evaluated) = 0;
+    virtual bool evaluateAsBoolean(ICompiledXpath * compiledXpath) = 0;
+    virtual const char * evaluateAsString(ICompiledXpath * compiledXpath, StringBuffer & evaluated) = 0;
+    virtual double evaluateAsNumber(ICompiledXpath * compiledXpath) = 0;
+    virtual  IXpathContextIterator *evaluateAsNodeSet(ICompiledXpath * compiledXpath) = 0;
+    virtual StringBuffer &toXml(const char *xpath, StringBuffer & xml) = 0;
+    virtual void addXmlContent(const char *xml) = 0;
 };
 
 interface IXpathContextIterator : extends IIteratorOf<IXpathContext> { };
@@ -90,23 +105,47 @@ public:
     }
 };
 
+class CXpathContextLocation : CInterface
+{
+private:
+    Linked<IXpathContext> context;
+public:
+    IMPLEMENT_IINTERFACE;
+    CXpathContextLocation(IXpathContext *ctx) : context(ctx)
+    {
+        if (context)
+            context->pushLocation();
+    }
+    virtual ~CXpathContextLocation()
+    {
+        if (context)
+            context->popLocation();
+    }
+};
+
 extern "C" XMLLIB_API ICompiledXpath* compileXpath(const char * xpath);
 extern "C" XMLLIB_API IXpathContext*  getXpathContext(const char * xmldoc, bool strictParameterDeclaration, bool removeDocNamespaces);
 
 #define ESDLScriptCtxSection_Store "store"
 #define ESDLScriptCtxSection_Logging "logging"
+#define ESDLScriptCtxSection_LogData "logdata"
 #define ESDLScriptCtxSection_TargetConfig "target"
 #define ESDLScriptCtxSection_BindingConfig "config"
 #define ESDLScriptCtxSection_ESDLInfo "esdl"
-#define ESDLScriptCtxSection_ESDLRequest "ESDLRequest"
-#define ESDLScriptCtxSection_FinalRequest "FinalRequest"
+#define ESDLScriptCtxSection_ESDLRequest "esdl_request"
+#define ESDLScriptCtxSection_FinalRequest "final_request"
+#define ESDLScriptCtxSection_InitialResponse "initial_response"
+#define ESDLScriptCtxSection_PreESDLResponse "pre_esdl_response"
 
 interface IEsdlScriptContext : extends IInterface
 {
-    virtual IXpathContext* createXpathContext(const char *section, bool strictParameterDeclaration) = 0;
+    virtual IXpathContext* createXpathContext(IXpathContext *parent, const char *section, bool strictParameterDeclaration) = 0;
+    virtual IXpathContext *getCopiedSectionXpathContext(IXpathContext *parent, const char *tgtSection, const char *srcSection, bool strictParameterDeclaration) = 0;
     virtual void *queryEspContext() = 0;
     virtual void setContent(const char *section, const char *xml) = 0;
+    virtual void appendContent(const char *section, const char *name, const char *xml) = 0;
     virtual void setContent(const char *section, IPropertyTree *tree) = 0;
+    virtual bool tokenize(const char *str, const char *delimeters, StringBuffer &resultPath) = 0;
     virtual void setAttribute(const char *section, const char *name, const char *value) = 0;
     virtual const char *queryAttribute(const char *section, const char *name) = 0;
     virtual const char *getAttribute(const char *section, const char *name, StringBuffer &s) = 0;
@@ -116,6 +155,7 @@ interface IEsdlScriptContext : extends IInterface
     virtual void toXML(StringBuffer &xml, const char *section, bool includeParentNode=false) = 0;
     virtual void toXML(StringBuffer &xml) = 0;
     virtual IPropertyTree *createPTreeFromSection(const char *section) = 0;
+    virtual void cleanupBetweenScripts() = 0;
 };
 
 extern "C" XMLLIB_API IEsdlScriptContext *createEsdlScriptContext(void * espContext);

+ 578 - 22
testing/unittests/esdltests.cpp

@@ -211,6 +211,14 @@ static constexpr const char * esdlScriptSelectPath = R"!!(
 </es:CustomRequestTransform>
 )!!";
 
+static constexpr const char * esdlImplicitNamespaceSelectPath = R"!!(
+<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="soap:Body/n:extra/n:{$query}/n:{$request}">
+  <es:param name="selectPath" select="''"/>
+
+  <es:set-value target="_OUTPUT_" select="$selectPath"/>
+</es:CustomRequestTransform>
+)!!";
+
 static constexpr const char* selectPathResult = R"!!(<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
     <extra>
@@ -247,6 +255,14 @@ static constexpr const char* selectPathResult = R"!!(<soap:Envelope xmlns:soap="
   </soap:Body>
 </soap:Envelope>)!!";
 
+bool areEquivalentTestXMLStrings(const char *xml1, const char *xml2)
+{
+    if (isEmptyString(xml1) || isEmptyString(xml2))
+        return false;
+    Owned<IPropertyTree> tree1 = createPTreeFromXMLString(xml1);
+    Owned<IPropertyTree> tree2 = createPTreeFromXMLString(xml2);
+    return areMatchingPTrees(tree1, tree2);
+}
 
 static const char *target_config = "<method queryname='EchoPersonInfo'/>";
 
@@ -275,6 +291,9 @@ class ESDLTests : public CppUnit::TestFixture
         CPPUNIT_TEST(testEsdlTransformImplicitPrefix);
         CPPUNIT_TEST(testEsdlTransformRequestNamespaces);
         CPPUNIT_TEST(testScriptContext);
+        CPPUNIT_TEST(testTargetElement);
+      //CPPUNIT_TEST(testScriptMap); //requires a particular roxie query
+      //CPPUNIT_TEST(testHTTPPostXml); //requires a particular roxie query
     CPPUNIT_TEST_SUITE_END();
 
 public:
@@ -297,14 +316,14 @@ public:
         scriptContext->setAttribute(ESDLScriptCtxSection_ESDLInfo, "request", "EchoPersonInfoRequest");
 
         scriptContext->setContent(ESDLScriptCtxSection_BindingConfig, config);
+        scriptContext->setContent(ESDLScriptCtxSection_TargetConfig, target_config);
         scriptContext->setContent(ESDLScriptCtxSection_ESDLRequest, xml);
         return scriptContext.getClear();
     }
 
     void runTransform(IEsdlScriptContext *scriptContext, const char *scriptXml, const char *srcSection, const char *tgtSection, const char *testname, int code)
     {
-        Owned<IPropertyTree> script = createPTreeFromXMLString(scriptXml);
-        Owned<IEsdlCustomTransform> tf = createEsdlCustomTransform(*script, nullptr);
+        Owned<IEsdlCustomTransform> tf = createEsdlCustomTransform(scriptXml, nullptr);
 
         tf->processTransform(scriptContext, srcSection, tgtSection);
         if (code)
@@ -314,17 +333,21 @@ public:
     {
         try
         {
+            //printf("starting %s:\n", testname);  //uncomment to help debug
             Owned<IEspContext> ctx = createEspContext(nullptr);
             Owned<IEsdlScriptContext> scriptContext = createTestScriptContext(ctx, xml, config);
             runTransform(scriptContext, scriptXml, ESDLScriptCtxSection_ESDLRequest, ESDLScriptCtxSection_FinalRequest, testname, code);
 
             StringBuffer output;
-            scriptContext->toXML(output.clear(), "FinalRequest");
-            if (result && !streq(result, output.str()))
+            scriptContext->toXML(output.clear(), ESDLScriptCtxSection_FinalRequest);
+
+
+            if (result && !areEquivalentTestXMLStrings(result, output.str()))
             {
                 fputs(output.str(), stdout);
                 fflush(stdout);
-                throw MakeStringException(100, "Test failed(%s)", testname);
+                fprintf(stdout, "\nTest failed(%s)\n", testname);
+                CPPUNIT_ASSERT(false);
             }
         }
         catch (IException *E)
@@ -334,7 +357,8 @@ public:
             {
                 StringBuffer m;
                 fprintf(stdout, "\nTest(%s) Expected %d Exception %d - %s\n", testname, code, E->errorCode(), E->errorMessage(m).str());
-                throw E;
+                E->Release();
+                CPPUNIT_ASSERT(false);
             }
             E->Release();
         }
@@ -360,7 +384,7 @@ public:
         <config strictParams='true'>
           <Transform>
             <Param name='testcase' value="absolute-soap-path"/>
-            <Param name='selectPath' select="/esdl_script_context/ESDLRequest/soap:Envelope/soap:Body/extra/EchoPersonInfo/EchoPersonInfoRequest/Row/Name/First"/>
+            <Param name='selectPath' select="/esdl_script_context/esdl_request/soap:Envelope/soap:Body/extra/EchoPersonInfo/EchoPersonInfoRequest/Row/Name/First"/>
           </Transform>
         </config>
       )!!";
@@ -488,11 +512,11 @@ public:
         </config>
       )!!";
 
-      runTest("implicit-prefix", esdlScriptSelectPath, soapRequestImplicitPrefix, config, implicitPrefixResult, 0);
+      runTest("implicit-prefix", esdlImplicitNamespaceSelectPath, soapRequestImplicitPrefix, config, implicitPrefixResult, 0);
 
       // The implicit 'n' prefix is required if the content has a namespace defined
       // with no prefix. This test is expected to throw an exception.
-      runTest("implicit-prefix-not-used", esdlScriptSelectPath, soapRequestImplicitPrefix, configNoPrefix, implicitPrefixResult, 99);
+      runTest("implicit-prefix-not-used", esdlImplicitNamespaceSelectPath, soapRequestImplicitPrefix, configNoPrefix, implicitPrefixResult, 99);
     }
 
     void testEsdlTransformRequestNamespaces()
@@ -685,11 +709,11 @@ public:
       )!!";
 
       // An invalid namespace URI that is expected to throw an exception
-      runTest("invalid-uri", esdlScriptSelectPath, soapRequestNsInvalid, configInvalidURI, namespaceResult, 99);
+      runTest("invalid-uri", esdlScriptSelectPath, soapRequestNsInvalid, configInvalidURI, namespaceResult, 5684);
 
       // Weird but valid URIs for namespaces
-      runTest("arbitrary-uri-1", esdlScriptSelectPath, soapRequestNsArbitrary1, configArbitraryURI1, namespaceResultArbitrary1, 0);
-      runTest("arbitrary-uri-2", esdlScriptSelectPath, soapRequestNsArbitrary2, configArbitraryURI2, namespaceResultArbitrary2, 0);
+      runTest("arbitrary-uri-1", esdlImplicitNamespaceSelectPath, soapRequestNsArbitrary1, configArbitraryURI1, namespaceResultArbitrary1, 0);
+      runTest("arbitrary-uri-2", esdlImplicitNamespaceSelectPath, soapRequestNsArbitrary2, configArbitraryURI2, namespaceResultArbitrary2, 0);
 
     }
 
@@ -1008,7 +1032,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
     <Param name='testcase' value="for each friend"/>
     <Param name='section' select="'Friends'"/>
     <Param name='garbage' select="''"/>
-    <Param name='ForBuildListPath' select='/esdl_script_context/ESDLRequest/root/extra/Friends/Name'/><!--absolute path may change, highly frowned upon-->
+    <Param name='ForBuildListPath' select='/esdl_script_context/esdl_request/root/extra/Friends/Name'/>
     <Param name='ForIdPath' select='extra/Friends/Name/First'/>
   </Transform>
 </config>)!!";
@@ -1063,7 +1087,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
     <Param name='section' select="'Relatives'"/>
     <Param name='garbage' select="''"/>
     <Param name='ForBuildListPath' select='extra/Relatives/Name'/>
-    <Param name='ForIdPath' select='/esdl_script_context/ESDLRequest/root/extra/Relatives/Name/First'/> <!--absolute path may change, highly frowned upon-->
+    <Param name='ForIdPath' select='/esdl_script_context/esdl_request/root/extra/Relatives/Name/First'/> <!--absolute path may change, highly frowned upon-->
   </Transform>
 </config>)!!";
 
@@ -1079,7 +1103,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
   </Transform>
 </config>)!!";
 
-        runTest("for each garbage path error", forEachScript, input, configGarbagePathError, nullptr, -1);
+        runTest("for each garbage path error", forEachScript, input, configGarbagePathError, nullptr, 5682);
 
         constexpr const char * resultNada = R"!!(<root xmlns:xx1="urn:x1" xmlns:xx2="urn:x2">
   <extra>
@@ -1317,7 +1341,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
 
         constexpr const char *config = R"!!(<config><Transform><Param name='testcase' value="target xpath errors"/></Transform></config>)!!";
 
-        runTest("target xpath errors", script, input, config, nullptr, -1); //createPropBranch: cannot create path : ID##
+        runTest("target xpath errors", script, input, config, nullptr, 5682); //createPropBranch: cannot create path : ID##
 
         static constexpr const char * script2 = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="Person">
             <es:SetValue target="ID##" select="/root/Person/Name/First"/>
@@ -1326,7 +1350,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
         </es:CustomRequestTransform>
         )!!";
 
-        runTest("target xpath errors", script2, input, config, nullptr, -1); //createPropBranch: cannot create path : ID##
+        runTest("target xpath errors", script2, input, config, nullptr, 5682); //createPropBranch: cannot create path : ID##
 }
     void testEsdlTransformFailLevel1A()
     {
@@ -1445,12 +1469,12 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
         )!!";
 
         static constexpr const char * script = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="Person">
-            <es:parameter name="testpass"/> 
+            <es:param name="testpass"/>
             <es:if test="es:storedValueExists('myvalue')">
               <es:set-value xpath_target="concat('check-value1-pass-', $testpass)" select="concat('already set as of pass-', $testpass)"/>
               <es:set-value target="myvalue" select="es:getStoredStringValue('myvalue')"/>
               <es:remove-node target="Name/ID[1]"/> //removing first one changes the index count so each is 1
-              <es:remove-node target="Name/ID[1]"/> 
+              <es:remove-node target="Name/ID[1]"/>
             </es:if>
             <es:if test="es:storedValueExists('myvalue2')">
               <es:set-value xpath_target="concat('check-value2-pass-', $testpass)" select="concat('already set in pass-', $testpass)"/>
@@ -1469,7 +1493,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
               <es:set-log-option name="option1" select="'this is a logging option value'"/>
               <es:set-log-option xpath_name="'option2'" select="'this is an xpath named logging option value'"/>
               <es:set-log-profile select="'myprofile'"/>
-              <es:rename-node target="FullName" new_name="Name"/> 
+              <es:rename-node target="FullName" new_name="Name"/>
             </es:if>
         </es:CustomRequestTransform>
         )!!";
@@ -1516,7 +1540,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
 
             StringBuffer output;
             scriptContext->toXML(output, "SecondPass");
-            if (result && !streq(result, output.str()))
+            if (result && !areEquivalentTestXMLStrings(result, output.str()))
             {
                 fputs(output.str(), stdout);
                 fflush(stdout);
@@ -1527,9 +1551,541 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
         {
             StringBuffer m;
             fprintf(stdout, "\nTest(%s) Exception %d - %s\n", "script context", E->errorCode(), E->errorMessage(m).str());
-            throw E;
+            E->Release();
+            CPPUNIT_ASSERT(false);
+        }
+    }
+
+    void testTargetElement()
+    {
+        static constexpr const char * input = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+         <root>
+            <Person>
+               <FullName>
+                  <First>Joe</First>
+                  <ID>GI101</ID>
+                  <ID>GI102</ID>
+               </FullName>
+               <Friends>Jane,John,Jaap,Jessica</Friends>
+            </Person>
+          </root>
+        )!!";
+
+        static constexpr const char * script = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="Person">
+            <es:variable name='value' select="'abc'"/>
+            <es:if-source xpath="NotThere">
+               <es:element name="NeverHere">
+                  <es:copy-of select="."/>
+               </es:element>
+            </es:if-source>
+            <es:if-source xpath="Person">
+              <es:set-value target="@found" select="true()"/>
+            </es:if-source>
+            <es:source xpath="Person">
+              <es:ensure-target xpath='How/Did/We/Get'>
+                 <es:element name='Here'>
+                    <es:variable name="tkns" select="es:tokenize('aaa,bbb;ccc+yyy', ',;+')"/>
+                    <es:variable name="friends" select="es:tokenize(Friends, ',')"/>
+                    <es:set-value target="@whoknows" select="$value"/>
+                    <es:set-value target="IDontKnow" select="$value"/>
+                    <es:element name="CopyTokens">
+                      <es:copy-of select="$tkns"/>
+                    </es:element>
+                    <es:element name="CopyFriends">
+                      <es:copy-of select="$friends"/>
+                    </es:element>
+                    <es:element name="CopyFields">
+                      <es:for-each select="FullName/*">
+                          <es:element name="CopyField">
+                            <es:copy-of select="."/>
+                          </es:element>
+                      </es:for-each>
+                    </es:element>
+                    <es:element name="CopyFullName">
+                       <es:copy-of select="FullName" new_name='FullerName'/>
+                       <es:set-value target="FullerName/@valid" select="true()"/>
+                    </es:element>
+                </es:element>
+              </es:ensure-target>
+            </es:source>
+            <es:if-target xpath='PartName'>
+               <es:set-value target="NotSet" select="$value"/>
+            </es:if-target>
+            <es:if-target xpath='FullName'>
+               <es:set-value target="IsSet" select="$value"/>
+            </es:if-target>
+            <es:target xpath='FullName'>
+               <es:set-value target="DidntFail" select="$value"/>
+            </es:target>
+        </es:CustomRequestTransform>
+        )!!";
+
+        constexpr const char *config1 = R"!!(<config>
+          <Transform>
+            <Param name='testcase' value="new features"/>
+          </Transform>
+        </config>)!!";
+
+            constexpr const char * result = R"!!(<root>
+  <Person found="true">
+    <FullName>
+      <First>Joe</First>
+      <ID>GI101</ID>
+      <ID>GI102</ID>
+      <IsSet>abc</IsSet>
+      <DidntFail>abc</DidntFail>
+    </FullName>
+    <Friends>Jane,John,Jaap,Jessica</Friends>
+    <How>
+      <Did>
+        <We>
+          <Get>
+            <Here whoknows="abc">
+              <IDontKnow>abc</IDontKnow>
+              <CopyTokens>
+                <token>aaa</token>
+                <token>bbb</token>
+                <token>ccc</token>
+                <token>yyy</token>
+              </CopyTokens>
+              <CopyFriends>
+                <token>Jane</token>
+                <token>John</token>
+                <token>Jaap</token>
+                <token>Jessica</token>
+              </CopyFriends>
+              <CopyFields>
+                <CopyField>
+                  <First>Joe</First>
+                </CopyField>
+                <CopyField>
+                  <ID>GI101</ID>
+                </CopyField>
+                <CopyField>
+                  <ID>GI102</ID>
+                </CopyField>
+              </CopyFields>
+              <CopyFullName>
+                <FullerName valid="true">
+                  <First>Joe</First>
+                  <ID>GI101</ID>
+                  <ID>GI102</ID>
+                </FullerName>
+              </CopyFullName>
+            </Here>
+          </Get>
+        </We>
+      </Did>
+    </How>
+  </Person>
+</root>)!!";
+
+
+        try {
+
+            Owned<IEspContext> ctx = createEspContext(nullptr);
+            Owned<IEsdlScriptContext> scriptContext = createTestScriptContext(ctx, input, config1);
+            runTransform(scriptContext, script, ESDLScriptCtxSection_ESDLRequest, "FirstPass", "target element 1", 0);
+
+            StringBuffer output;
+            scriptContext->toXML(output, "FirstPass");
+            if (result && !areEquivalentTestXMLStrings(result, output.str()))
+            {
+                fputs(output.str(), stdout);
+                fflush(stdout);
+                throw MakeStringException(100, "Test failed(%s)", "target element");
+            }
+        }
+        catch (IException *E)
+        {
+            StringBuffer m;
+            fprintf(stdout, "\nTest(%s) Exception %d - %s\n", "target element", E->errorCode(), E->errorMessage(m).str());
+            E->Release();
+            CPPUNIT_ASSERT(false);
         }
     }
+
+    void testHTTPPostXml()
+    {
+        static constexpr const char * input = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+         <root>
+            <Person>
+               <FullName>
+                  <First>Joe</First>
+                  <ID>GI101</ID>
+                  <ID>GI102</ID>
+               </FullName>
+            </Person>
+          </root>
+        )!!";
+
+        static constexpr const char * script = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="Person">
+            <es:variable name='value' select="'abc'"/>
+            <es:http-post-xml url="'http://127.0.0.1:9876'" section="logging" name="roxiestuff">
+              <es:content>
+                <es:element name="Envelope">
+                  <es:namespace prefix="soap" uri="http://schemas.xmlsoap.org/soap/envelope/" current="true" />
+                  <es:element name="Body">
+                    <es:element name="roxieechopersoninfoRequest">
+                      <es:namespace uri="urn:hpccsystems:ecl:roxieechopersoninfo" current="true" />
+                      <es:element name="roxieechopersoninforequest">
+                        <es:element name="Row">
+                          <es:element name="Name">
+                            <es:set-value target="First" value="'aaa'"/>
+                            <es:set-value target="Last" value="'bbb'"/>
+                            <es:element name="Aliases">
+                              <es:set-value target="Alias" value="'ccc'"/>
+                              <es:set-value target="Alias" value="'ddd'"/>
+                              <es:add-value target="Alias" value="'eee'"/>
+                            </es:element>
+                          </es:element>
+                        </es:element>
+                      </es:element>
+                    </es:element>
+                  </es:element>
+                </es:element>
+              </es:content>
+            </es:http-post-xml>
+            <es:element name="HttpPostStuff">
+              <es:copy-of select="$roxiestuff"/>
+            </es:element>
+        </es:CustomRequestTransform>
+        )!!";
+
+        constexpr const char *config1 = R"!!(<config>
+          <Transform>
+            <Param name='testcase' value="new features"/>
+          </Transform>
+        </config>)!!";
+
+            constexpr const char * result = R"!!(<root>
+  <Person>
+    <FullName>
+      <First>Joe</First>
+      <ID>GI101</ID>
+      <ID>GI102</ID>
+    </FullName>
+    <HttpPostStuff>
+      <roxiestuff>
+        <request url="http://127.0.0.1:9876">
+          <content>
+            <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+              <soap:Body>
+                <roxieechopersoninfoRequest xmlns="urn:hpccsystems:ecl:roxieechopersoninfo">
+                  <roxieechopersoninforequest>
+                    <Row>
+                      <Name>
+                        <First>aaa</First>
+                        <Last>bbb</Last>
+                        <Aliases>
+                          <Alias>ccc</Alias>
+                          <Alias>ddd</Alias>
+                        </Aliases>
+                      </Name>
+                    </Row>
+                  </roxieechopersoninforequest>
+                </roxieechopersoninfoRequest>
+              </soap:Body>
+            </soap:Envelope>
+          </content>
+        </request>
+        <response status="200 OK" error-code="0" content-type="text/xml">
+          <content>
+            <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+              <soap:Body>
+                <roxieechopersoninfoResponse xmlns="urn:hpccsystems:ecl:roxieechopersoninfo" sequence="0">
+                  <Results>
+                    <Result>
+                      <Dataset xmlns="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse" name="RoxieEchoPersonInfoResponse">
+                        <Row>
+                          <Name>
+                            <First>aaa</First>
+                            <Last>bbb</Last>
+                            <Aliases>
+                              <Alias>ddd</Alias>
+                              <Alias>eee</Alias>
+                            </Aliases>
+                          </Name>
+                          <Addresses/>
+                        </Row>
+                      </Dataset>
+                    </Result>
+                  </Results>
+                </roxieechopersoninfoResponse>
+              </soap:Body>
+            </soap:Envelope>
+          </content>
+        </response>
+      </roxiestuff>
+    </HttpPostStuff>
+  </Person>
+</root>)!!";
+
+        try {
+
+            Owned<IEspContext> ctx = createEspContext(nullptr);
+            Owned<IEsdlScriptContext> scriptContext = createTestScriptContext(ctx, input, config1);
+            runTransform(scriptContext, script, ESDLScriptCtxSection_ESDLRequest, "MyResult", "http post xml", 0);
+
+            StringBuffer output;
+            scriptContext->toXML(output, "MyResult");
+            if (result && !areEquivalentTestXMLStrings(result, output.str()))
+            {
+                fputs(output.str(), stdout);
+                fflush(stdout);
+                throw MakeStringException(100, "Test failed(%s)", "http post xml");
+            }
+        }
+        catch (IException *E)
+        {
+            StringBuffer m;
+            fprintf(stdout, "\nTest(%s) Exception %d - %s\n", "http post xml", E->errorCode(), E->errorMessage(m).str());
+            E->Release();
+            CPPUNIT_ASSERT(false);
+        }
+    }
+
+    void testScriptMap()
+    {
+        constexpr const char * serverScripts = R"!!(<Transforms xmlns:es='urn:hpcc:esdl:script'>
+      <es:BackendRequest>
+        <es:set-value target="Row/Name/First" value="'modified-request-at-service'" />
+      </es:BackendRequest>
+      <es:BackendRequest>
+        <es:set-value target="BRSRV2" value="'s2'" />
+      </es:BackendRequest>
+      <es:BackendRequest>
+        <es:set-value target="BRSRV3" value="'s3'" />
+      </es:BackendRequest>
+      <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+        <es:target xpath="soap:Body">
+          <es:target xpath="resp:roxieechopersoninfoResponse">
+            <es:target xpath="resp:Results/resp:Result">
+              <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+                <es:set-value target="ds1:Row/ds1:Name/ds1:Last" value="'modified-response-at-service'" />
+              </es:target>
+            </es:target>
+          </es:target>
+        </es:target>
+      </es:BackendResponse>
+      <es:BackendResponse>
+        <es:set-value target="BRESPSRV2" value="'s22'" />
+      </es:BackendResponse>
+      <es:BackendResponse>
+        <es:set-value target="BRESPSRV3" value="'s33'" />
+      </es:BackendResponse>
+      <es:PreLogging>
+        <es:set-value target="PLSRV1" value="'s111'" />
+      </es:PreLogging>
+      <es:PreLogging>
+        <es:set-value target="PLSRV2" value="'s222'" />
+      </es:PreLogging>
+      <es:PreLogging>
+        <es:set-value target="PLSRV3" value="'s333'" />
+      </es:PreLogging>
+  </Transforms>)!!";
+
+        constexpr const char * methodScripts = R"!!(<Transforms xmlns:es='urn:hpcc:esdl:script'>
+  <es:BackendRequest>
+    <es:append-to-value target="Row/Name/First" value="'-and-method'" />
+  </es:BackendRequest>
+  <es:BackendRequest>
+    <es:set-value target="BRMTH2" value="'m2'" />
+  </es:BackendRequest>
+  <es:BackendRequest>
+    <es:set-value target="BRMTH3" value="'m3'" />
+  </es:BackendRequest>
+  <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+    <es:target xpath="soap:Body">
+      <es:target xpath="resp:roxieechopersoninfoResponse">
+        <es:target xpath="resp:Results/resp:Result">
+          <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+            <es:append-to-value target="ds1:Row/ds1:Name/ds1:Last" value="'-and-method'" />
+          </es:target>
+        </es:target>
+      </es:target>
+    </es:target>
+  </es:BackendResponse>
+  <es:BackendResponse xmlns:resp="urn:hpccsystems:ecl:roxieechopersoninfo" xmlns:ds1="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse">
+    <es:http-post-xml url="'http://127.0.0.1:9876'" section="logdata/LogDataset" name="roxie_call_success">
+      <es:content>
+        <es:element name="Envelope">
+          <es:namespace prefix="soap" uri="http://schemas.xmlsoap.org/soap/envelope/" current="true" />
+          <es:element name="Body">
+            <es:element name="roxieechopersoninfoRequest">
+              <es:namespace uri="urn:hpccsystems:ecl:roxieechopersoninfo" current="true" />
+              <es:element name="roxieechopersoninforequest">
+                <es:element name="Row">
+                  <es:element name="Name">
+                    <es:set-value target="First" value="'echoFirst'"/>
+                    <es:set-value target="Last" value="'echoLast'"/>
+                    <es:element name="Aliases">
+                      <es:set-value target="Alias" value="'echoA1'"/>
+                      <es:add-value target="Alias" value="'echoA2'"/>
+                    </es:element>
+                  </es:element>
+                </es:element>
+              </es:element>
+            </es:element>
+          </es:element>
+        </es:element>
+      </es:content>
+    </es:http-post-xml>
+    <es:target xpath="soap:Body">
+    <es:target xpath="resp:roxieechopersoninfoResponse">
+    <es:target xpath="resp:Results/resp:Result">
+    <es:target xpath="ds1:Dataset[@name='RoxieEchoPersonInfoResponse']">
+        <es:source xpath="$roxie_call_success/response/content">
+          <es:source xpath="soap:Envelope/soap:Body">
+            <es:source xpath="resp:roxieechopersoninfoResponse/resp:Results/resp:Result">
+              <es:source xpath="ds1:Dataset/ds1:Row">
+                <es:append-to-value target="ds1:Row/ds1:Name/ds1:Last" value="concat('-plus-echoed-alias-', ds1:Name/ds1:Aliases/ds1:Alias[2])" />
+              </es:source>
+            </es:source>
+          </es:source>
+        </es:source>
+    </es:target>
+    </es:target>
+    </es:target>
+    </es:target>
+  </es:BackendResponse>
+  <es:BackendResponse>
+    <es:set-value target="BRESPMTH3" value="'m33'" />
+  </es:BackendResponse>
+  <es:PreLogging>
+    <es:http-post-xml url="'http://127.0.0.1:9876'" section="logdata/LogDatasets" name="roxie_call_exception">
+      <es:content>
+        <es:element name="Envelope">
+          <es:namespace prefix="soap" uri="http://schemas.xmlsoap.org/soap/envelope/" current="true" />
+          <es:element name="Body">
+            <es:element name="nonexistent_query">
+              <es:namespace uri="urn:hpccsystems:ecl:roxieechopersoninfo" current="true" />
+              <es:element name="nonexistent_queryrequest">
+                <es:element name="Row">
+                  <es:element name="Name">
+                    <es:set-value target="First" value="'aaa'"/>
+                    <es:set-value target="Last" value="'bbb'"/>
+                    <es:element name="Aliases">
+                      <es:set-value target="Alias" value="'ccc'"/>
+                      <es:set-value target="Alias" value="'ddd'"/>
+                    </es:element>
+                  </es:element>
+                </es:element>
+              </es:element>
+            </es:element>
+          </es:element>
+        </es:element>
+      </es:content>
+    </es:http-post-xml>
+  </es:PreLogging>
+  <es:PreLogging>
+    <es:set-value target="PLMTH2" value="'m222'" />
+  </es:PreLogging>
+  <es:PreLogging>
+    <es:set-value target="PLMTH3" value="'m333'" />
+  </es:PreLogging>
+  </Transforms>)!!";
+
+        constexpr const char * input = R"!!(<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <roxieechopersoninfoResponse xmlns="urn:hpccsystems:ecl:roxieechopersoninfo" sequence="0">
+   <Results>
+    <Result>
+     <Dataset xmlns="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse" name="RoxieEchoPersonInfoResponse">
+      <Row>
+       <Name>
+        <First>aaa</First>
+        <Last>bbbb</Last>
+        <Aliases>
+         <Alias>a</Alias>
+         <Alias>b</Alias>
+         <Alias>c</Alias>
+        </Aliases>
+       </Name>
+       <Addresses>
+        <Address>
+         <Line1>111</Line1>
+         <Line2>222</Line2>
+         <City>Boca Raton</City>
+         <State>FL</State>
+         <Zip>33487</Zip>
+         <type>ttt</type>
+        </Address>
+       </Addresses>
+      </Row>
+     </Dataset>
+    </Result>
+   </Results>
+  </roxieechopersoninfoResponse>
+ </soap:Body>
+</soap:Envelope>)!!";
+
+        constexpr const char *config1 = R"!!(<config>
+          <Transform>
+            <Param name='testcase' value="transform map"/>
+          </Transform>
+        </config>)!!";
+
+        constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+  <soap:Body>
+    <roxieechopersoninfoResponse xmlns="urn:hpccsystems:ecl:roxieechopersoninfo" sequence="0">
+      <Results>
+        <Result>
+          <Dataset xmlns="urn:hpccsystems:ecl:roxieechopersoninfo:result:roxieechopersoninforesponse" name="RoxieEchoPersonInfoResponse">
+            <Row>
+              <Name>
+                <First>aaa</First>
+                <Last>modified-response-at-service-and-method-plus-echoed-alias-echoA2</Last>
+                <Aliases>
+                  <Alias>a</Alias>
+                  <Alias>b</Alias>
+                  <Alias>c</Alias>
+                </Aliases>
+              </Name>
+              <Addresses>
+                <Address>
+                  <Line1>111</Line1>
+                  <Line2>222</Line2>
+                  <City>Boca Raton</City>
+                  <State>FL</State>
+                  <Zip>33487</Zip>
+                  <type>ttt</type>
+                </Address>
+              </Addresses>
+            </Row>
+          </Dataset>
+        </Result>
+      </Results>
+    </roxieechopersoninfoResponse>
+  </soap:Body>
+  <BRESPSRV2>s22</BRESPSRV2>
+  <BRESPSRV3>s33</BRESPSRV3>
+  <BRESPMTH3>m33</BRESPMTH3>
+</soap:Envelope>)!!";
+
+      Owned<IEspContext> ctx = createEspContext(nullptr);
+      Owned<IEsdlScriptContext> scriptContext = createTestScriptContext(ctx, input, config1);
+
+      bool legacy = false;
+      Owned<IEsdlTransformMethodMap> map = createEsdlTransformMethodMap();
+      map->addMethodTransforms("", serverScripts, legacy);
+      map->addMethodTransforms("mymethod", methodScripts, legacy);
+
+      IEsdlTransformSet *serviceSet = map->queryMethodEntryPoint("", "BackendResponse");
+      IEsdlTransformSet *methodSet = map->queryMethodEntryPoint("mymethod", "BackendResponse");
+
+      scriptContext->setContent(ESDLScriptCtxSection_InitialResponse, input);
+
+      processServiceAndMethodTransforms(scriptContext, {serviceSet, methodSet}, ESDLScriptCtxSection_InitialResponse, "MyResult");
+      StringBuffer output;
+      scriptContext->toXML(output, "MyResult");
+      if (result && !areEquivalentTestXMLStrings(result, output.str()))
+      {
+          fputs(output.str(), stdout);
+          fflush(stdout);
+          throw MakeStringException(100, "Test failed(%s)", "transform map");
+      }
+    }
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION( ESDLTests );