简体   繁体   English

独立模式下的 Wiremock 请求模板:我可以使用 XML 文件作为响应模板并使用 XPATH 注入值吗?

[英]Wiremock request templating in standalone mode: can I use a XML file as response template and inject value with XPATH?

I know that request template supports XPath, so that I can get value from request like {{xPath request.body '/outer/inner/text()'}} .我知道请求模板支持 XPath,因此我可以从{{xPath request.body '/outer/inner/text()'}}类的请求中获取值。 I already have a XML file as response, and I want to inject this value I got from request, but keep the other parts of this response XML intact.我已经有一个 XML 文件作为响应,我想注入我从请求中获得的这个值,但保持这个响应 XML 的其他部分完好无损。 For example, I want to inject it to XPATH /svc_result/slia/pos/msid .例如,我想将其注入 XPATH /svc_result/slia/pos/msid

And I need to use it in standalone mode.我需要在独立模式下使用它。

I see another question( Wiremock Stand alone - How to manipulate response with request data ) but that was with JSON, I have XML request/response.我看到另一个问题( Wiremock Standalone - 如何使用请求数据操作响应),但那是使用 JSON,我有 XML 请求/响应。

How can it be done?如何做呢? Thanks.谢谢。

For example, I have this definition of mapping:例如,我有这样的映射定义:

{
    "request": {
        "method": "POST",
        "bodyPatterns": [
            {
                "matchesXPath": {
                    "expression": "/svc_init/slir/msids/msid[@type='MSISDN']/text()",
                    "equalTo": "200853000105614"
                }
            },
            {
                "matchesXPath": "/svc_init/hdr/client[id and pwd]"
            }
        ]
    },
    "response": {
        "status": 200,
        "bodyFileName": "slia.xml",
        "headers": {
            "Content-Type": "application/xml;charset=UTF-8"
        }
    }
}

And this request:而这个请求:

<?xml version="1.0"?>
<!DOCTYPE svc_init>
<svc_init ver="3.2.0">
    <hdr ver="3.2.0">
        <client>
            <id>dummy</id>
            <pwd>dummy</pwd>
        </client>
    </hdr>
    <slir ver="3.2.0" res_type="SYNC">
        <msids>
            <msid type="MSISDN">200853000105614</msid>
        </msids>
    </slir>
</svc_init>

I expect this response, with xxxxxxxxxxx replaced with the <msid> in the request.我期待这个响应,用请求中的<msid>替换xxxxxxxxxxx

<?xml version="1.0" ?>
<!DOCTYPE svc_result SYSTEM "MLP_SVC_RESULT_320.DTD">
<svc_result ver="3.2.0">
    <slia ver="3.0.0">
        <pos>
            <msid type="MSISDN" enc="ASC">xxxxxxxxxxx</msid>
            <pd>
                <time utc_off="+0800">20111122144915</time>
                <shape>
                    <EllipticalArea srsName="www.epsg.org#4326">
                        <coord>
                            <X>00 01 01N</X>
                            <Y>016 31 53E</Y>
                        </coord>
                        <angle>0</angle>
                        <semiMajor>2091</semiMajor>
                        <semiMinor>2091</semiMinor>
                        <angularUnit>Degrees</angularUnit>
                    </EllipticalArea>
                </shape>
                <lev_conf>90</lev_conf>
            </pd>
            <gsm_net_param>
                <cgi>
                    <mcc>100</mcc>
                    <mnc>01</mnc>
                    <lac>2222</lac>
                    <cellid>10002</cellid>
                </cgi>
                <neid>
                    <vmscid>
                        <vmscno>00004946000</vmscno>
                    </vmscid>
                    <vlrid>
                        <vlrno>99994946000</vlrno>
                    </vlrid>
                </neid>
            </gsm_net_param>
        </pos>
    </slia>
</svc_result>

My first thought was to use transformerParameters to change the response file by inserting the value from the body.我的第一个想法是使用transformerParameters通过插入正文中的值来更改响应文件。 Unfortunately, WireMock doesn't resolve the helpers before inserting them into the body response.不幸的是,WireMock 在将 helper 插入到 body 响应中之前并没有解析它们。 So while we can reference that MSID value via an xpath helper like因此,虽然我们可以通过 xpath helper 来引用该 MSID 值,例如

{{xPath request.body '/svc_init/slir/msids/msid/text()'}}

if we try to insert that as a custom transformer parameter, it won't resolve.如果我们尝试将其作为自定义转换器参数插入,则无法解析。 ( I've written up an issue on the WireMock github about this. ) 我已经在 WireMock github 上写了一个关于这个的问题。

Unfortunately, I think this leaves us with having to write a custom extension that will take the request and find the value and then modify the response file.不幸的是,我认为这让我们不得不编写一个自定义扩展来接受请求并找到值,然后修改响应文件。 More information on creating a custom transformer extensions can be found here. 可以在此处找到有关创建自定义转换器扩展的更多信息

At last I created my own transformer:最后我创建了自己的变压器:

package com.company.department.app.extensions;

import com.github.tomakehurst.wiremock.common.FileSource;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

public class NLGResponseTransformer extends ResponseTransformer {

    private static final Logger LOG = LoggerFactory.getLogger(NLGResponseTransformer.class);

    private static final String SLIA_FILE = "/stubs/__files/slia.xml";
    private static final String REQ_IMSI_XPATH = "/svc_init/slir/msids/msid";
    private static final String[] RES_IMSI_XPATHS = {
            "/svc_result/slia/pos/msid",
            "/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/msid"
    };
    private static final String[] RES_TIME_XPATHS = {
            // for slia.xml
            "/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/time",
            // for slia_poserror.xml
            "/svc_result/slia/pos/poserror/time"
    };

    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
    private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final String UTC_OFF = "utc_off";
    private static final String TRANSFORM_FACTORY_ATTRIBUTE_INDENT_NUMBER = "indent-number";
    protected static final String COMPANY_MLP_320_SLIA_EXTENSION_DTD = "company_mlp320_slia_extension.dtd";
    protected static final String MLP_SVC_RESULT_320_DTD = "MLP_SVC_RESULT_320.DTD";

    @Override
    public String getName() {
        return "inject-request-values";
    }

    @Override
    public Response transform(Request request, Response response, FileSource fileSource, Parameters parameters) {
        Document responseDocument = injectValuesFromRequest(request);
        String transformedResponse = transformToString(responseDocument);
        if (transformedResponse == null) {
            return response;
        }
        return Response.Builder.like(response)
                .but()
                .body(transformedResponse)
                .build();
    }

    private Document injectValuesFromRequest(Request request) {
        // NOTE: according to quickscan:
        // "time" element in the MLP is the time MME reports cell_id to GMLC (NLG), NOT the time when MME got the cell_id.
        LocalDateTime now = LocalDateTime.now();
        Document responseTemplate = readDocument(SLIA_FILE);
        Document requestDocument = readDocumentFromBytes(request.getBody());
        if (responseTemplate == null || requestDocument == null) {
            return null;
        }
        try {
            injectIMSI(responseTemplate, requestDocument);
            injectTime(responseTemplate, now);
        } catch (XPathExpressionException e) {
            LOG.error("Cannot parse XPath expression {}. Cause: ", REQ_IMSI_XPATH, e);
        }
        return responseTemplate;
    }

    private Document readDocument(String inputStreamPath) {
        try {
            DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
            // ignore missing dtd
            builder.setEntityResolver((publicId, systemId) -> {
                if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
                        systemId.contains(MLP_SVC_RESULT_320_DTD)) {
                    return new InputSource(new StringReader(""));
                } else {
                    return null;
                }
            });
            return builder.parse(this.getClass().getResourceAsStream(inputStreamPath));
        } catch (Exception e) {
            LOG.error("Cannot construct document from resource path. ", e);
            return null;
        }
    }

    private Document readDocumentFromBytes(byte[] array) {
        try {
            DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
            // ignore missing dtd
            builder.setEntityResolver((publicId, systemId) -> {
                if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
                        systemId.contains(MLP_SVC_RESULT_320_DTD)) {
                    return new InputSource(new StringReader(""));
                } else {
                    return null;
                }
            });
            return builder.parse(new ByteArrayInputStream(array));
        } catch (Exception e) {
            LOG.error("Cannot construct document from byte array. ", e);
            return null;
        }
    }

    private XPath newXPath() {
        return XPathFactory.newInstance().newXPath();
    }


    private void injectTime(Document responseTemplate, LocalDateTime now) throws XPathExpressionException {
        for (String timeXPath: RES_TIME_XPATHS) {
            Node timeTarget = (Node) (newXPath().evaluate(timeXPath, responseTemplate, XPathConstants.NODE));
            if (timeTarget != null) {
                // set offset in attribute
                Node offset = timeTarget.getAttributes().getNamedItem(UTC_OFF);
                offset.setNodeValue(getOffsetString());
                // set value
                timeTarget.setTextContent(TIME_FORMAT.format(now));
            }
        }
    }

    private void injectIMSI(Document responseTemplate, Document requestDocument) throws XPathExpressionException {
        Node imsiSource = (Node) (newXPath().evaluate(REQ_IMSI_XPATH, requestDocument, XPathConstants.NODE));
        String imsi = imsiSource.getTextContent();
        for (String xpath : RES_IMSI_XPATHS) {
            Node imsiTarget = (Node) (newXPath().evaluate(xpath, responseTemplate, XPathConstants.NODE));
            if (imsiTarget != null) {
                imsiTarget.setTextContent(imsi);
            }
        }
    }


    private String transformToString(Document document) {
        if (document == null) {
            return null;
        }
        document.setXmlStandalone(true); // make document to be standalone, so we can avoid outputing standalone="no" in first line
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans;
        try {
            trans = tf.newTransformer();
            trans.setOutputProperty(OutputKeys.INDENT, "no"); // no extra indent; file already has intent of 4
            // cannot find a workaround to inject dtd in doctype line. TODO
            //trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "MLP_SVC_RESULT_320.DTD [<!ENTITY % extension SYSTEM \"company_mlp320_slia_extension.dtd\"> %extension;]");
            StringWriter sw = new StringWriter();
            trans.transform(new DOMSource(document), new StreamResult(sw));
            // Spaces between tags are considered as text node, so when outputing we need to remove the extra empty lines
            return sw.toString().replaceAll("\\n\\s*\\n", "\n");
        } catch (TransformerException e) {
            LOG.error("Cannot transform response document to String. ", e);
            return null;
        }
    }


    /**
     * Compare system default timezone with UTC and get zone offset in form of (+/-)XXXX.
     * Dependent on the machine default timezone/locale.
     * @return
     */
    private String getOffsetString() {
        // getting offset in (+/-)XX:XX format, or "Z" if is UTC
        String offset = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).getOffset().toString();
        if (offset.equals("Z")) {
            return "+0000";
        }
        return offset.replace(":", "");
    }
}

And use it like this:并像这样使用它:

  1. mvn package it as a JAR(non-runnable), put it aside wiremock standalone jar, for example libs mvn package为 JAR(不可运行),将其放在 wiremock 独立 jar,例如libs
  2. Run this:运行这个:
java -cp libs/* com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --extensions com.company.department.app.extensions NLGResponseTransformer --https-port 8443 --verbose
  • Put the whole command on the same line.将整个命令放在同一行。

  • Notice the app jar which contains this transformer and wiremock standalone jar should be among classpath.请注意,包含此转换器和wiremock 独立 jar 的应用程序 jar 应该在类路径中。 Also, other dependencies under libs are needed.此外,还需要 libs 下的其他依赖项。 (I use jib maven plugin which copies all dependencies under libs/ ; I also move app and wiremock jars to libs/ , so I can put "-cp libs/*"). (我使用 jib maven 插件复制libs/下的所有依赖项;我还将 app 和 wiremock jar 移动到libs/ ,所以我可以放置“-cp libs/*”)。 If that does not work, try to specify the location of these two jars in -cp .如果这不起作用,请尝试在-cp指定这两个 jar 的位置。 Be ware that Wiremock will runs OK even when the extension class is not found .请注意,即使未找到扩展类,Wiremock 也能正常运行 So maybe add some loggings.所以也许添加一些日志记录。

  • You can use --root-dir to point to stubs files root, for example --root-dir resources/stubs in my case.您可以使用--root-dir指向存根文件根目录,例如在我的情况下--root-dir resources/stubs By default it points to .默认情况下它指向. (where java runs). (java运行的地方)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM