Browse Source

HPCC-16961 Automatically provide sample JSON responses for ESP services

Generated forms will now have a link to Sample JSON response messages.

Navigating to the service page, for example: http://IP:8010/WsWorkunits
now has links that will provide a collection of all the sample messages
for all the methods in the given service.

Signed-off-by: Anthony Fishbeck <anthony.fishbeck@lexisnexis.com>
Anthony Fishbeck 8 years ago
parent
commit
a483d15ddd

+ 1 - 0
esp/bindings/SOAP/soaplib/CMakeLists.txt

@@ -56,6 +56,7 @@ include_directories (
          ./../../../../system/jlib 
          ./../../../bindings 
          ./../../../platform 
+         ./../../../services/common
          ./../../../../system/xmllib 
          ./.. 
          ./../../../bindings/SOAP/xpp 

+ 142 - 34
esp/bindings/http/platform/httpbinding.cpp

@@ -46,6 +46,7 @@
 #include "xmlvalidator.hpp"
 #include "xsdparser.hpp"
 #include "espsecurecontext.hpp"
+#include "jsonhelpers.hpp"
 
 #define FILE_UPLOAD     "FileUploadAccess"
 
@@ -558,6 +559,7 @@ int EspHttpBinding::onGet(CHttpRequest* request, CHttpResponse* response)
         case sub_serv_soap_builder:
         case sub_serv_reqsamplexml:
         case sub_serv_respsamplexml:
+        case sub_serv_respsamplejson:
             context.setClientVersion(atof(m_defaultSvcVersion));
 
         default:
@@ -605,6 +607,8 @@ int EspHttpBinding::onGet(CHttpRequest* request, CHttpResponse* response)
             return onGetReqSampleXml(context, request, response, serviceName.str(), methodName.str());
         case sub_serv_respsamplexml:
             return onGetRespSampleXml(context, request, response, serviceName.str(), methodName.str());
+        case sub_serv_respsamplejson:
+            return onGetRespSampleJson(context, request, response, serviceName.str(), methodName.str());
         case sub_serv_query:
             return onGetQuery(context, request, response, serviceName.str(), methodName.str());
         case sub_serv_file_upload:
@@ -1336,40 +1340,135 @@ static void genSampleXml(StringStack& parent, IXmlType* type, StringBuffer& out,
     }
 }
 
-void EspHttpBinding::generateSampleXml(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method)
+void EspHttpBinding::generateSampleXml(bool isRequest, IEspContext &context, CHttpRequest* request, StringBuffer &content, const char *serv, const char *method)
 {
-    StringBuffer serviceQName, methodQName;
+    StringBuffer schemaXml, element;
+    getXMLMessageTag(context, isRequest, method, element);
+    getSchema(schemaXml,context,request,serv,method,false);
+    Owned<IXmlSchema> schema;
+    IXmlType* type = nullptr;
+    try
+    {
+        schema.setown(createXmlSchema(schemaXml));
+        if (!schema)
+        {
+            content.appendf("<Error>generateSampleXml schema error: %s::%s</Error>", serv, method);
+            return;
+        }
 
-    if (!qualifyServiceName(context, serv, method, serviceQName, &methodQName))
+        type = schema->queryElementType(element);
+        if (!type)
+        {
+            content.appendf("<Error>generateSampleXml unknown type: %s in %s::%s</Error>", element.str(), serv, method);
+            return;
+        }
+    }
+    catch (IException *E)
+    {
+        StringBuffer msg;
+        content.appendf("<Error>generateSampleXml Exception: %s in %s::%s</Error>", E->errorMessage(msg).str(), serv, method);
+        E->Release();
+        return;
+    }
+    StringStack parent;
+    StringBuffer nsdecl("xmlns=\"");
+    genSampleXml(parent,type, content, element, generateNamespace(context, request, serv, method, nsdecl).append('\"').str());
+}
+
+
+
+void EspHttpBinding::generateSampleXml(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
+{
+    StringBuffer serviceName;
+    StringBuffer methodName;
+    if (!qualifyServiceName(context, serv, method, serviceName, (method) ? &methodName : nullptr))
         return;
 
+    StringBuffer content;
+    if (method && *method)
+        generateSampleXml(isRequest, context, request, content, serviceName, methodName);
+    else
+    {
+        MethodInfoArray methods;
+        getQualifiedNames(context, methods);
+
+        content.appendf("<Examples><%s>\n", isRequest ? "Requests" : "Responses");
+        ForEachItemIn(i, methods)
+            generateSampleXml(isRequest, context, request, content, serviceName.str(), methods.item(i).m_label.str());
+        content.appendf("\n</%s></Examples>", isRequest ? "Requests" : "Responses");
+    }
+
+    response->setContent(content.length(), content.str());
+    response->setContentType(HTTP_TYPE_TEXT_XML_UTF8);
+    response->setStatus(HTTP_STATUS_OK);
+    response->send();
+    return;
+}
+
+void EspHttpBinding::generateSampleJson(bool isRequest, IEspContext &context, CHttpRequest* request, StringBuffer &content, const char *serv, const char *method)
+{
     StringBuffer schemaXml, element;
-    getXMLMessageTag(context, isRequest, methodQName.str(), element);
+    getXMLMessageTag(context, isRequest, method, element);
     getSchema(schemaXml,context,request,serv,method,false);
-    Owned<IXmlSchema> schema = createXmlSchema(schemaXml);
-    if (schema.get())
+
+    Owned<IXmlSchema> schema;
+    IXmlType* type = nullptr;
+    try
     {
-        IXmlType* type = schema->queryElementType(element);
-        if (type)
+        schema.setown(createXmlSchema(schemaXml));
+        if (!schema)
         {
-            StringBuffer content;
-            StringStack parent;
-            StringBuffer nsdecl("xmlns=\"");
-
-            content.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
-            if (context.queryRequestParameters()->hasProp("display"))
-                content.append("<?xml-stylesheet type=\"text/xsl\" href=\"/esp/xslt/xmlformatter.xsl\"?>");
+            content.appendf("{\"Error\": \"generateSampleJson schema error: %s::%s\"}", serv, method);
+            return;
+        }
 
-            genSampleXml(parent,type, content, element, generateNamespace(context, request, serviceQName.str(), methodQName.str(), nsdecl).append('\"').str());
-            response->setContent(content.length(), content.str());
-            response->setContentType(HTTP_TYPE_APPLICATION_XML_UTF8);
-            response->setStatus(HTTP_STATUS_OK);
-            response->send();
+        type = schema->queryElementType(element);
+        if (!type)
+        {
+            content.appendf("{\"Error\": \"generateSampleJson unknown type: %s in %s::%s\"}", element.str(), serv, method);
             return;
         }
     }
+    catch (IException *E)
+    {
+        StringBuffer msg;
+        content.appendf("{\"Error\": \"generateSampleJson unknown type: %s in %s::%s\"}", E->errorMessage(msg).str(), serv, method);
+        E->Release();
+        return;
+    }
 
-    throw MakeStringException(-1,"Unknown type: %s", element.str());
+    StringArray parentTypes;
+    JsonHelpers::buildJsonMsg(parentTypes, type, content, element, NULL, REQSF_ROOT|REQSF_SAMPLE_DATA|REQSF_FORMAT);
+}
+
+void EspHttpBinding::generateSampleJson(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
+{
+    StringBuffer serviceName;
+    StringBuffer methodName;
+    if (!qualifyServiceName(context, serv, method, serviceName, (method) ? &methodName : nullptr))
+        return;
+
+    StringBuffer content;
+    if (method && *method)
+        generateSampleJson(isRequest, context, request, content, serviceName, methodName);
+    else
+    {
+        MethodInfoArray methods;
+        getQualifiedNames(context, methods);
+        content.appendf("{\"Examples\": {\"%s\": [\n", isRequest ? "Request" : "Response");
+        ForEachItemIn(i, methods)
+        {
+            delimitJSON(content, true);
+            generateSampleJson(isRequest, context, request, content, serviceName.str(), methods.item(i).m_label.str());
+        }
+        content.append("]}}\n");
+    }
+
+    response->setContent(content.length(), content.str());
+    response->setContentType(HTTP_TYPE_TEXT_PLAIN_UTF8);
+    response->setStatus(HTTP_STATUS_OK);
+    response->send();
+    return;
 }
 
 void EspHttpBinding::generateSampleXmlFromSchema(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method, const char * schemaxml)
@@ -1420,6 +1519,12 @@ int EspHttpBinding::onGetRespSampleXml(IEspContext &ctx, CHttpRequest* request,
     return 0;
 }
 
+int EspHttpBinding::onGetRespSampleJson(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
+{
+    generateSampleJson(false, ctx, request, response, serv, method);
+    return 0;
+}
+
 int EspHttpBinding::onStartUpload(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
 {
     StringArray fileNames, files;
@@ -1673,21 +1778,24 @@ int EspHttpBinding::onGetIndex(IEspContext &context, CHttpRequest* request,  CHt
         MethodInfoArray methods;
         getQualifiedNames(context, methods);
 
-        if (supportGeneratedForms() || request->queryParameters()->hasProp("list_forms"))
-        {
-            page.appendContent(new CHtmlText("The following operations are supported:<br/>"));
+        page.appendContent(new CHtmlText("For complete sets of example messages:<br/>"));
+        list = (CHtmlList *)page.appendContent(new CHtmlList);
+        list->appendContent(new CHtmlLink("XML Requests", wsLink.set(urlParams).append("&reqxml_").str()));
+        list->appendContent(new CHtmlLink("XML Responses", wsLink.set(urlParams).append("&respxml_").str()));
+        list->appendContent(new CHtmlLink("JSON Responses", wsLink.set(urlParams).append("&respjson_").str()));
 
-            //links to the form pages
-            list = (CHtmlList *)page.appendContent(new CHtmlList);
-            StringBuffer urlFormParams(urlParams);
-            urlFormParams.append(urlFormParams.length()>0 ? "&form" : "?form");
-            for(int i = 0, tot = methods.length(); i < tot; ++i)
+        page.appendContent(new CHtmlText("The following operations are supported:<br/>"));
+
+        //links to the form pages
+        list = (CHtmlList *)page.appendContent(new CHtmlList);
+        StringBuffer urlFormParams(urlParams);
+        urlFormParams.append(urlFormParams.length()>0 ? "&form" : "?form");
+        for(int i = 0, tot = methods.length(); i < tot; ++i)
+        {
+            CMethodInfo &method = methods.item(i);
             {
-                CMethodInfo &info = methods.item(i);
-                {
-                    wsLink.clear().appendf("%s%s", methods.item(i).m_label.str(),urlFormParams.str());
-                    list->appendContent(new CHtmlLink(methods.item(i).m_label.str(), wsLink.str()));
-                }
+                wsLink.setf("%s%s", method.m_label.str(),urlFormParams.str());
+                list->appendContent(new CHtmlLink(method.m_label.str(), wsLink.str()));
             }
         }
         

+ 5 - 0
esp/bindings/http/platform/httpbinding.hpp

@@ -90,6 +90,7 @@ interface IEspHttpBinding
     virtual int onGetSoapBuilder(IEspContext &context, CHttpRequest* request, CHttpResponse* response,  const char *serv, const char *method)=0;
     virtual int onGetReqSampleXml(IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)=0;
     virtual int onGetRespSampleXml(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method)=0;
+    virtual int onGetRespSampleJson(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method)=0;
     virtual int onStartUpload(IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)=0;
     virtual int onFinishUpload(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method,
     StringArray& fileNames, StringArray& files, IMultiException *me)=0;
@@ -248,6 +249,7 @@ public:
 
     virtual int onGetReqSampleXml(IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);
     virtual int onGetRespSampleXml(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method);
+    virtual int onGetRespSampleJson(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method);
 
     virtual int onStartUpload(IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);
     virtual int onFinishUpload(IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method,
@@ -295,6 +297,9 @@ protected:
     bool getSchema(StringBuffer& schema, IEspContext &ctx, CHttpRequest* req, const char *service, const char *method,bool standalone);
     virtual void appendSchemaNamespaces(IPropertyTree *namespaces, IEspContext &ctx, CHttpRequest* req, const char *service, const char *method){}
     void generateSampleXml(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method);
+    void generateSampleXml(bool isRequest, IEspContext &context, CHttpRequest* request, StringBuffer &content, const char *serv, const char *method);
+    void generateSampleJson(bool isRequest, IEspContext &context, CHttpRequest* request, StringBuffer &content, const char *serv, const char *method);
+    void generateSampleJson(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response,    const char *serv, const char *method);
     void generateSampleXmlFromSchema(bool isRequest, IEspContext &context, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method, const char * schemaxml);
     virtual void getSoapMessage(StringBuffer& soapmsg, IEspContext &context, CHttpRequest* request, const char *serv, const char *method);
     void onBeforeSendResponse(IEspContext& context, CHttpRequest* request,MemoryBuffer& contentconst,

+ 1 - 0
esp/bindings/http/platform/httpservice.cpp

@@ -174,6 +174,7 @@ const char* getSubServiceDesc(sub_service stype)
     DEF_CASE(sub_serv_getversion)
     DEF_CASE(sub_serv_reqsamplexml)
     DEF_CASE(sub_serv_respsamplexml)
+    DEF_CASE(sub_serv_respsamplejson)
     DEF_CASE(sub_serv_file_upload)
 
     default: return "invalid-type";

+ 2 - 0
esp/bindings/http/platform/httptransport.cpp

@@ -1456,6 +1456,8 @@ void CHttpRequest::parseEspPathInfo()
                     m_sstype=sub_serv_reqsamplexml;
                 else if (m_queryparams && (m_queryparams->hasProp("respxml_")))
                     m_sstype=sub_serv_respsamplexml;
+                else if (m_queryparams && (m_queryparams->hasProp("respjson_")))
+                    m_sstype=sub_serv_respsamplejson;
                 else if (m_queryparams && (m_queryparams->hasProp("soap_builder_")))
                     m_sstype=sub_serv_soap_builder;
                 else if (m_queryparams && (m_queryparams->hasProp("roxie_builder_")))

+ 1 - 0
esp/bindings/http/platform/httptransport.ipp

@@ -278,6 +278,7 @@ typedef enum sub_service_
     sub_serv_getversion,
     sub_serv_reqsamplexml,
     sub_serv_respsamplexml,
+    sub_serv_respsamplejson,
     sub_serv_file_upload,
 
     sub_serv_max

+ 1 - 0
esp/protocols/http/CMakeLists.txt

@@ -60,6 +60,7 @@ include_directories (
          ./../../../system/xmllib 
          ./../../../system/jlib 
          ./../../platform 
+         ./../../services/common
          ./../../../system/security/shared 
          ./../../../system/security/LdapSecurity
     )

+ 77 - 19
esp/services/common/jsonhelpers.hpp

@@ -29,6 +29,7 @@
 #define REQSF_SAMPLE_DATA  0x0002
 #define REQSF_TRIM         0x0004
 #define REQSF_ESCAPEFORMATTERS 0x0008
+#define REQSF_FORMAT       0x0010
 #define REQSF_EXCLUSIVE (REQSF_SAMPLE_DATA | REQSF_TRIM)
 
 namespace JsonHelpers
@@ -229,12 +230,30 @@ namespace JsonHelpers
         else
             out.append("null");
     }
-    static void buildJsonMsg(StringArray& parentTypes, IXmlType* type, StringBuffer& out, const char* tag, IPropertyTree *reqTree, unsigned flags)
+    inline void checkNewlineJsonMsg(StringBuffer &out, unsigned &level, unsigned flags, int increment)
+    {
+        if (!(flags & REQSF_FORMAT))
+            return;
+        out.newline();
+        level+=increment;
+        if (level>0)
+            out.pad(level);
+    }
+    inline void checkDelimitJsonMsg(StringBuffer &out, unsigned level, unsigned flags)
+    {
+        delimitJSON(out, (flags & REQSF_FORMAT));
+        if (out.length() && out.charAt(out.length()-1)=='\n')
+            out.pad(level);
+    }
+    static void buildJsonMsg(unsigned &level, StringArray& parentTypes, IXmlType* type, StringBuffer& out, const char* tag, IPropertyTree *reqTree, unsigned flags)
     {
         assertex(type!=NULL);
 
         if (flags & REQSF_ROOT)
+        {
             out.append("{");
+            checkNewlineJsonMsg(out, level, flags, 2);
+        }
 
         const char* typeName = type->queryName();
         if (type->isComplexType())
@@ -246,6 +265,7 @@ namespace JsonHelpers
             if (tag)
                 appendJSONName(out, tag);
             out.append('{');
+            checkNewlineJsonMsg(out, level, flags, 2);
             int taglen=out.length()+1;
             if (type->getSubType()==SubType_Complex_SimpleContent)
             {
@@ -266,24 +286,46 @@ namespace JsonHelpers
                 int flds = type->getFieldCount();
                 for (int idx=0; idx<flds; idx++)
                 {
-                    delimitJSON(out);
-                    IPropertyTree *childtree = NULL;
+                    checkDelimitJsonMsg(out, level, flags);
+                    IXmlType *childType = type->queryFieldType(idx);
                     const char *childname = type->queryFieldName(idx);
-                    if (reqTree)
-                        childtree = reqTree->queryPropTree(childname);
-                    buildJsonMsg(parentTypes, type->queryFieldType(idx), out, childname, childtree, flags & ~REQSF_ROOT);
+                    bool repeats = type->queryFieldRepeats(idx);
+                    if (repeats)
+                    {
+                        Owned<IPropertyTreeIterator> children;
+                        if (reqTree)
+                            children.setown(reqTree->getElements(childname));
+                        appendJSONName(out, childname);
+                        out.append('[');
+                        checkNewlineJsonMsg(out, level, flags, 2);
+                        if (!children)
+                            buildJsonMsg(level, parentTypes, childType, out, NULL, NULL, flags & ~REQSF_ROOT);
+                        else
+                        {
+                            ForEach(*children)
+                                buildJsonMsg(level, parentTypes, childType, out, NULL, &children->query(), flags & ~REQSF_ROOT);
+                        }
+                        checkNewlineJsonMsg(out, level, flags, -2);
+                        out.append(']');
+                    }
+                    else
+                    {
+                        IPropertyTree *childtree = NULL;
+                        if (reqTree)
+                            childtree = reqTree->queryPropTree(childname);
+                        buildJsonMsg(level, parentTypes, childType, out, childname, childtree, flags & ~REQSF_ROOT);
+                    }
                 }
             }
 
             if (typeName)
                 parentTypes.pop();
+            checkNewlineJsonMsg(out, level, flags, -2); //end of children
             out.append("}");
         }
         else if (type->isArray())
         {
-            if (typeName && !parentTypes.appendUniq(typeName))
-                return; // recursive
-
+            bool skipContent = (typeName && !parentTypes.appendUniq(typeName)); // recursive
             const char* itemName = type->queryFieldName(0);
             IXmlType*   itemType = type->queryFieldType(0);
             if (!itemName || !itemType)
@@ -293,21 +335,29 @@ namespace JsonHelpers
             if (tag)
                 out.appendf("\"%s\": ", tag);
             out.append('{');
+            checkNewlineJsonMsg(out, level, flags, 2);
             out.appendf("\"%s\": [", itemName);
-            int taglen=out.length();
-            if (reqTree)
+            checkNewlineJsonMsg(out, level, flags, 2);
+            if (!skipContent)
             {
-                Owned<IPropertyTreeIterator> items = reqTree->getElements(itemName);
-                ForEach(*items)
-                    buildJsonMsg(parentTypes, itemType, delimitJSON(out), NULL, &items->query(), flags & ~REQSF_ROOT);
+                if (reqTree)
+                {
+                    Owned<IPropertyTreeIterator> items = reqTree->getElements(itemName);
+                    ForEach(*items)
+                    {
+                        checkDelimitJsonMsg(out, level, flags);
+                        buildJsonMsg(level, parentTypes, itemType, out, NULL, &items->query(), flags & ~REQSF_ROOT);
+                    }
+                }
+                else
+                    buildJsonMsg(level, parentTypes, itemType, out, NULL, NULL, flags & ~REQSF_ROOT);
+                if (typeName)
+                    parentTypes.pop();
             }
-            else
-                buildJsonMsg(parentTypes, itemType, out, NULL, NULL, flags & ~REQSF_ROOT);
 
+            checkNewlineJsonMsg(out, level, flags, -2);
             out.append(']');
-
-            if (typeName)
-                parentTypes.pop();
+            checkNewlineJsonMsg(out, level, flags, -2);
             out.append("}");
         }
         else // simple type
@@ -317,7 +367,15 @@ namespace JsonHelpers
         }
 
         if (flags & REQSF_ROOT)
+        {
+            checkNewlineJsonMsg(out, level, flags, -2);
             out.append('}');
+        }
+    }
+    static void buildJsonMsg(StringArray& parentTypes, IXmlType* type, StringBuffer& out, const char* tag, IPropertyTree *reqTree, unsigned flags)
+    {
+        unsigned level = 0;
+        buildJsonMsg(level, parentTypes, type, out, tag, reqTree, flags);
     }
 };
 #endif // _JSONHELPERS_HPP__

+ 5 - 0
esp/services/esdl_svc_engine/esdl_binding.cpp

@@ -2756,6 +2756,11 @@ int EsdlBindingImpl::onGetRespSampleXml(IEspContext &ctx, CHttpRequest* request,
     return onGetSampleXml(false, ctx, request, response, serv, method);
 }
 
+int EsdlBindingImpl::onGetRespSampleJson(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
+{
+    return 0;
+}
+
 int EsdlBindingImpl::onGetSampleXml(bool isRequest, IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method)
 {
     StringBuffer schema;

+ 1 - 0
esp/services/esdl_svc_engine/esdl_binding.hpp

@@ -366,6 +366,7 @@ public:
     virtual StringBuffer &generateNamespace(IEspContext &context, CHttpRequest* request, const char *serv, const char *method, StringBuffer &ns);
     virtual int onGetReqSampleXml(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);
     virtual int onGetRespSampleXml(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);
+    virtual int onGetRespSampleJson(IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);
     virtual void handleSoapRequestException(IException *e, const char *source);
 
     int onGetSampleXml(bool isRequest, IEspContext &ctx, CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);

+ 7 - 13
esp/xslt/gen_form.xsl

@@ -177,19 +177,12 @@
                                 </xsl:if>
                             </xsl:variable>
                             <b>&gt;<xsl:value-of select="$methodName"/>
-                            </b>&nbsp;<a>
-                                <xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'wsdl'"/></xsl:call-template></xsl:attribute>
-                                <img src="files_/img/wsdl.gif" title="WSDL" border="0" align="bottom"/>
-                            </a>&nbsp;<a>
-                                <xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'xsd'"/></xsl:call-template></xsl:attribute>
-                                <img src="files_/img/xsd.gif" title="Schema" border="0" align="bottom"/>
-                            </a>&nbsp;<a>
-                                <xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'reqxml'"/></xsl:call-template></xsl:attribute>
-                                <img src="files_/img/reqxml.gif" title="Sample Request XML" border="0" align="bottom"/>
-                            </a>&nbsp;<a>
-                                <xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'respxml'"/></xsl:call-template></xsl:attribute>
-                                <img src="files_/img/respxml.gif" title="Sample Response XML" border="0" align="bottom"/>
-                            </a>
+                            </b>&nbsp;&nbsp;
+                                &nbsp;<a><xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'wsdl'"/></xsl:call-template></xsl:attribute>WSDL</a>
+                                &nbsp;<a><xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'xsd'"/></xsl:call-template></xsl:attribute>XSD</a>
+                                &nbsp;<a><xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'reqxml'"/></xsl:call-template></xsl:attribute>XMLRequest</a>
+                                &nbsp;<a><xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'respxml'"/></xsl:call-template></xsl:attribute>XMLResponse</a>
+                                &nbsp;<a><xsl:attribute name="href"><xsl:call-template name="build_link"><xsl:with-param name="type" select="'respjson'"/></xsl:call-template></xsl:attribute>JSONResponse</a>
                         </td>
                     </tr>
                     <tr>
@@ -1459,6 +1452,7 @@
                     <xsl:choose>
                         <xsl:when test="$type='reqxml'"><xsl:value-of select="concat($methodName,'?reqxml_','&amp;',$params)"/></xsl:when>
                         <xsl:when test="$type='respxml'"><xsl:value-of select="concat($methodName,'?respxml_','&amp;',$params)"/></xsl:when>
+                        <xsl:when test="$type='respjson'"><xsl:value-of select="concat($methodName,'?respjson_','&amp;',$params)"/></xsl:when>
                         <xsl:when test="$type='xsd'"><xsl:value-of select="concat($methodName,'?xsd','&amp;',$params)"/></xsl:when>
                         <xsl:when test="$type='wsdl'"><xsl:value-of select="concat($methodName,'?wsdl','&amp;',$params)"/></xsl:when>
                         <xsl:when test="$type='action'"><xsl:value-of select="concat($methodName, $queryParams)"/></xsl:when>

+ 13 - 5
system/xmllib/xsdparser.cpp

@@ -92,6 +92,7 @@ public:
     size_t getFieldCount() { return 0; }
     IXmlType* queryFieldType(int idx) { return NULL; }
     const char* queryFieldName(int idx) { return NULL; }
+    bool queryFieldRepeats(int idx) {  return false; }
 
     const char* queryName() { return m_name.get(); }
 
@@ -455,6 +456,7 @@ protected:
     size_t     m_fldCount;
     char**     m_fldNames;
     IXmlType** m_fldTypes;
+    bool *     m_fldRepeats = nullptr;
     size_t     m_nAttrs;
     IXmlAttribute** m_attrs;
     XmlSubType m_subType;
@@ -462,9 +464,9 @@ protected:
 public: 
     IMPLEMENT_IINTERFACE;
 
-    CComplexType(const char* name, XmlSubType subType, size_t count, IXmlType** els, char** names, size_t nAttrs, IXmlAttribute** attrs=NULL) 
+    CComplexType(const char* name, XmlSubType subType, size_t count, IXmlType** els, char** names, size_t nAttrs, IXmlAttribute** attrs, bool *repeats)
         : m_name(name), m_subType(subType), m_fldCount(count), m_fldNames(names), 
-        m_fldTypes(els), m_nAttrs(nAttrs), m_attrs(attrs) { }
+        m_fldTypes(els), m_nAttrs(nAttrs), m_attrs(attrs), m_fldRepeats(repeats) { }
     
     virtual ~CComplexType() 
     { 
@@ -485,6 +487,8 @@ public:
                 m_attrs[i]->Release();
             delete[] m_attrs;
         }
+        if (m_fldRepeats)
+            delete[] m_fldRepeats;
     }
 
     const char* queryName() {  return m_name.get(); }
@@ -495,6 +499,7 @@ public:
     size_t getFieldCount() { return m_fldCount; }
     IXmlType* queryFieldType(int idx) { return m_fldTypes[idx]; }
     const char* queryFieldName(int idx) {  return m_fldNames[idx]; }
+    bool queryFieldRepeats(int idx) {  return m_fldRepeats ? m_fldRepeats[idx] : false; }
 
     size_t getAttrCount() { return m_nAttrs; }
     IXmlAttribute* queryAttr(int idx) { return m_attrs[idx]; }
@@ -531,6 +536,7 @@ public:
     size_t getFieldCount() { return 1; }
     IXmlType* queryFieldType(int idx) { return m_itemType; }
     const char* queryFieldName(int idx) { return m_itemName.get(); }
+    bool queryFieldRepeats(int idx) {  return true; }
 
     size_t getAttrCount() { return 0; }  // removed assert false to account for arrays
     const char* queryAttrName(int idx) { assert(false); return NULL; }
@@ -565,7 +571,6 @@ protected:
     {  
         if (name)
         {
-            assert(m_types.find(name) == m_types.end());
             m_types[name] = type;
         }
         else
@@ -851,7 +856,7 @@ IXmlType* CXmlSchema::parseComplexType(IPTree* complexDef)
                     throw MakeStringException(-1, "Invalid schema encoutered");
                 }
                 
-                CComplexType* typ = new CComplexType(name,subType,fldCount,types,NULL,nAttrs,attrs);
+                CComplexType* typ = new CComplexType(name,subType,fldCount,types,NULL,nAttrs,attrs, NULL);
                 addCache(name,typ);
                 return typ;
             }
@@ -875,7 +880,8 @@ IXmlType* CXmlSchema::parseComplexType(IPTree* complexDef)
 
             IXmlType** types = fldCount ? new IXmlType*[fldCount] : NULL;
             char** names = fldCount ? new char*[fldCount] : NULL;
-            CComplexType* typ = new CComplexType(name,subType,fldCount,types,names,nAttrs,attrs);
+            bool *repeats = fldCount ? new bool[fldCount] : NULL;
+            CComplexType* typ = new CComplexType(name,subType,fldCount,types,names,nAttrs,attrs, repeats);
             addCache(name,typ);
 
             int fldIdx = 0;
@@ -885,12 +891,14 @@ IXmlType* CXmlSchema::parseComplexType(IPTree* complexDef)
                 
                 const char* itemName = el.queryProp("@name");
                 const char* typeName = el.queryProp("@type");
+                const char *maxOccurs = el.queryProp("@maxOccurs");
                 IXmlType* type = typeName ? queryTypeByName(typeName,el.queryProp("@default")) : parseTypeDef(&el);
                 if (!type)
                     type = getNativeSchemaType("none", el.queryProp("@default")); //really should be tag only, no content?
                 
                 types[fldIdx] = type;
                 names[fldIdx] = strdup(itemName);
+                repeats[fldIdx] = (maxOccurs && streq(maxOccurs, "unbounded"));
                 fldIdx++;
             }
     

+ 1 - 0
system/xmllib/xsdparser.hpp

@@ -75,6 +75,7 @@ interface XMLLIB_API IXmlType : implements IInterface
     virtual size_t  getFieldCount() = 0;
     virtual IXmlType* queryFieldType(int idx) = 0;
     virtual const char* queryFieldName(int idx) = 0; 
+    virtual bool queryFieldRepeats(int idx) = 0;
 
     virtual bool isArray() = 0; 
     virtual XmlSubType getSubType() = 0;