Pārlūkot izejas kodu

HPCC-13420 Restful Roxie: support calling queries via HTTP-GET

Signed-off-by: Anthony Fishbeck <anthony.fishbeck@lexisnexis.com>
Anthony Fishbeck 10 gadi atpakaļ
vecāks
revīzija
c012b34ab1

+ 130 - 8
common/thorhelper/roxiehelper.cpp

@@ -553,6 +553,7 @@ bool CSafeSocket::readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHt
         if (pHttpHelper != NULL && strncmp((char *)&len, "POST", 4) == 0)
         {
 #define MAX_HTTP_HEADERSIZE 8000
+            pHttpHelper->setIsHttp(true);
             char header[MAX_HTTP_HEADERSIZE + 1]; // allow room for \0
             sock->read(header, 1, MAX_HTTP_HEADERSIZE, bytesRead, timeout);
             header[bytesRead] = 0;
@@ -589,10 +590,52 @@ bool CSafeSocket::readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHt
             else
                 left = len = 0;
 
-            pHttpHelper->setIsHttp(true);
             if (!len)
                 throw MakeStringException(THORHELPER_DATA_ERROR, "Badly formed HTTP header");
         }
+        else if (pHttpHelper != NULL && strncmp((char *)&len, "GET", 3) == 0)
+        {
+#define MAX_HTTP_GET_HEADERSIZE 16000
+            pHttpHelper->setIsHttp(true);
+            char header[MAX_HTTP_GET_HEADERSIZE + 1]; // allow room for \0
+            sock->read(header, 1, MAX_HTTP_GET_HEADERSIZE, bytesRead, timeout);
+            header[bytesRead] = 0;
+            char *payload = strstr(header, "\r\n\r\n");
+            if (payload)
+            {
+                *payload = 0;
+                payload += 4;
+                char *str;
+
+                pHttpHelper->parseHTTPRequestLine(header);
+
+                // capture authentication token
+                if ((str = strstr(header, "Authorization: Basic ")) != NULL)
+                    pHttpHelper->setAuthToken(str+21);
+
+                pHttpHelper->setIsHttp(true);
+
+                StringBuffer queryName;
+                const char *target = pHttpHelper->queryTarget();
+                if (!target || !*target)
+                    throw MakeStringException(THORHELPER_DATA_ERROR, "HTTP-GET Target not specified");
+                else if (!pHttpHelper->validateTarget(target))
+                    throw MakeStringException(THORHELPER_DATA_ERROR, "HTTP-GET Target not found");
+                const char *query = pHttpHelper->queryQueryName();
+                if (!query || !*query)
+                    throw MakeStringException(THORHELPER_DATA_ERROR, "HTTP-GET Query not specified");
+
+                queryName.append(query);
+                Owned<IPropertyTree> req = createPTreeFromHttpParameters(queryName, pHttpHelper->queryUrlParameters(), true, pHttpHelper->queryContentFormat()==MarkupFmt_JSON);
+                if (pHttpHelper->queryContentFormat()==MarkupFmt_JSON)
+                    toJSON(req, ret);
+                else
+                    toXML(req, ret);
+                return true;
+            }
+            else
+                throw MakeStringException(THORHELPER_DATA_ERROR, "Badly formed HTTP GET");
+        }
         else if (strnicmp((char *)&len, "STAT", 4) == 0)
             isStatus = true;
         else
@@ -618,6 +661,13 @@ bool CSafeSocket::readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHt
 
         return len != 0;
     }
+    catch (IException *E)
+    {
+        if (pHttpHelper)
+            checkSendHttpException(*pHttpHelper, E, NULL);
+        heartbeat = false;
+        throw;
+    }
     catch (...)
     {
         heartbeat = false;
@@ -625,11 +675,11 @@ bool CSafeSocket::readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHt
     }
 }
 
-void CSafeSocket::setHttpMode(const char *queryName, bool arrayMode, TextMarkupFormat _mlfmt)
+void CSafeSocket::setHttpMode(const char *queryName, bool arrayMode, HttpHelper &httphelper)
 {
     CriticalBlock c(crit); // Should not be needed
     httpMode = true;
-    mlFmt = _mlfmt;
+    mlFmt = httphelper.queryContentFormat();
     heartbeat = false;
     assertex(contentHead.length()==0 && contentTail.length()==0);
     if (mlFmt==MarkupFmt_JSON)
@@ -640,19 +690,91 @@ void CSafeSocket::setHttpMode(const char *queryName, bool arrayMode, TextMarkupF
     else
     {
         StringAttrBuilder headText(contentHead), tailText(contentTail);
-        headText.append(
-            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
-            "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
-            "<soap:Body>");
+        if (httphelper.getUseEnvelope())
+            headText.append(
+                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+                "<soap:Body>");
         if (arrayMode)
         {
             headText.append("<").append(queryName).append("ResponseArray>");
             tailText.append("</").append(queryName).append("ResponseArray>");
         }
-        tailText.append("</soap:Body></soap:Envelope>");
+        if (httphelper.getUseEnvelope())
+            tailText.append("</soap:Body></soap:Envelope>");
     }
 }
 
+void CSafeSocket::checkSendHttpException(HttpHelper &httphelper, IException *E, const char *queryName)
+{
+    if (!httphelper.isHttp())
+        return;
+    if (httphelper.queryContentFormat()==MarkupFmt_JSON)
+        sendJsonException(E, queryName);
+    else
+        sendSoapException(E, queryName);
+}
+
+void CSafeSocket::sendSoapException(IException *E, const char *queryName)
+{
+    try
+    {
+        if (!queryName)
+            queryName = "Unknown"; // Exceptions when parsing query XML can leave queryName unset/unknowable....
+
+        StringBuffer response;
+        response.append("<").append(queryName).append("Response");
+        response.append(" xmlns=\"urn:hpccsystems:ecl:").appendLower(strlen(queryName), queryName).append("\">");
+        response.appendf("<Results><Result><Exception><Source>Roxie</Source><Code>%d</Code>", E->errorCode());
+        response.append("<Message>");
+        StringBuffer s;
+        E->errorMessage(s);
+        encodeXML(s.str(), response);
+        response.append("</Message></Exception></Result></Results>");
+        response.append("</").append(queryName).append("Response>");
+        write(response.str(), response.length());
+    }
+    catch(IException *EE)
+    {
+        StringBuffer error("While reporting exception: ");
+        EE->errorMessage(error);
+        DBGLOG("%s", error.str());
+        EE->Release();
+    }
+#ifndef _DEBUG
+    catch(...) {}
+#endif
+}
+
+void CSafeSocket::sendJsonException(IException *E, const char *queryName)
+{
+    try
+    {
+        if (!queryName)
+            queryName = "Unknown"; // Exceptions when parsing query XML can leave queryName unset/unknowable....
+
+        StringBuffer response;
+        appendfJSONName(response, "%sResponse", queryName).append(" {");
+        appendJSONName(response, "Results").append(" {");
+        appendJSONName(response, "Exception").append(" [{");
+        appendJSONValue(response, "Source", "Roxie");
+        appendJSONValue(response, "Code", E->errorCode());
+        StringBuffer s;
+        appendJSONValue(response, "Message", E->errorMessage(s).str());
+        response.append("}]}}");
+        write(response.str(), response.length());
+    }
+    catch(IException *EE)
+    {
+        StringBuffer error("While reporting exception: ");
+        DBGLOG("%s", EE->errorMessage(error).str());
+        EE->Release();
+    }
+#ifndef _DEBUG
+    catch(...) {}
+#endif
+}
+
 void CSafeSocket::setHeartBeat()
 {
     CriticalBlock c(crit);

+ 29 - 4
common/thorhelper/roxiehelper.hpp

@@ -31,10 +31,12 @@ class THORHELPER_API HttpHelper : public CInterface
 {
 private:
     bool _isHttp;
+    bool useEnvelope;
     StringAttr url;
     StringAttr authToken;
     StringAttr contentType;
     StringArray pathNodes;
+    StringArray *validTargets;
     Owned<IProperties> parameters;
 private:
     inline void setHttpHeaderValue(StringAttr &s, const char *v, bool ignoreExt)
@@ -51,12 +53,16 @@ private:
 
 public:
     IMPLEMENT_IINTERFACE;
-    HttpHelper() { _isHttp = false; parameters.setown(createProperties(true));}
+    HttpHelper(StringArray *_validTargets) : validTargets(_validTargets) { _isHttp = false; useEnvelope=true; parameters.setown(createProperties(true));}
     bool isHttp() { return _isHttp; }
+    bool getUseEnvelope(){return useEnvelope;}
+    void setUseEnvelope(bool _useEnvelope){useEnvelope=_useEnvelope;}
     bool getTrim() {return parameters->getPropBool(".trim", true); /*http currently defaults to true, maintain compatibility */}
     void setIsHttp(bool __isHttp) { _isHttp = __isHttp; }
     const char *queryAuthToken() { return authToken.sget(); }
     const char *queryTarget() { return (pathNodes.length()) ? pathNodes.item(0) : NULL; }
+    const char *queryQueryName() { return (pathNodes.length()>1) ? pathNodes.item(1) : NULL; }
+
     inline void setAuthToken(const char *v)
     {
         setHttpHeaderValue(authToken, v, false);
@@ -75,8 +81,20 @@ public:
             parseURL();
         }
     }
-    TextMarkupFormat queryContentFormat(){return (strieq(queryContentType(), "application/json")) ? MarkupFmt_JSON : MarkupFmt_XML;}
+    TextMarkupFormat queryContentFormat()
+    {
+        if (!contentType.length())
+        {
+            if (pathNodes.length()>2 && strieq(pathNodes.item(2), "json"))
+                contentType.set("application/json");
+            else
+                contentType.set("text/xml");
+        }
+
+        return (strieq(queryContentType(), "application/json")) ? MarkupFmt_JSON : MarkupFmt_XML;
+    }
     IProperties *queryUrlParameters(){return parameters;}
+    bool validateTarget(const char *target){return (validTargets) ? validTargets->contains(target) : false;}
 };
 
 //========================================================================================= 
@@ -87,7 +105,10 @@ interface SafeSocket : extends IInterface
     virtual size32_t write(const void *buf, size32_t size, bool takeOwnership=false) = 0;
     virtual bool readBlock(MemoryBuffer &ret, unsigned maxBlockSize, unsigned timeout = (unsigned) WAIT_FOREVER) = 0;
     virtual bool readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHttpHelper, bool &, bool &, unsigned maxBlockSize) = 0;
-    virtual void setHttpMode(const char *queryName, bool arrayMode, TextMarkupFormat txtfmt) = 0;
+    virtual void checkSendHttpException(HttpHelper &httphelper, IException *E, const char *queryName) = 0;
+    virtual void sendSoapException(IException *E, const char *queryName) = 0;
+    virtual void sendJsonException(IException *E, const char *queryName) = 0;
+    virtual void setHttpMode(const char *queryName, bool arrayMode, HttpHelper &httphelper) = 0;
     virtual void setHeartBeat() = 0;
     virtual bool sendHeartBeat(const IContextLogger &logctx) = 0;
     virtual void flush() = 0;
@@ -124,7 +145,11 @@ public:
     size32_t write(const void *buf, size32_t size, bool takeOwnership=false);
     bool readBlock(MemoryBuffer &ret, unsigned maxBlockSize, unsigned timeout = (unsigned) WAIT_FOREVER);
     bool readBlock(StringBuffer &ret, unsigned timeout, HttpHelper *pHttpHelper, bool &, bool &, unsigned maxBlockSize);
-    void setHttpMode(const char *queryName, bool arrayMode, TextMarkupFormat txtfmt);
+    void setHttpMode(const char *queryName, bool arrayMode, HttpHelper &httphelper);
+    void checkSendHttpException(HttpHelper &httphelper, IException *E, const char *queryName);
+    void sendSoapException(IException *E, const char *queryName);
+    void sendJsonException(IException *E, const char *queryName);
+
     void setHeartBeat();
     bool sendHeartBeat(const IContextLogger &logctx);
     void flush();

+ 1 - 1
ecl/eclagent/eclagent.cpp

@@ -335,7 +335,7 @@ public:
 
         Owned<IDebuggerContext> debuggerContext;
         unsigned slavesReplyLen = 0;
-        HttpHelper httpHelper;
+        HttpHelper httpHelper(NULL);
         try
         {
             client->querySocket()->getPeerAddress(peer);

+ 0 - 91
esp/services/common/jsonhelpers.hpp

@@ -31,97 +31,6 @@
 #define REQSF_ESCAPEFORMATTERS 0x0008
 #define REQSF_EXCLUSIVE (REQSF_SAMPLE_DATA | REQSF_TRIM)
 
-namespace HttpParamHelpers
-{
-    static const char * nextParameterTag(StringBuffer &tag, const char *path)
-    {
-        while (*path=='.')
-            path++;
-        const char *finger = strchr(path, '.');
-        if (finger)
-        {
-            tag.clear().append(finger - path, path);
-            finger++;
-        }
-        else
-            tag.set(path);
-        return finger;
-    }
-
-    static void ensureParameter(IPropertyTree *pt, StringBuffer &tag, const char *path, const char *value, const char *fullpath)
-    {
-        if (!tag.length())
-            return;
-
-        unsigned idx = 1;
-        if (path && isdigit(*path))
-        {
-            StringBuffer pos;
-            path = nextParameterTag(pos, path);
-            idx = (unsigned) atoi(pos.str())+1;
-            if (idx>25) //adf
-                throw MakeStringException(-1, "Array items above 25 not supported in HPCC WS HTTP parameters: %s", fullpath);
-        }
-
-        if (tag.charAt(tag.length()-1)=='$')
-        {
-            if (path && *path)
-                throw MakeStringException(-1, "'$' not allowed in parent node of parameter path: %s", fullpath);
-            tag.setLength(tag.length()-1);
-            StringArray values;
-            values.appendList(value, "\r");
-            ForEachItemIn(pos, values)
-            {
-                const char *itemValue = values.item(pos);
-                while (*itemValue=='\n')
-                    itemValue++;
-                pt->addProp(tag, itemValue);
-            }
-            return;
-        }
-        unsigned count = pt->getCount(tag);
-        while (count++ < idx)
-            pt->addPropTree(tag, createPTree(tag));
-        StringBuffer xpath(tag);
-        xpath.append('[').append(idx).append(']');
-        pt = pt->queryPropTree(xpath);
-
-        if (!path || !*path)
-        {
-            pt->setProp(NULL, value);
-            return;
-        }
-
-        StringBuffer nextTag;
-        path = HttpParamHelpers::nextParameterTag(nextTag, path);
-        ensureParameter(pt, nextTag, path, value, fullpath);
-    }
-
-    static void ensureParameter(IPropertyTree *pt, const char *path, const char *value)
-    {
-        const char *fullpath = path;
-        StringBuffer tag;
-        path = HttpParamHelpers::nextParameterTag(tag, path);
-        ensureParameter(pt, tag, path, value, fullpath);
-    }
-
-    static IPropertyTree *createPTreeFromHttpParameters(const char *name, IProperties *parameters)
-    {
-        Owned<IPropertyTree> pt = createPTree(name);
-        Owned<IPropertyIterator> props = parameters->getIterator();
-        ForEach(*props)
-        {
-            StringBuffer key = props->getPropKey();
-            if (!key.length() || key.charAt(key.length()-1)=='!')
-                continue;
-            const char *value = parameters->queryProp(key);
-            if (value && *value)
-                ensureParameter(pt, key, value);
-        }
-        return pt.getClear();
-    }
-};
-
 namespace JsonHelpers
 {
     static StringBuffer &appendJSONExceptionItem(StringBuffer &s, int code, const char *msg, const char *objname="Exceptions", const char *arrayName = "Exception")

+ 3 - 3
esp/services/ws_ecl/ws_ecl_service.cpp

@@ -1478,7 +1478,7 @@ void CWsEclBinding::getWsEcl2XmlRequest(StringBuffer& soapmsg, IEspContext &cont
         return;
     }
 
-    Owned<IPropertyTree> reqTree = HttpParamHelpers::createPTreeFromHttpParameters(wsinfo.queryname, parameters);
+    Owned<IPropertyTree> reqTree = createPTreeFromHttpParameters(wsinfo.queryname, parameters, true, false);
 
     if (!validate)
         toXML(reqTree, soapmsg, 0, 0);
@@ -1510,7 +1510,7 @@ void CWsEclBinding::getWsEclJsonRequest(StringBuffer& jsonmsg, IEspContext &cont
     try
     {
         IProperties *parameters = context.queryRequestParameters();
-        Owned<IPropertyTree> reqTree = HttpParamHelpers::createPTreeFromHttpParameters(wsinfo.queryname, parameters);
+        Owned<IPropertyTree> reqTree = createPTreeFromHttpParameters(wsinfo.queryname, parameters, true, false);
 
         if (!validate)
         {
@@ -2362,7 +2362,7 @@ int CWsEclBinding::onGet(CHttpRequest* request, CHttpResponse* response)
 
             if (!wsecl->connMap.getValue(target.str()))
                 throw MakeStringException(-1, "Target cluster not mapped to roxie process!");
-            Owned<IPropertyTree> pt = HttpParamHelpers::createPTreeFromHttpParameters(qid.str(), parms);
+            Owned<IPropertyTree> pt = createPTreeFromHttpParameters(qid.str(), parms, true, false);
             StringBuffer soapreq(
                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                 "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""

+ 12 - 79
roxie/ccd/ccdlistener.cpp

@@ -58,76 +58,6 @@ static void controlException(StringBuffer &response, IException *E, const IRoxie
 
 //================================================================================================================
 
-static void sendSoapException(SafeSocket &client, IException *E, const char *queryName)
-{
-    try
-    {
-        if (!queryName)
-            queryName = "Unknown"; // Exceptions when parsing query XML can leave queryName unset/unknowable....
-
-        StringBuffer response;
-        response.append("<").append(queryName).append("Response");
-        response.append(" xmlns=\"urn:hpccsystems:ecl:").appendLower(strlen(queryName), queryName).append("\">");
-        response.appendf("<Results><Result><Exception><Source>Roxie</Source><Code>%d</Code>", E->errorCode());
-        response.append("<Message>");
-        StringBuffer s;
-        E->errorMessage(s);
-        encodeXML(s.str(), response);
-        response.append("</Message></Exception></Result></Results>");
-        response.append("</").append(queryName).append("Response>");
-        client.write(response.str(), response.length());
-    }
-    catch(IException *EE)
-    {
-        StringBuffer error("While reporting exception: ");
-        EE->errorMessage(error);
-        DBGLOG("%s", error.str());
-        EE->Release();
-    }
-#ifndef _DEBUG
-    catch(...) {}
-#endif
-}
-
-static void sendJsonException(SafeSocket &client, IException *E, const char *queryName)
-{
-    try
-    {
-        if (!queryName)
-            queryName = "Unknown"; // Exceptions when parsing query XML can leave queryName unset/unknowable....
-
-        StringBuffer response;
-        appendfJSONName(response, "%sResponse", queryName).append(" {");
-        appendJSONName(response, "Results").append(" {");
-        appendJSONName(response, "Exception").append(" [{");
-        appendJSONValue(response, "Source", "Roxie");
-        appendJSONValue(response, "Code", E->errorCode());
-        StringBuffer s;
-        appendJSONValue(response, "Message", E->errorMessage(s).str());
-        response.append("}]}}");
-        client.write(response.str(), response.length());
-    }
-    catch(IException *EE)
-    {
-        StringBuffer error("While reporting exception: ");
-        DBGLOG("%s", EE->errorMessage(error).str());
-        EE->Release();
-    }
-#ifndef _DEBUG
-    catch(...) {}
-#endif
-}
-
-static void sendHttpException(SafeSocket &client, TextMarkupFormat fmt, IException *E, const char *queryName)
-{
-    if (fmt==MarkupFmt_JSON)
-        sendJsonException(client, E, queryName);
-    else
-        sendSoapException(client, E, queryName);
-}
-
-//================================================================================================================
-
 class CHttpRequestAsyncFor : public CInterface, public CAsyncFor
 {
 private:
@@ -161,7 +91,7 @@ public:
         StringBuffer error("EXCEPTION: ");
         E->errorMessage(error);
         DBGLOG("%s", error.str());
-        sendHttpException(client, httpHelper.queryContentFormat(), E, queryName);
+        client.checkSendHttpException(httpHelper, E, queryName);
         E->Release();
     }
 
@@ -1357,9 +1287,14 @@ private:
                     else
                         throw MakeStringException(ROXIE_DATA_ERROR, "Malformed JSON request");
                 }
-                else if (strieq(queryName, "envelope"))
+                else
                 {
-                    queryXML.setown(queryXML->getPropTree("Body/*"));
+                    if (strieq(queryName, "envelope"))
+                        queryXML.setown(queryXML->getPropTree("Body/*"));
+                    else if (!strnicmp(httpHelper.queryContentType(), "application/soap", strlen("application/soap")))
+                        throw MakeStringException(ROXIE_DATA_ERROR, "Malformed SOAP request");
+                    else
+                        httpHelper.setUseEnvelope(false);
                     if (!queryXML)
                         throw MakeStringException(ROXIE_DATA_ERROR, "Malformed SOAP request (missing Body)");
                     String reqName(queryXML->queryName());
@@ -1384,8 +1319,6 @@ private:
 
                     queryXML->renameProp("/", queryName.get());  // reset the name of the tree
                 }
-                else
-                    throw MakeStringException(ROXIE_DATA_ERROR, "Malformed SOAP request");
             }
 
             // convert to XML with attribute values in single quotes - makes replaying queries easier
@@ -1418,7 +1351,7 @@ private:
 readAnother:
         Owned<IDebuggerContext> debuggerContext;
         unsigned slavesReplyLen = 0;
-        HttpHelper httpHelper;
+        HttpHelper httpHelper(&allQuerySetNames);
         try
         {
             if (client)
@@ -1654,7 +1587,7 @@ readAnother:
                         StringBuffer querySetName;
                         if (isHTTP)
                         {
-                            client->setHttpMode(queryName, isRequestArray, httpHelper.queryContentFormat());
+                            client->setHttpMode(queryName, isRequestArray, httpHelper);
                             querySetName.set(httpHelper.queryTarget());
                         }
                         queryFactory.setown(globalPackageSetManager->getQuery(queryName, &querySetName, NULL, logctx));
@@ -1803,7 +1736,7 @@ readAnother:
             if (client)
             {
                 if (isHTTP)
-                    sendHttpException(*client, httpHelper.queryContentFormat(), E, queryName);
+                    client->checkSendHttpException(httpHelper, E, queryName);
                 else
                     client->sendException("Roxie", code, error.str(), isBlocked, logctx);
             }
@@ -1828,7 +1761,7 @@ readAnother:
             if (client)
             {
                 if (isHTTP)
-                    sendHttpException(*client, httpHelper.queryContentFormat(), E, queryName);
+                    client->checkSendHttpException(httpHelper, E, queryName);
                 else
                     client->sendException("Roxie", code, error.str(), isBlocked, logctx);
             }

+ 94 - 0
system/jlib/jptree.cpp

@@ -7126,3 +7126,97 @@ IPropertyTree *createPTreeFromJSONString(unsigned len, const char *json, byte fl
     reader->load();
     return LINK(iMaker->queryRoot());
 }
+
+
+static const char * nextHttpParameterTag(StringBuffer &tag, const char *path)
+{
+    while (*path=='.')
+        path++;
+    const char *finger = strchr(path, '.');
+    if (finger)
+    {
+        tag.clear().append(finger - path, path);
+        finger++;
+    }
+    else
+        tag.set(path);
+    return finger;
+}
+
+static void ensureHttpParameter(IPropertyTree *pt, StringBuffer &tag, const char *path, const char *value, const char *fullpath)
+{
+    if (!tag.length())
+        return;
+
+    unsigned idx = 1;
+    if (path && isdigit(*path))
+    {
+        StringBuffer pos;
+        path = nextHttpParameterTag(pos, path);
+        idx = (unsigned) atoi(pos.str())+1;
+    }
+
+    if (tag.charAt(tag.length()-1)=='$')
+    {
+        if (path && *path)
+            throw MakeStringException(-1, "'$' not allowed in parent node of parameter path: %s", fullpath);
+        tag.setLength(tag.length()-1);
+        StringArray values;
+        values.appendList(value, "\r");
+        ForEachItemIn(pos, values)
+        {
+            const char *itemValue = values.item(pos);
+            while (*itemValue=='\n')
+                itemValue++;
+            pt->addProp(tag, itemValue);
+        }
+        return;
+    }
+    unsigned count = pt->getCount(tag);
+    while (count++ < idx)
+        pt->addPropTree(tag, createPTree(tag));
+    StringBuffer xpath(tag);
+    xpath.append('[').append(idx).append(']');
+    pt = pt->queryPropTree(xpath);
+
+    if (!path || !*path)
+    {
+        pt->setProp(NULL, value);
+        return;
+    }
+
+    StringBuffer nextTag;
+    path = nextHttpParameterTag(nextTag, path);
+    ensureHttpParameter(pt, nextTag, path, value, fullpath);
+}
+
+static void ensureHttpParameter(IPropertyTree *pt, const char *path, const char *value)
+{
+    const char *fullpath = path;
+    StringBuffer tag;
+    path = nextHttpParameterTag(tag, path);
+    ensureHttpParameter(pt, tag, path, value, fullpath);
+}
+
+IPropertyTree *createPTreeFromHttpParameters(const char *name, IProperties *parameters, bool skipLeadingDotParameters, bool nestedRoot, ipt_flags flags)
+{
+    Owned<IPropertyTree> pt = createPTree(name, flags);
+
+    Owned<IPropertyIterator> props = parameters->getIterator();
+    ForEach(*props)
+    {
+        const char *key = props->getPropKey();
+        const char *value = parameters->queryProp(key);
+        if (skipLeadingDotParameters && key && *key=='.')
+            continue;
+        ensureHttpParameter(pt, key, value);
+    }
+    if (nestedRoot)
+    {
+        Owned<IPropertyTree> root = createPTree(flags);
+        root->setPropTree(name, pt.getClear());
+        return root.getClear();
+    }
+
+    return pt.getClear();
+}

+ 3 - 0
system/jlib/jptree.hpp

@@ -22,6 +22,7 @@
 #include "jlib.hpp"
 #include "jexcept.hpp"
 #include "jiter.hpp"
+#include "jprop.hpp"
 
 enum TextMarkupFormat
 {
@@ -206,6 +207,8 @@ jlib_decl IPropertyTree *createPTreeFromIPT(const IPropertyTree *srcTree, ipt_fl
 
 jlib_decl IPropertyTree *createPTreeFromJSONString(const char *json, byte flags=ipt_none, PTreeReaderOptions readFlags=ptr_ignoreWhiteSpace, IPTreeMaker *iMaker=NULL);
 jlib_decl IPropertyTree *createPTreeFromJSONString(unsigned len, const char *json, byte flags=ipt_none, PTreeReaderOptions readFlags=ptr_ignoreWhiteSpace, IPTreeMaker *iMaker=NULL);
+jlib_decl IPropertyTree *createPTreeFromHttpParameters(const char *name, IProperties *parameters, bool skipLeadingDotParameters, bool nestedRoot, ipt_flags flags=ipt_none);
+
 
 #define XML_SortTags 0x01
 #define XML_Embed    0x02