<% /*
    $Revision$
    $Id$

    File:           api2_rest.inc
    Description:    Functies voor de http-REST interface op de modellen
    Notes:          Hier wordt van alles met de 'buitenwereld' gecommuniceerd.
                    Het manipuleren met behulp van modellen gebeurt in api2.inc
    Status:

*/
%>
<!-- #include file="api2.inc" -->
<!-- #include file="api2_swagger.inc" -->
<!-- #include file="../scf/scaffolding_common.inc" -->
<%
var DEZE = this;
api2_rest = {
    authenticate: function _authenticate()
    {
        var method = String(Request.ServerVariables("REQUEST_METHOD"));
        if (user_key > 0 && method != "GET" && Session("stateless") != 1 && !Session("aut_client_key")) // Vereis dan wel het CSRF token
        {
            var token = Request.ServerVariables("HTTP_X_CSRF_TOKEN").Count   // Meegegeven als X-CSRF-TOKEN
                            ? String(Request.ServerVariables("HTTP_X_CSRF_TOKEN"))
                            : "";
            protectRequest.validateToken(token);
        }
        if (user_key < 0)
        {
            Response.Status = "401 Unauthorized";
            // Sommige applicaties kunnen in reactie hierop een b64 encoded username:password sturen
            // Die onderscheppen wij in LoginTry.asp uiteindelijk
            if (S("basic_auth_realm"))
                Response.AddHeader("WWW-Authenticate", "Basic realm=\"" + S("basic_auth_realm") + "\"");
            Response.End;
        }

        CheckForLogging(Request.QueryString("logging")); // Nu pas kan autorisatie via user gecontroleerd worden
    },

    impersonate: function _impersonate(model)
    {
        // Impersonate? (anno jan-2016 in de praktijk nergens gebruikt, kan mogelijk vervallen)
        if (!S("fac_api_allow_impersonate") || !model.impersonate_auth)
            return;

        var IMPERS;
        if (S("fac_api_key_in_url"))
            IMPERS = getQParam("SWITCHUSER", "");
        if (!IMPERS && Request.ServerVariables("HTTP_X_FACILITOR_SWITCH_USER").Count)
            IMPERS = String(Request.ServerVariables("HTTP_X_FACILITOR_SWITCH_USER")); // Meegegeven als X-FACILITOR-SWITCH-USER
        if (!IMPERS)
            return;

        var sql = "SELECT prs_perslid_key, prs_perslid_naam"
               + "  FROM prs_perslid"
               + " WHERE prs_perslid_verwijder IS NULL"
                + "   AND prs_perslid_oslogin = " + safe.quoted_sql_upper(IMPERS);
        var oRs = Oracle.Execute(sql);
        if (oRs.Eof)
        {
            Response.Status = "412 Invalid X-Facilitor-Switch-User header";
            Response.End;
        };
        __Log("IMPERS User is: " + oRs("prs_perslid_naam").Value);
        var other_user_key = oRs("prs_perslid_key").Value;
        oRs.Close();

        var xfunc = user.func_enabled2(model.module, { prs_key: other_user_key, isOptional: true });
        var can = (xfunc && xfunc.canRead(model.impersonate_auth));
        if (can)
        {
            /* global */ user_key = other_user_key;
            /* global */ user = new Perslid(user_key);
        }
        else
        {
            Response.Status = "412 Unauthorized X-Facilitor-Switch-User header";
            Response.End;
        }
    },
    process: function _process(model)
    {
        var wasCodePage = Session.Codepage;
        Session.Codepage = 65001; // We doen *uitsluitend* utf-8
        Response.Charset = 'utf-8';
        var inputformat = outputformat = getQParamSafe("format", "invalid").toLowerCase();
        if (outputformat == "auto")
        {
            var accept = String(Request.ServerVariables("HTTP_ACCEPT")).split(",")[0]; // Altijd alleen eerste bekijken
            switch (accept.toLowerCase())
            {
                case "application/xml":    outputformat = "xml";  break;
                case "application/json":   outputformat = "json"; break;
                case "text/csv":           outputformat = "csv"; break;
                case "text/html":          outputformat = "html"; break; // vanuit browser?
                default: outputformat = "json";
            }
        }

        if (outputformat == "json")
            /* global */ JSON_Result = true; // Zelf doen we er niets mee maar
                                             // shared.simple_page kijkt er naar

        var orgError = api2.error;
        api2.error = function (code, msg)
        {
            api2_rest.plugin.error_handler(code, msg, orgError);
        }

        api2_rest.authenticate();
        // Kip-ei: de omzetting naar new model() mag pas als je geauthenticeerd bent
        // Hierboven willen we het echter al wel meegeven
        if (typeof model == "function") // Nieuwe stijl is het een function. Even compatible.
            model = new model();

        api2_rest.impersonate(model);

        var method = String(Request.ServerVariables("REQUEST_METHOD"));
        method = api2_rest.plugin.transform_method(method);

        if (!/GET|PUT|POST|DELETE|PATCH/.test(method)) // Overigens houdt IIS deze al eerder tegen
        {
            __Log("Returning status: 405 Method not allowed");
            Response.Status = "405 Method not allowed";
            Response.End;
        }
        // JGL: Met zeer grote tegenzin behoorlijk UWVA specifiek
        if (Session("isWriteonlyApi2User"))
        {
            if (method == "PUT" || (method == "GET" && getQParam("api2") == "reports"))
            {
                // Dat is toegestaan
            }
            else
            {
                Response.Status = api2.error(400, "Method not supported for this user");
            }
        }

        var key = getQParamInt("id", -1); // Voor POST/PUT/DELETE

        var jsondata = {};
        var filter = shared.qs2json(model);
        filter = api2_rest.plugin.transform_filter(filter);
        var requestparams = { filter: filter, include: getQParamArray("include", []), method: method };
        if (filter.mode == "attachment" && !("includes" in model && "custom_fields" in model.includes))
            api2.error(400, "Attachment not supported for this model");

        if (filter.mode == "attachment")
        {
            if (key > 0 && method == "POST") // het bestand mag dan wel nieuw zijn (POST is van toepassing),
                method = "PUT";              // intern is het een update van een bestaand record, dus PUT
        }

        if (!("REST_" + method in model) || !model["REST_" + method])
        {
            __Log("Method {0} not supported for this model, returning 501".format(method));
            Response.Status = "501 Not Implemented";
            // TODO The response MUST include an Allow header containing a list of valid methods for the requested resource.
            Response.End;
        }

        if (/PUT|POST|PATCH/.test(method)) // Dan is er in de body data meegestuurd
        {
            if (filter.mode == "attachment" || filter.mode == "files")
            {
                // Body bevat application/octet-stream, dat lezen we later hier uit
                var bytes = Request.TotalBytes;
                if (bytes == 0)
                    api2.error(400, "Empty file");

                var bodyStream = Server.CreateObject("ADODB.Stream");
                bodyStream.Type = 1; // adTypeBinary eerst nog
                bodyStream.Open();
                try
                {
                    "---";
                    "--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
                    "---";
                    bodyStream.Write(Request.BinaryRead(bytes));
                }
                catch(e)
                {
                    var msg = L("lcl_shared_upload_error_start") + e.description + L("lcl_shared_upload_error_end");
                    api2.error(400, msg);
                }
            }
            else
            {
                if (inputformat == "auto")
                {
                    var contenttype = String(Request.ServerVariables("HTTP_CONTENT_TYPE")).split(";")[0]; // "text/xml; charset=utf-8", alleen de mimetype willen we
                    switch (contenttype.toLowerCase())
                    {
                        case "application/xml":
                            inputformat = "xml";
                            break;
                        case "application/json":
                        case "application/scim+json":
                            inputformat = "json";
                            break;
                        case "application/x-www-form-urlencoded":
                            inputformat = "form";
                            break;
                        default:
                            inputformat = outputformat;
                    }
                }

                switch (inputformat)
                {
                    case "json":
                    {
                        var parsed = RequestJSON();
                        if (parsed.error)
                            api2.error(400, "Error parsing input JSON: " + parsed.error);
                        jsondata = api2_rest.plugin.transform_incoming(requestparams, parsed.json);
                        if (!jsondata)
                            api2.error(400, "Error parsing input JSON: Empty");
                        break;
                    }
                    case "xml":
                    {
                        var parsed = RequestXML();
                        if (parsed.error)
                            api2.error(400, "Error parsing input XML: " + parsed.error);
                        jsondata = api2_rest.xml2json(parsed.xml);
                        if (!jsondata)
                            api2.error(400, "Error parsing input XML: Empty");
                        break;
                    }
                    //case "form":
                    //{
                    //    jsondata = { };
                    //    jsondata[model.recordname] = shared.form2json(model); // of api2.form2JSONdata inzetten?
                    //}
                    default:
                        UNKNOWN_CONTENT_TYPE;
                }
                if (!jsondata || !(model.record_name in jsondata || (model.multi_update && model.records_name in jsondata)))
                {
                    api2.error(400,  "No '{0}' found in input".format(model.record_name));
                }
                else
                {
                    // Validate the data and data types according to the model
                    // Throws api2.error if data is not found in model or given data is incompatible with its datatype
                    api2.validateJSON(jsondata, model);
                }
            }
        }

        var isSingle = /PUT|POST|DELETE|PATCH/.test(method) || (key > 0);  // PUT, POST en DELETE altijd single

        if (outputformat == "doc")
        {
            // Dan hoeven we verder bijna niets te doen
            if ("autfunction" in model)
            {
                if (typeof model["autfunction"] == "string")
                    model.autfunctionname = L("lcl_" + model.autfunction);
                else
                    delete model["autfunction"];
            }
            // Database hints toevoegen
            var sql = "SELECT fac_locale_xsl_label, COALESCE (fac_locale_xsl_cust, fac_locale_xsl_tekst) tekst"
                    + "  FROM fac_locale_xsl"
                    + " WHERE     fac_locale_xsl_lang = " + safe.quoted_sql(user_lang)
                    + "       AND fac_locale_xsl_module = 'ASP'"
                    + "       AND fac_locale_xsl_label LIKE 'hint.{0}%'".format(model.table)
                    + "       AND COALESCE (fac_locale_xsl_cust, fac_locale_xsl_tekst) IS NOT NULL";
            var oRs = Oracle.Execute(sql);
            while (!oRs.EOF)
            {
              var label = oRs("fac_locale_xsl_label").Value.split(".").pop(); // Na de laatste punt
              if (label in model.fields)
                model.fields[label].hint = oRs("tekst").Value;
              oRs.MoveNext();
            }
            oRs.Close()

            // TODO: velden strippen waar je niets mee te maken hebt?
        }
        else if (outputformat == "api" && getQParamInt("swagger", 0) == 0)
        {
            // TODO: Onderstaande in een of ander standaardformaat opleveren?
            var result = { id: model.records_name,
                           "name": model.record_title,
                           "names": model.records_title,
                           "records_name": model.records_name,
                           "record_name": model.record_name,
                           "authorization": model.autfunction,
                           methods: [],
                           includes: [],
                           fields: []
                         };
            for (var i in model.includes)
                result.includes.push(i);
            if (model["REST_GET"])
                result.methods.push("GET");
            if (model["REST_PUT"])
                result.methods.push("PUT");
            if (model["REST_POST"])
                result.methods.push("POST");
            if (model["REST_DELETE"])
                result.methods.push("DELETE");
            if (model["REST_PATCH"])
                result.methods.push("PATCH");

            for (var fld in model.fields)
            {                           // TODO: We missen hard-coded filters als reservableequipment/allowedinroom nu nog
                if (!model.fields.hidden)
                    result.fields.push({ id: fld,
                                          filter: model.fields[fld].filter,
                                          type: model.fields[fld].typ,
                                          label: model.fields[fld].label,
                                          required: model.fields[fld].required,
                                          valuelist: model.fields[fld].LOV && api2.splitLOV(model.fields[fld].LOV),
                                          translate: model.fields[fld].translate
                                        });
            }
        }
        else if (outputformat == "api" && getQParamInt("swagger", 0) == 1)
        {
            result = swaggermodel(model);
        }
        else
        {
            if (method == "DELETE")
            {
                if (!(key > 0))
                    api2.error(400, "Missing id");
                var result = model["REST_" + method]( requestparams, key );
            }
            else if (filter.mode == "attachment")
            {   // GET/PUT/POST https://xxxx.facilitor.nl/api2/visitors/99685/attachments/1040/testfile.jpg" bestaande folder
                // POST https://xxxx.facilitor.nl/api2/issues/0/attachments/1040/testfile.jpg" nieuw object, maakt TEMP aan
                // bodyStream heeft al een waarde gekregen
                if (wasCodePage != 65001 && filter.filename)
                {
                    // Door de IIS rewriter is de filenaam in de url utf-8 encoded
                    // Zet dat hier terug om naar Windows-1252
                    var fileStream = new ActiveXObject("ADODB.Stream");
                    fileStream.Open();
                    fileStream.Type = 2; // adTypeText
                    fileStream.Charset = 'Windows-1252';
                    fileStream.WriteText(filter.filename);
                    fileStream.Position = 0;
                    fileStream.Charset = 'utf-8';
                    filter.filename = fileStream.ReadText();
                    fileStream.Close();
                }

                requestparams.filter.id = key; // Die kan er maar beter wel zijn!
                requestparams.include = ["custom_fields"];
                var jsondata = { "custom_fields": [
                                   {
                                     "propertyid": parseInt(filter.subfolder, 10),
                                     "value": filter.filename, // Gaat de database in bij "F"
                                     "attachments": [
                                       {
                                         "name": filter.filename // Zo gaat hij heten op het filesysteem
                                       }
                                     ]
                                   }
                                 ]
                               };
                if (method == "PUT" || method == "POST")
                {
                    jsondata["custom_fields"][0]["attachments"][0].datastream = bodyStream;
                    if (key == 0) // new record
                    {
                        var result = model.includes["custom_fields"].model.REST_POST(requestparams, jsondata.custom_fields[0], -1);
                        var token = result.token;
                        // Bestandsnaam kan aangepast zijn.
                        jsondata.custom_fields[0].value = result.jsondata.value;
                        jsondata.custom_fields[0].attachments[0] = result.jsondata.attachments[0];
                        var record = { xflexparentkey: -1, propertyid: jsondata.custom_fields[0].propertyid};
                        var fileparams = { getFile: jsondata["custom_fields"][0].value, api2name: null, tmpfolder: token };
                    }
                    else
                    {
                        requestparams.custom_fields_only = true; // Voorkom bij nieuwe bijlage onnodige update vam hele issue
                        model["REST_" + method](requestparams, jsondata, key); // via het hoofdmodel met authorisatie controle en alles
                        var record = { xflexparentkey: key, propertyid: jsondata.custom_fields[0].propertyid};
                        var fileparams = { getFile: jsondata["custom_fields"][0].value, api2name: model.records_name };
                    }
                    var data = model.includes["custom_fields"].model.get_file_info(requestparams, record, fileparams );

                    api2_rest.deliver(data, /* dummy model */ { record_name: "attachment" }, outputformat, true);
                    return jsondata; // Attachment data teruggeven. Bestandsnaam kan aangepast zijn.
                }
                // Geen atachment dus door met de reguliere code
                var data = model["REST_" + method](requestparams, jsondata, key);
                if (method == "GET")
                {
                    if (!data.length) // mogelijk not authorized op hele record
                        Response.Status = "404 Not Found";
                    else
                        model.includes["custom_fields"].model.streamattachment(filter, data[0].custom_fields);
                    Response.End;
                }
                else
                    result = data;
            }
            else if (method == "GET")
            {
                if (outputformat == "csv" || outputformat == "xlsx")
                    requestparams.filter.nolimit = 1;
                var result = model["REST_" + method]( requestparams, null, key );
            }
            else if (filter.mode == "files")
            {
                var result = model["REST_" + method]( requestparams, bodyStream, key );
            }
            else if (model.record_name in jsondata) // een enkel record
            {
                if (method == "PUT" && !(key > 0))
                    api2.error(400, "Missing id");
                if (jsondata[model.record_name] instanceof Array)
                    api2.error(400, "{0} should be single record only.".format(method))

                var result = model["REST_" + method]( requestparams, jsondata[model.record_name], key );
            }
            else
            {   // Loop door de multiple records en geef de REST_ functie altijd één record
                var result = {};
                for (var record in jsondata[model.records_name])
                {
                    var thisdata = jsondata[model.records_name][record];
                    var result = model["REST_" + method]( requestparams, thisdata, thisdata.id );
                }
            }
        }

        switch (method)
        {
            case "DELETE":
            {
                Response.Status = "204 No Content";
                Response.End;
                break;
            }
            case "GET":
            {
                data = result;
                break;
            }
            case "PUT":
            case "POST":
            case "PATCH":
            {
                if (filter.mode == "files")
                {
                    var data = model.REST_GET({ filter: { filename: result.filename, iecode: filter.iecode }, dataonly: true }); // resulterende data weer terug
                    isSingle = true;
                }
                else
                {
                    var key = result.key;
                    if (key > 0 && !Session("isWriteonlyApi2User"))
                    {
                        // var params = { filter: shared.qs2json(model), include: getQParamArray("include", []) }, jsondata, key
                        // requestparams.include is mogelijk uitgebreid met wat er in de body stond
                        data = model.REST_GET({ filter: { id: key }, include: requestparams.include }); // resulterende data weer terug
                        __Log(data);
                    }
                    else
                    {
                        data = [];
                        isSingle = false;
                    }
                }
            }
        }

        api2_rest.deliver(data, model, outputformat, isSingle);

    },
    // Data is een array met 'records'
    deliver: function _deliver(data, model, format, single )
    {
        if (single && !data.length)
        {
            __Log("Returning status: 404 Not Found");
            Response.Status = "404 Not Found";
            Response.End;
        }

        if (format == "html" || format == "json" || format == "table" || format == "csv" || format == "xlsx")
        {
            var result = { };
            if (model.formatted_get)
                result = data;
            else if (single)
                result[model.record_name] = data[0];
            else
            {
                result.total_count = model.total_count;
                if (!(getQParam("justcount", 0) == 1))
                {
                    result.limit = model.limit;
                    result.offset = model.offset;
                    result[model.records_name] = data;
                }
            }

            var resultdata = api2_rest.plugin.transform_outgoing({}, result);
        }
        switch (format)
        {
            case "api":
            {
                //var xml_antwoord = api2_rest.json2xml(data, model, single);
                var str_antwoord = JSON.stringify(data, null, getQParam("pretty","0")=="1"?2:0);
                Response.ContentType = "application/json";
                break;
            }
            case "doc":
            {
                if (model.fields) // Ga de hints er bij zoeken
                {
                    var safefieldnames = [];
                    var lcl2fld = {};
                    for (var fld in model.fields)
                    {
                        if (fld.substring(0,1) == "_")
                        {
                            delete model.fields[fld];
                            continue;
                        }

                        for (var prop in model.fields[fld])
                        {
                            if (typeof model.fields[fld][prop] == "function") // Bijvoorbeeld model_issues.filter
                                model.fields[fld][prop] = "<<function>>";
                        }

                        if (model.fields[fld].LOV)
                            model.fields[fld].valuelist = api2.splitLOV(model.fields[fld].LOV, "lov_");

                        var lclname = "{0}.{1}.hint".format(model.records_name, fld);
                        lcl2fld[lclname] = fld;
                        safefieldnames.push(safe.quoted_sql(lclname));
                    }
                    if (safefieldnames.length)
                    {
                        //model.fields[fld].hint = "Hallo";
                        var sql = "SELECT fac_locale_xsl_label, "
                                + "       COALESCE(fac_locale_xsl_cust, fac_locale_xsl_tekst) fac_locale_xsl_tekst"
                                + "  FROM fac_locale_xsl xsl"
                                + " WHERE fac_locale_xsl_lang = " + safe.quoted_sql(user_lang)
                                + "   AND fac_locale_xsl_module = 'ASP'"
                                + "   AND fac_locale_xsl_label IN (" + safefieldnames.join(", ") + ")";
                        var oRs = Oracle.Execute(sql);
                        while (!oRs.Eof)
                        {
                            model.fields(lcl2fld[oRs("fac_locale_xsl_label").Value]) = oRs("fac_locale_xsl_tekst").value;
                            oRs.MoveNext();
                        }
                        oRs.Close();
                    }
                }
                Response.ContentType = "text/html";
                var str_antwoord = simple_json2xml(model, "api");
                var xml_antwoord = new ActiveXObject("MSXML2.DOMDocument.6.0");
                xml_antwoord.loadXML(str_antwoord);
                if (xml_antwoord.parseError.errorCode)
                {
                  abort_with_warning("XSL error: " + xml_antwoord.parseError.reason + " @ " + xml_antwoord.parseError.line + "." + xml_antwoord.parseError.linepos  + "\n"+  xml_antwoord.parseError.srcText);
                }

                var style = new ActiveXObject("MSXML2.DOMDocument.6.0");
                style.async = false;
                style.resolveExternals = true; // XSL kan includes hebben
                style.validateOnParse = true; // en moet correct zijn
                if (Request.QueryString("debug").Count == 0 || !user.has("WEB_FACTAB"))
                {
                    var xslname = model.xslname || "reference.xsl";
                    style.load(Server.MapPath(rooturl + "/appl/api2/" + xslname)); // De stylesheet laden. API's redeneren vanuit de root
                    var str_antwoord = xml_antwoord.transformNode(style); // terugstoppen in antwoord
                }
                else
                {
                    // Het kan zijn dat de stylesheet bepaalde informatie verbergt.
                    // Daarom niet zo maar aanbieden
                    if (Application("otap_environment") != "O")
                        ONLY_ON_OTAP_O;

                    style.load(Server.MapPath(rooturl + "/appl/shared/indent.xsl")); // De stylesheet laden. API's redeneren vanuit de root
                    var str_antwoord = "<pre>" + Server.HTMLEncode(xml_antwoord.transformNode(style)) + "</pre>";
                }
                if (style.parseError.errorCode)
                {
                  abort_with_warning("XSL error: " + style.parseError.reason + " @ " + style.parseError.line + "." + style.parseError.linepos );
                }
                break;
            }
            case "json":
                var str_antwoord = JSON.stringify(resultdata, null, getQParam("pretty","0")=="1"?2:0);
                Response.ContentType = "application/json";
                break;
            case "html":
                Response.ContentType = "text/html";
                var antwoord = JSON.stringify(resultdata, null, 2);
                var str_antwoord = "<!DOCTYPE html>\n<html>\n<head>"
                                 +     "\n<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>"
                                 +     "\n<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=yes'>"
                                 +     "\n<title>{0}</title>".format(safe.html(single?model.record_name:model.records_name))
                                 + "\n</head>"
                                 + "\n<body><pre>\n"
                                 +      Server.HTMLEncode(antwoord)
                                 + "\n</pre>\n</body>\n</html>";
                break;
            case "csv":
                Response.ContentType = "text/csv";
                var str_antwoord = api2_rest.json2csv(single?[resultdata[model.record_name]]:resultdata[model.records_name], model, single);
                break;
            case "xlsx":
                Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
                var str_antwoord = api2_rest.json2xlsx(single?[resultdata[model.record_name]]:resultdata[model.records_name], model, single);
                break;
            case "table":
                Response.ContentType = "text/html";
                if (single)
                    var antwoord = api2_rest.json2htmltable([resultdata[model.record_name]], model, single);
                else
                    var antwoord = api2_rest.json2htmltable(resultdata[model.records_name], model, single);
                if (getQParamInt("formail", 0) == 1)
                {
                    var fso = new ActiveXObject("Scripting.FileSystemObject");
                    var fcss = fso.OpenTextFile(Server.MapPath("appl/api2/table.css"));
                    var tekst = fcss.ReadAll();
                    fcss.Close();
                    var styletxt = "\n<style>/*embedding api2/table.css*/\n" + tekst + "\n</style>"
                }
                else
                {
                    var styletxt = "\n <link rel=stylesheet type='text/css' href='" + rooturl + "/appl/api2/table.css'>"
                }
                var str_antwoord = "\n<!DOCTYPE html>"
                                 + "\n<html>"
                                 + "\n<head>"
                                 + "\n     <title>{0}</title>".format(single?model.record_name:model.records_name)
                                 + styletxt
                                 + "\n</head>"
                                 + "\n<body>"
                                 +      antwoord
                                 + "\n</body>"
                                 + "\n</html>";
                break;
            case "xml":
                Response.ContentType = "text/xml";
                var xml_antwoord = api2_rest.json2xml(data, model, single);
                // TODO: Output XSL transform ondersteunen?
                var xsl = getQParamSafe("xsl", "");
                var xslfile;
                if (xsl)
                {
                    var fso = new ActiveXObject("Scripting.FileSystemObject");
                    xslfile = Server.MapPath(custpath + "/xsl/" + xsl + ".xsl");
                    if (!fso.FileExists(xslfile))
                      abort_with_warning("Stylesheet '{0}' not found".format(xsl));
                }
                else if (getQParam("pretty","0")=="1")
                    xslfile = Server.MapPath(rooturl + "/appl/shared/indent.xsl");
                if (xslfile)
                {
                    var style = new ActiveXObject("MSXML2.DOMDocument.6.0");
                    style.async = false;
                    style.resolveExternals = false;
                    style.load(xslfile);
                    if (style.parseError.errorCode)
                    {
                      abort_with_warning("XSL error: " + style.parseError.reason + " @ " + style.parseError.line + "." + style.parseError.linepos );
                    }
                    var str_antwoord = xml_antwoord.transformNode(style); // terugstoppen in antwoord
                }
                else
                    var str_antwoord = xml_antwoord.xml;
                //Response.ContentType = "application/json";
                //var str_antwoord = JSON.stringify(api2_rest.xml2json(xml_antwoord), null, 2);;
                break;
            default:
                WRONG_FORMAT;
        }

        // str_antwoord heeft nu het te versturen antwoord
        // Bepaal eTag
        var eTag = api2_rest.plugin.get_eTag({}, resultdata);
        if (!eTag)
        {
            var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
            var eTag = '"' + oCrypto.hex_sha1(String(S("cache_changecounter")) + "_" + str_antwoord).toLowerCase() + '"';
        }
        Response.AddHeader("ETag", eTag);
        if (Request.ServerVariables("HTTP_IF_NONE_MATCH") == eTag)
        {   // We hebben een match! Effectief besparen wel alleen op dataverkeer, de queries zijn al geweest
            Response.Clear();
            Response.Status = "304 Not modified";
            Response.End;
        }

        Response.write(str_antwoord);
    },

    // Merk op dat (onbedoeld) de tabel-kolommen bepaald worden door de velden in het *eerste* record
    // en niet zozeer door het gedefinieerde fields-object
    // Ook: verwacht wordt dat de volgorde van velden altijd gelijk is wat JavaScript 'for' niet
    // garandeert (maar in de praktijk wel goed gaat)
    json2htmltable: function _json2htmltable(data, model, single)
    {
        var ths = [];
        if (!data.length)
            return "\n<table>\n</table>"

        // Header maken
        var ths = [];
        for (var fld in data[0])
        {
            var lbl = fld;
            if (fld in model.fields)
                lbl = model.fields[fld].label;
            else
            {
                var val = data[0][fld];
                if (val && typeof val == "object")
                {
                    if (model.includes && fld in model.includes)
                        var lbl = model.includes[fld].model.records_title;
                    else
                        lbl = fld; // Attachments/flexfiles
                }
            }
            ths.push("<th title='{0}'>{1}</th>".format(safe.htmlattr(lbl), fld));
        }

        var trs = [];
        for (var i = 0; i < data.length; i++)
        {
            var rec = data[i];
            var tds = [];
            for (var fld in data[0]) // Hier dezelfde kolomvolgorde (van eerste record) gebruiken
            {
                var cls = "";
                if (model.fields[fld] && model.fields[fld].typ.match(/number|float|currency|key/) && !model.fields[fld].foreign)
                {
                    cls = " style='text-align:right'";
                }
                if (fld in rec)
                    var val = rec[fld];
                else
                    var val = "";
                var safeval = Server.HTMLEncode(String(val));
                if (val === null)
                    safeval = '&nbsp;';
                else if (typeof val == 'object' && val instanceof Date)
                    safeval = toISODateTimeString(val);
                else if (val && typeof val == "object" && "id" in val)
                {
                    var naam = val.name||"???";
                    if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
                        naam = toISODateTimeString(naam);
                    safeval = val.id + " <em>(" + safe.html(naam) + ")</em>";
                }
                else if (val && typeof val == "object" && model.includes && fld in model.includes)
                    safeval = api2_rest.json2htmltable(val, model.includes[fld].model, true); // dereference
                else if (val instanceof Array) // attachments array
                    safeval = api2_rest.json2htmltable(val, model, single);
                if (!single && model.primary && model.fields[fld] && model.fields[fld].dbs == model.primary)
                    safeval = "<a href='{0}/{1}.table'>{1}</a>".format(model.records_name, safeval);
                if (fld == "content_url")
                    safeval = "<a href='{0}' target='_blank'>{1}</a>".format(safe.htmlattr(val), safeval);
                tds.push("<td{0}>{1}</td>".format(single?"":cls, safeval));
            }
            trs.push(tds.join(""));
        }
        if (single && data.length == 1)
        {
            var html = "";
            for (var i = 0; i < ths.length; i++)
                html += "\n<tr>" + ths[i] + tds[i] + "</tr>";
            return "\n<table>\n" + html + "\n</table>";
        }
        else
            return "\n<table style='border-collapse: separate;'>"
                 + "\n<thead><tr style='position:sticky;top:0'>" + ths.join("") + "</tr>\n</thead>"
                 + "\n<tbody>\n<tr>" + trs.join("</tr>\n<tr>") + "</tr>\n</tbody>\n</table>";
    },

    json2csv: function _json2csv(data, model, single)
    {
        var trs = [];

        if (getQParam("nocsvheader",0)==0)
        { // Header maken
            var tds = [];
            for (var fld in data[0])
            {
                tds.push(fld);
            }
            trs.push(tds.join(";"));
        }
        // Nu de rows
        for (var i = 0; i < data.length; i++)
        {
            var rec = data[i];
            var tds = [];
            for (var fld in rec)
            {
                var field = model.fields[fld];
                var val = rec[fld];
                if (val == null)
                    safeval = "";
                else
                {
                    var safeval = String(val);
                    if (field.typ == "varchar" || field.typ == "memo")
                        safeval = safeval.replace(/\x0D\x0A/g,'\x0A').replace(/\"/g,'""'); // Voorzichtig met newlines

                    if (field.typ == "date" || field.typ == "datetime")
                        safeval = toISODateTimeString(val);
                    else if (val && typeof val == "object" && "id" in val)
                    {
                        var naam = val.name||"???";
                        if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
                            naam = toISODateTimeString(naam);
                        safeval = val.id + " (" + Server.HTMLEncode(naam) + ")";
                    }
                    else if (val && typeof val == "object" && model.includes && fld in model.includes)
                        api2.error(500, "Include={0} not supported with csv".format(fld));
                    else if (val instanceof Array) // attachments array
                        api2.error(500, "Include={0} not supported with csv".format("attachments"));

                    if (field.typ != "number" && field.typ != "check" && field.typ != "check0")
                        safeval = "\"" + safeval + "\"";
                }
                tds.push(safeval);
            }
            trs.push(tds.join(";"));
        }
        return trs.join("\r\n");
    },

    // Zie ook resultset_table_v2.inc/ResultsetTable.prototype.ProcessAsNativeExcel
    json2xlsx: function _json2xlsx(data, model, single)
    {
        var oExcel = new ActiveXObject("SLNKXLSX.Excel");
        var tmpxlsx = "XLSX_" + shared.random(16) +".xlsx"
        var oWb = oExcel.workbook_new(shared.tempFolder() + "/" + tmpxlsx, 1); // 1=constant memory
        oWb.set_property("author", user.naam());
        oWb.set_property("title", model.records_name);
        oWb.set_property("comments", "Created with Facilitor v{0}".format(FCLTVersion));

        var oWs = oWb.add_worksheet("Sheet1");

        var maxlengths = [];
        // Header row
        oWs.freeze_panes(1, 0);
        var col = 0;
        for (var fld in data[0])
        {
            var caption = fld; // Bij API2 beter dan model.field[fld].label omdat het iets meer Machine2Machine is.
            if (fld in model.fields) // niet bij includes
            {
                var tooltip = model.fields[fld].label;
                if (tooltip)
                    oWs.write_comment(0, col, tooltip); // maar wel label in het commentaar
            }
            var len = oWs.write_string(0, col++, caption, 1); // 1 = bold
            maxlengths.push(len);

            oWs.set_column_width(i, 8.43 /* LXW_DEF_COL_WIDTH */, 1); // Forceer alvast 'wrap' omdat met constant memory het niet achteraf kan
        }

        // Nu de rows
        for (var row = 0; row < data.length; row++)
        {
            var i = 0;
            var rec = data[row];
            var rownum = row + 1;
            for (var fld in rec)
            {
                var field = model.fields[fld];
                var val = rec[fld];
                if (val == null)
                {
                    i ++;
                    continue;
                }
                if (val && typeof val == "object" && "id" in val)
                {
                    var naam = val.name||"???";
                    //if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
                    //    naam = toISODateTimeString(naam);
                    val = naam; // TODO: val.id in tooltip?;
                    field.typ = "varchar";
                }
                else if (val && typeof val == "object" && model.includes && fld in model.includes)
                    api2.error(500, "Include={0} not supported with xlsx".format(fld));
                else if (val instanceof Array) // attachments array
                    api2.error(500, "Include={0} not supported with xlsx".format("attachments"));

                if (typeof val == "object" && isNaN(val))
                {
                    api2.error(500, "Field '{0}' value is NaN in record {1}".format(fld, row));
                }

                //if (typeof val == "string")
                //    field.typ = "varchar";

                var len = 0;
                //if  (typeof val == "string" && inArray(field.typ, ["date", "datetime", "time", "currency", "float", "number"]))
                //{
                //   if (!(this.columns[i].orgContent instanceof Function))
                //       __DoLog("Vreemd: kolom {0} is string en niet van het type {1}?".format(this.columns[i].caption, this.columns[i].datatype), "#ff0000");
                //   len = oWs.write_string(rownum, i, val);
                //}
                //else
                 switch (field.typ)
                {
                    case "varchar":
                    case "memo"    : len = oWs.write_string(rownum, i, String(val));
                                     break;
                    case "date"    : oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 0); //TODO .midnight() toepassen eindigt in verkeerde tijdzone?
                                     len = 16;
                                     break;
                    case "datetime": oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 1);
                                     len = 10;
                                     break;
                    case "time"    : // In de database ondersteunen we dit niet maar komt voor bij fac_report
                                     oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 2);
                                     len = 5;
                                     break;
                    case "currency": oWs.write_number(rownum, i, val, 1);
                                     break;
                    case "float"   : oWs.write_number(rownum, i, val, 1);
                                     break;
                    case "key":
                    case "check0":
                    case "check":
                    case "number":   oWs.write_number(rownum, i, val, 0);
                                     break;
                    case "processingtime":
                                    if (val["duration"] != null)
                                        val = val["duration"] + " " + (val["unit"]=='U'?L("lcl_mld_hours"):L("lcl_mld_days"));
                                    else
                                        val = "";
                                    len = oWs.write_string(rownum, i, val);
                                    break;
                    case "html":    val = val.replace(/(<([^>]+)>)/ig,"");  // strip html
                                    val = val.replace(/\&nbsp\;/ig, " ");// &nbsp; naar spatie;
                                    val = val.replace(/\&amp;\;/ig, "&");// er zijn er nog meer maar dit vind ik genoeg
                                    val = val.replace(/\&lt\;/ig, "<");
                                    val = val.replace(/\&gt\;/ig, ">");
                                    val = val.replace(/\&apos\;/ig, "'>'");
                                    val = val.replace(/\&quot\;/ig, "\"");
                                    // TODO: andere html-entities ook vertalen?
                                    len = oWs.write_string(rownum, i, val);
                                    break;
                    default:        len = oWs.write_string(rownum, i, String(val));
                }

                if (len > maxlengths[i])
                    maxlengths[i] = len;
                i++;
            }
        }
        for (var i = 0; i < maxlengths.length; i++)
        {
            if (maxlengths[i] > 8.43 * 10) // LXW_DEF_COL_WIDTH, 8.43 is de default Excel cell width
            {
                oWs.set_column_width(i, Math.min(maxlengths[i] / 10, 50), 1);
            }
        }
        oWb.workbook_close();
        var mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        var filename = "Facilitor {0} {1}.xlsx".format(model.records_name, toISODateTimeString(new Date()));
        var pcontentdp = "attachment";

        Response.AddHeader("content-disposition", pcontentdp + "; filename= \"" + safe.ContentName(filename) + "\"")
        StreamFile(shared.tempFolder(), tmpxlsx, { mime: mime ,
                                                   pcontentdp: "none", // geen pcontentdp, dat hebben we zelf gedaan
                                                   deletefile: __Logging == 0});
   },

    // TODO: Wanneer attributes gebruiken en wanneer (sub)-elements?
    // Streven: data == xml2json(json2xml(data))
    json2xml: function _json2xml(data, model, single)
    {
        var rootname = model.records_name;
        var record_name = model.record_name;
        var xmlDoc = new ActiveXObject("MSXML2.DOMDocument.6.0");
        xmlDoc.appendChild(xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""));

        var record2json = function(record, record_name)
        {
            var elementRecord = xmlDoc.createElement(record_name);
            for (var fld in record)
            {
                var elementField = xmlDoc.createElement(fld);
                if (record[fld] instanceof Date)
                {
                    var elementFieldText = xmlDoc.createTextNode(String(record[fld].toJSON()));
                    elementField.appendChild(elementFieldText);
                }
                else if (record[fld] instanceof Array)
                {
                    var inc_record_name;
                    for (var ii in model.includes)
                    {
//                        if (model.includes[ii].model.records_name == fld)
//                            inc_record_name = model.includes[ii].model.record_name;
                        if (ii == fld)
                            inc_record_name = model.includes[ii].model.record_name;
                    }
                    // Fallback voor attachments array --> "attachment"
                    if (!inc_record_name)
                        inc_record_name = fld.substr(0, fld.length-1);

                    for (var i = 0; i < record[fld].length; i++)
                        elementField.appendChild(record2json(record[fld][i], inc_record_name));
                }
                else if (record[fld] && typeof record[fld] == "object") // Veronderstelt dat dit foreign met name/key is
                {
                    if ("name" in record[fld] && "id" in record[fld])
                    {
                        elementField.setAttribute("name", record[fld].name===null?"":record[fld].name);
                        elementField.setAttribute("id", record[fld].id);
                    }
                    else
                        elementField = record2json(record[fld], fld);
                }
                else
                {
                    var elementFieldText = xmlDoc.createTextNode(String(record[fld]||""));
                    elementField.appendChild(elementFieldText);
                }
                elementRecord.appendChild(elementField);
            }
            return elementRecord;
        };

        if (single)
        {
            xmlDoc.appendChild(record2json(data[0], record_name));
        }
        else
        {
            var arrayElement = xmlDoc.createElement(rootname);
            arrayElement.setAttribute("total_count", model.total_count);
            arrayElement.setAttribute("limit", model.limit);
            arrayElement.setAttribute("offset", model.offset);

            for (var i = 0; i < data.length; i++)
                arrayElement.appendChild(record2json(data[i], record_name));

            xmlDoc.appendChild(arrayElement);
        }
        return xmlDoc;
    },
    // Streven: data == json2xml(xml2json(xml))
    // http://davidwalsh.name/convert-xml-json maar @attributes er uit gehaald
    xml2json: function _xml2json(xml)
    {
        // Create the return object
        var obj = {};

        if (xml.nodeType == 1)
        { // element
          // do attributes
          if (xml.attributes.length > 0)
          {
            // JGL removed: obj["@attributes"] = {};
            for (var j = 0; j < xml.attributes.length; j++)
            {
              var attribute = xml.attributes.item(j);
              obj[attribute.nodeName] = attribute.nodeValue;
            }
          }
          else
          {
            if (!xml.hasChildNodes())
                return null; // beter dan een leeg object
          }
        }
        else if (xml.nodeType == 3)
        { // text
          obj = xml.nodeValue;
        }

        // do children
        if (xml.hasChildNodes())
        {
          for(var i = 0; i < xml.childNodes.length; i++)
          {
            var item = xml.childNodes.item(i);
            var nodeName = item.nodeName;
            if (typeof(obj[nodeName]) == "undefined")
            {
              // JGL Added: Only one Textnode is simplified. Autodetect data
              if (item.nodeType == 3 && xml.childNodes.length == 1)
              {
                  var dt = myJSON.internal_parsedate(null, item.nodeValue);
                  if (dt && dt instanceof Date)
                      return dt;
                  return item.nodeValue;
              }

              obj[nodeName] = api2_rest.xml2json(item);
            }
            else
            {
              if (typeof(obj[nodeName].push) == "undefined")
              {
                var old = obj[nodeName];
                obj[nodeName] = [];
                obj[nodeName].push(old);
              }
              obj[nodeName].push(api2_rest.xml2json(item));
            }
          }
        }
        return obj;
    },
    find_plugin: function(inErrorHandler)
    {
        var plugin_name = getQParamSafe("plugin", "").toLowerCase();
        if (!plugin_name)
            return {};
        var fso = new ActiveXObject("Scripting.FileSystemObject");
        var paths = ["/cust/" + customerId, "/cust", "/appl/api2"]; // Hieronder zoeken naar '/plugins' folder
        for (var p in paths)
        {
            var ppath = Server.MapPath(rooturl + paths[p] + "/plugins/" + plugin_name + ".wsc")
            //__Log(ppath);
            if (fso.FileExists(ppath))
            {
                try
                {
                    var hook = GetObject("script:" + ppath);
                }
                catch(e)
                {
                    var txt = "Loading {0} failed: {1}".format(ppath, e.description);
                    if (inErrorHandler)
                    {
                        Response.Write(txt); // Zeer fataal. Probeer niet meer fancy te doen.
                        Response.End;
                    }
                    else
                        api2.error(500, txt);
                }
				// Via DEZE kan de aanroeper eigenlijk alle globale functies benaderen
                // zoals __Log
                hook.initialize({ S: S, Oracle: Oracle, customerId: customerId, safe: safe, DEZE: DEZE });
                return hook;
            }
        }
        abort_with_warning("Undefined plugin {0}".format(plugin_name), 500);
        //api2.error(500, "Undefined plugin {0}".format(plugin_name)); // Geen API2 aanroep als je al in de problemen zit: oneindige recursie.
    },
    plugin: {
        transform_filter: function(filter)
        {
            var outdata = filter;
            var hook = api2_rest.find_plugin();
            if ("transform_filter" in hook)
            {
                outdata = hook.transform_filter(filter);
            }
            hook = null;
            return outdata;
        },
        transform_method: function(method)
        {
            var outmethod = method;
            var hook = api2_rest.find_plugin();
            if ("transform_method" in hook)
            {
                outmethod = hook.transform_method(method);
            }
            hook = null;
            return outmethod;
        },
        error_handler: function(code, msg, orgHandler)
        {
            var hook = api2_rest.find_plugin(true);
            if ("error_handler" in hook)
                outdata = hook.error_handler(code, msg, orgHandler);
            else
                outdata = orgHandler(code, msg);
            hook = null;
            return outdata;
        },
        transform_incoming: function(params, data)
        {
            var outdata = data;
            var hook = api2_rest.find_plugin();
            if ("transform_incoming" in hook)
            {
                outdata = hook.transform_incoming(params, data);
            }
            hook = null;
            return outdata;
        },
        transform_outgoing: function(params, data)
        {
            var outdata = data;
            var hook = api2_rest.find_plugin();
            if ("transform_outgoing" in hook)
                outdata = hook.transform_outgoing(params, data);
            hook = null;
            return outdata;
        },
        get_eTag: function(params, data)
        {
            var outdata = null;
            var hook = api2_rest.find_plugin();
            if ("get_eTag" in hook)
                outdata = hook.get_eTag(params, data);
            hook = null;
            return outdata;
        }
    }
}

// LET OP: Verwacht wordt dat de JSON-code in de body utf-8 encoded is, niet windows-1252!
// (in de praktijk moet je *moeite* doen om windows-1252 te krijgen dus dit is handiger)
function RequestJSON()
{
    var jvraag;
    if(Request.TotalBytes > 0)
    {
        var lngBytesCount = Request.TotalBytes;
        "---";
        "--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
        "---";
        jvraag = BytesToStr(Request.BinaryRead(lngBytesCount));
    }
    __Log("Request body: " + jvraag);
    try
    {
        var vraag = myJSON.parse(jvraag);
    }
    catch (e)
    {
        result = { error: e.description || (e.message + ": " + e.name) };
        __DoLog("JSON parse faalt met: '{0}'\n{1}".format(result.error, jvraag), "ffff00");
        return result ;
    }
    return { json: vraag };
}

function RequestXML()
{
    var xvraag;
    if(Request.TotalBytes > 0)
    {
        var lngBytesCount = Request.TotalBytes;
        "---";
        "--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
        "---";
        xvraag = BytesToStr(Request.BinaryRead(lngBytesCount));
    }

    __Log("Request body: " + xvraag);
    try
    {
        var inputXML = Server.CreateObject("MSXML2.DOMDocument.6.0");
        inputXML.loadXML(xvraag); // inputXML.load(Request) zou nog niet vertalen naar Windows-1252
    }
    catch (e)
    {
        return { error: e.description };
    }
    if (inputXML.parseError.errorCode)
    {
        return { error: inputXML.parseError.reason + " @ " + inputXML.parseError.line + "." + inputXML.parseError.linepos };
    }
    return { xml: inputXML };
}

function BytesToStr(bytes)
{
    var stream = Server.CreateObject("ADODB.Stream");
    stream.type = 1;
    stream.open;
    stream.write(bytes);
    stream.position = 0;
    stream.type = 2; // Text
    stream.charset = "utf-8";

    // Merk op dat we windows-1252 niet ondersteunen, je moet echt utf-8 sturen

    // Mogelijk was het nog utf-8. Dat is riskant met unicode quotes (FCLT#60591)
    // Daarom expliciete conversie
    var win1252Stream = new ActiveXObject("ADODB.Stream");
    win1252Stream.Open();
    win1252Stream.CharSet = "Windows-1252"; // het doel
    stream.copyTo(win1252Stream);
    stream.close;

    win1252Stream.position = 0;
    var ReadBytes = 131072; // optimaal volgens https://docs.microsoft.com/en-us/sql/ado/reference/ado-api/readtext-method?view=sql-server-ver15
    var sOut = "";
    while (!win1252Stream.EOS)
    {
        sOut = sOut + win1252Stream.ReadText(ReadBytes);
    }
    win1252Stream.close;
    return sOut;
}

function getQParamISODate(pName, defVal)
{
    return _get_ParamISODate(Request.Querystring, pName, defVal)
}
function getFParamISODate(pName, defVal)
{
    return _get_ParamISODate(Request.Form, pName, defVal)
}
function _get_ParamISODate(pColl, pName, defVal)
{
    var strval = _get_Param(pColl, pName, defVal, true); // force: een lege waarde wordt als afwezig beschouwd
    if (strval)
    {
        var dt = myJSON.internal_parsedate(null, strval);
        if (dt && dt instanceof Date)
            return dt;
    }

    if (defVal instanceof Date)
    {
        return defVal;
    }
    if (defVal === null) // bewust triple===
    {
        return null;
    }
    // Error message will get to client and/or IIS logfiles
    _AiAi("INTERNAL_ERROR_PARAMETER_" + pName + "_IS_NOT_ISODATE");
}

var simple_json2xml = (function (undefined) {
    "use strict";
    var tag = function (name, closing) {
        return (closing ? "</" : "\n<") + name + ">";
    };
    return function (obj, rootname) {
        var xml = "";
        for (var i in obj) {
            if (obj.hasOwnProperty(i)) {
                var value = obj[i],
                    type = typeof value;
                if (value === obj) // bij merged_model zouden we hier oneindige recursie krijgen
                    continue;
                if (value instanceof Array && type == 'object') {
                    for (var sub in value) {
                        xml += simple_json2xml(value[sub]);
                    }
                } else if (value instanceof Object && type == 'object') {
                    xml += tag(i) + simple_json2xml(value) + tag(i, 1);
                } else {
                    xml += tag(i) + (value===null?"":Server.HTMLEncode(value)) + tag(i, 1);
                }
            }
        }

        return rootname ? tag(rootname) + xml + tag(rootname, 1) : xml;
    };
})(simple_json2xml || {});
%>