fileContents = stores.getFilesBlobStore().get(filename);
+
+ return fileContents
+ .map(bytes -> new ResponseDefinition(HTTP_OK, bytes))
+ .orElseGet(ResponseDefinition::notFound);
}
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/BasicMappingBuilder.java b/src/main/java/com/github/tomakehurst/wiremock/client/BasicMappingBuilder.java
index f8d14ca5d8..c28caf2b7c 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/client/BasicMappingBuilder.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/client/BasicMappingBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2023 Thomas Akehurst
+ * Copyright (C) 2011-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,14 +28,7 @@
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.RequestMethod;
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
-import com.github.tomakehurst.wiremock.matching.ContentPattern;
-import com.github.tomakehurst.wiremock.matching.MultiValuePattern;
-import com.github.tomakehurst.wiremock.matching.MultipartValuePatternBuilder;
-import com.github.tomakehurst.wiremock.matching.RequestPattern;
-import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
-import com.github.tomakehurst.wiremock.matching.StringValuePattern;
-import com.github.tomakehurst.wiremock.matching.UrlPattern;
-import com.github.tomakehurst.wiremock.matching.ValueMatcher;
+import com.github.tomakehurst.wiremock.matching.*;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import java.util.ArrayList;
import java.util.List;
@@ -282,6 +275,12 @@ public BasicMappingBuilder andMatching(String customRequestMatcherName, Paramete
return this;
}
+ @Override
+ public BasicMappingBuilder andMatching(CustomMatcherDefinition matcherDefinition) {
+ requestPatternBuilder.andMatching(matcherDefinition);
+ return this;
+ }
+
private boolean requiredScenarioExist() {
return scenarioName == null && (requiredScenarioState != null || newScenarioState != null);
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/MappingBuilder.java b/src/main/java/com/github/tomakehurst/wiremock/client/MappingBuilder.java
index c05b41c749..8afc9af32a 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/client/MappingBuilder.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/client/MappingBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2023 Thomas Akehurst
+ * Copyright (C) 2011-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,11 +19,7 @@
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
import com.github.tomakehurst.wiremock.http.Request;
-import com.github.tomakehurst.wiremock.matching.ContentPattern;
-import com.github.tomakehurst.wiremock.matching.MultiValuePattern;
-import com.github.tomakehurst.wiremock.matching.MultipartValuePatternBuilder;
-import com.github.tomakehurst.wiremock.matching.StringValuePattern;
-import com.github.tomakehurst.wiremock.matching.ValueMatcher;
+import com.github.tomakehurst.wiremock.matching.*;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import java.util.Map;
import java.util.Set;
@@ -97,6 +93,8 @@ MappingBuilder withServeEventListener(
MappingBuilder andMatching(String customRequestMatcherName, Parameters parameters);
+ MappingBuilder andMatching(CustomMatcherDefinition matcherDefinition);
+
MappingBuilder willReturn(ResponseDefinitionBuilder responseDefBuilder);
StubMapping build();
diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java b/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java
index 6860f86d68..3096376876 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java
@@ -254,7 +254,12 @@ public static EqualToXmlPattern equalToXml(String value) {
}
public static EqualToXmlPattern equalToXml(String value, boolean enablePlaceholders) {
- return new EqualToXmlPattern(value, enablePlaceholders, null, null, null);
+ return new EqualToXmlPattern(value, enablePlaceholders, null, null, null, false);
+ }
+
+ public static EqualToXmlPattern equalToXml(
+ String value, boolean enablePlaceholders, boolean ignoreOrderOfSameNode) {
+ return new EqualToXmlPattern(value, enablePlaceholders, ignoreOrderOfSameNode);
}
public static EqualToXmlPattern equalToXml(
@@ -267,7 +272,23 @@ public static EqualToXmlPattern equalToXml(
enablePlaceholders,
placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex,
- null);
+ null,
+ false);
+ }
+
+ public static EqualToXmlPattern equalToXml(
+ String value,
+ boolean enablePlaceholders,
+ String placeholderOpeningDelimiterRegex,
+ String placeholderClosingDelimiterRegex,
+ boolean ignoreOrderOfSameNode) {
+ return new EqualToXmlPattern(
+ value,
+ enablePlaceholders,
+ placeholderOpeningDelimiterRegex,
+ placeholderClosingDelimiterRegex,
+ null,
+ ignoreOrderOfSameNode);
}
public static MatchesXPathPattern matchingXPath(String value) {
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/AbstractFileSource.java b/src/main/java/com/github/tomakehurst/wiremock/common/AbstractFileSource.java
index b613d6e7cf..f05bd31ff3 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/AbstractFileSource.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/AbstractFileSource.java
@@ -15,6 +15,7 @@
*/
package com.github.tomakehurst.wiremock.common;
+import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.github.tomakehurst.wiremock.security.NotAuthorisedException;
@@ -54,10 +55,13 @@ public TextFile getTextFileNamed(String name) {
@Override
public void createIfNecessary() {
- assertWritable();
+ if (rootDirectory.isDirectory()) {
+ return;
+ }
+
if (rootDirectory.exists() && rootDirectory.isFile()) {
throw new IllegalStateException(rootDirectory + " already exists and is a file");
- } else if (!rootDirectory.exists()) {
+ } else if (!rootDirectory.exists() && !readOnly()) {
rootDirectory.mkdirs();
}
}
@@ -97,11 +101,12 @@ private List toTextFileList(List fileList) {
@Override
public void writeTextFile(String name, String contents) {
- writeTextFileAndTranslateExceptions(contents, writableFileFor(name));
+ writeBinaryFile(name, contents.getBytes(UTF_8));
}
@Override
public void writeBinaryFile(String name, byte[] contents) {
+ createIfNecessary();
writeBinaryFileAndTranslateExceptions(contents, writableFileFor(name));
}
@@ -166,29 +171,20 @@ private void assertFilePathIsUnderRoot(String path) {
}
}
- private void ensureDirectoryExists(File toFile) throws IOException {
- Path toPath = toFile.toPath();
- if (!java.nio.file.Files.exists(toPath)) {
- Path toParentPath = toPath.getParent();
- java.nio.file.Files.createDirectories(toParentPath);
- }
- }
-
- private void writeTextFileAndTranslateExceptions(String contents, File toFile) {
+ private void writeBinaryFileAndTranslateExceptions(byte[] contents, File toFile) {
try {
ensureDirectoryExists(toFile);
- Files.write(toFile.toPath(), contents.getBytes(UTF_8));
+ Files.write(toFile.toPath(), contents);
} catch (IOException ioe) {
- throw new RuntimeException(ioe);
+ throwUnchecked(ioe);
}
}
- private void writeBinaryFileAndTranslateExceptions(byte[] contents, File toFile) {
- try {
- ensureDirectoryExists(toFile);
- Files.write(toFile.toPath(), contents);
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
+ private void ensureDirectoryExists(File toFile) throws IOException {
+ Path toPath = toFile.toPath();
+ if (!java.nio.file.Files.exists(toPath)) {
+ Path toParentPath = toPath.getParent();
+ java.nio.file.Files.createDirectories(toParentPath);
}
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/DefaultNetworkAddressRules.java b/src/main/java/com/github/tomakehurst/wiremock/common/DefaultNetworkAddressRules.java
index 5a136f40d7..5667029f14 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/DefaultNetworkAddressRules.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/DefaultNetworkAddressRules.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022-2023 Thomas Akehurst
+ * Copyright (C) 2022-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
*/
package com.github.tomakehurst.wiremock.common;
-import static com.github.tomakehurst.wiremock.common.NetworkAddressRange.ALL;
+import static com.github.tomakehurst.wiremock.common.NetworkAddressRange.ALL_RANGES;
import static com.github.tomakehurst.wiremock.common.NetworkAddressUtils.isValidInet4Address;
import static java.util.stream.Collectors.toSet;
@@ -37,7 +37,7 @@ public DefaultNetworkAddressRules(
networkAddressRange ->
!(networkAddressRange instanceof NetworkAddressRange.DomainNameWildcard))
.collect(toSet()),
- Set.of(ALL));
+ ALL_RANGES);
this.allowedHostPatterns =
defaultIfEmpty(
allowed.stream()
@@ -45,7 +45,7 @@ public DefaultNetworkAddressRules(
networkAddressRange ->
(networkAddressRange instanceof NetworkAddressRange.DomainNameWildcard))
.collect(toSet()),
- Set.of(ALL));
+ ALL_RANGES);
this.denied =
denied.stream()
.filter(
@@ -80,4 +80,12 @@ public boolean isAllowed(String testValue) {
&& deniedHostPatterns.stream().noneMatch(rule -> rule.isIncluded(testValue));
}
}
+
+ @Override
+ public boolean isAllowedAll() {
+ return allowed.equals(ALL_RANGES)
+ && allowedHostPatterns.equals(ALL_RANGES)
+ && denied.isEmpty()
+ && deniedHostPatterns.isEmpty();
+ }
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/KeyLocks.java b/src/main/java/com/github/tomakehurst/wiremock/common/KeyLocks.java
new file mode 100644
index 0000000000..5a12e59a3d
--- /dev/null
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/KeyLocks.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 Thomas Akehurst
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.tomakehurst.wiremock.common;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class KeyLocks {
+
+ private final ConcurrentHashMap locks = new ConcurrentHashMap<>();
+
+ @SuppressWarnings("unchecked")
+ public T withLock(String key, Callable action) {
+ try {
+ lock(key);
+ return (T) Exceptions.uncheck(action::call, Object.class);
+ } finally {
+ unlock(key);
+ }
+ }
+
+ private void lock(String key) {
+ LockWrapper lockWrapper =
+ locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
+ lockWrapper.lock.lock();
+ }
+
+ private void unlock(String key) {
+ LockWrapper lockWrapper = locks.get(key);
+ lockWrapper.lock.unlock();
+ if (lockWrapper.removeThreadFromQueue() == 0) {
+ locks.remove(key, lockWrapper);
+ }
+ }
+
+ private static class LockWrapper {
+ private final Lock lock = new ReentrantLock();
+ private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
+
+ private LockWrapper addThreadInQueue() {
+ numberOfThreadsInQueue.incrementAndGet();
+ return this;
+ }
+
+ private int removeThreadFromQueue() {
+ return numberOfThreadsInQueue.decrementAndGet();
+ }
+ }
+}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRange.java b/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRange.java
index 2cfa7a7183..d62f21692d 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRange.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRange.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022-2023 Thomas Akehurst
+ * Copyright (C) 2022-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,11 +21,13 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;
+import java.util.Set;
import java.util.regex.Pattern;
public abstract class NetworkAddressRange {
public static final NetworkAddressRange ALL = new All();
+ public static final Set ALL_RANGES = Set.of(ALL);
private static final Pattern SINGLE_IP =
Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRules.java b/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRules.java
index 7542fe9f2b..4940faa95b 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRules.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/NetworkAddressRules.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 Thomas Akehurst
+ * Copyright (C) 2023-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,14 +15,14 @@
*/
package com.github.tomakehurst.wiremock.common;
-import static com.github.tomakehurst.wiremock.common.NetworkAddressRange.ALL;
+import static com.github.tomakehurst.wiremock.common.NetworkAddressRange.ALL_RANGES;
import static java.util.Collections.emptySet;
import java.util.HashSet;
import java.util.Set;
public interface NetworkAddressRules {
- NetworkAddressRules ALLOW_ALL = new DefaultNetworkAddressRules(Set.of(ALL), emptySet());
+ NetworkAddressRules ALLOW_ALL = new DefaultNetworkAddressRules(ALL_RANGES, emptySet());
static Builder builder() {
return new Builder();
@@ -30,6 +30,8 @@ static Builder builder() {
boolean isAllowed(String testValue);
+ boolean isAllowedAll();
+
public static class Builder {
private final Set allowed = new HashSet<>();
private final Set denied = new HashSet<>();
@@ -47,7 +49,7 @@ public Builder deny(String expression) {
public NetworkAddressRules build() {
Set allowedRanges = allowed;
if (allowedRanges.isEmpty()) {
- allowedRanges = Set.of(ALL);
+ allowedRanges = ALL_RANGES;
}
return new DefaultNetworkAddressRules(Set.copyOf(allowedRanges), Set.copyOf(denied));
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java b/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java
index 1e4934a0a6..b01ecdc70d 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java
@@ -18,6 +18,7 @@
import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
import static com.github.tomakehurst.wiremock.common.Strings.ordinalIndexOf;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import com.github.tomakehurst.wiremock.http.QueryParameter;
import com.google.common.collect.ImmutableListMultimap;
@@ -27,6 +28,7 @@
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
+import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.stream.Collectors;
@@ -97,8 +99,20 @@ public static String urlToPathParts(URI uri) {
return nodeCount > 0 ? String.join("-", uriPathNodes) : "";
}
- public static String decode(String encoded) {
- return URLDecoder.decode(encoded, UTF_8);
+ private static String decode(String encoded) {
+ if (!isISOOffsetDateTime(encoded)) {
+ return URLDecoder.decode(encoded, UTF_8);
+ }
+ return encoded;
+ }
+
+ private static boolean isISOOffsetDateTime(String encoded) {
+ try {
+ ISO_OFFSET_DATE_TIME.parse(encoded);
+ } catch (DateTimeParseException e) {
+ return false;
+ }
+ return true;
}
public static URL safelyCreateURL(String url) {
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDocument.java b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDocument.java
index dbb573804f..3b4098589c 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDocument.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDocument.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020-2021 Thomas Akehurst
+ * Copyright (C) 2020-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,8 +15,6 @@
*/
package com.github.tomakehurst.wiremock.common.xml;
-import static javax.xml.xpath.XPathConstants.NODESET;
-
import com.github.tomakehurst.wiremock.common.ListOrSingle;
import java.util.HashMap;
import java.util.Map;
@@ -24,12 +22,12 @@
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathEvaluationResult;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.Document;
-import org.w3c.dom.NodeList;
import org.xmlunit.util.Convert;
-public class XmlDocument extends XmlNode {
+public class XmlDocument extends XmlDomNode {
private final Document document;
@@ -47,20 +45,19 @@ public ListOrSingle findNodes(String xPathExpression, Map xPathEvaluationResult;
if (namespaces != null) {
Map fullNamespaces = addStandardNamespaces(namespaces);
NamespaceContext namespaceContext = Convert.toNamespaceContext(fullNamespaces);
xPath.setNamespaceContext(namespaceContext);
- nodeSet =
- (NodeList)
- xPath.evaluate(
- xPathExpression, Convert.toInputSource(new DOMSource(document)), NODESET);
+ xPathEvaluationResult =
+ xPath.evaluateExpression(
+ xPathExpression, Convert.toInputSource(new DOMSource(document)));
} else {
- nodeSet = (NodeList) xPath.evaluate(xPathExpression, document, NODESET);
+ xPathEvaluationResult = xPath.evaluateExpression(xPathExpression, document);
}
- return toListOrSingle(nodeSet);
+ return toListOrSingle(xPathEvaluationResult);
} catch (XPathExpressionException e) {
throw XPathException.fromXPathException(e);
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDomNode.java b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDomNode.java
new file mode 100644
index 0000000000..63098d210c
--- /dev/null
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDomNode.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 Thomas Akehurst
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.tomakehurst.wiremock.common.xml;
+
+import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
+
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xml.sax.XMLReader;
+
+public class XmlDomNode extends XmlNode {
+
+ private final Node domNode;
+ private final Map attributes;
+
+ public XmlDomNode(Node domNode) {
+ this.domNode = domNode;
+ attributes =
+ domNode.hasAttributes()
+ ? convertAttributeMap(domNode.getAttributes())
+ : Collections.emptyMap();
+ }
+
+ private static Map convertAttributeMap(NamedNodeMap namedNodeMap) {
+ Map map = new HashMap<>();
+ for (int i = 0; i < namedNodeMap.getLength(); i++) {
+ Node node = namedNodeMap.item(i);
+ map.put(node.getNodeName(), node.getNodeValue());
+ }
+
+ return Collections.unmodifiableMap(map);
+ }
+
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ public String getName() {
+ return domNode.getNodeName();
+ }
+
+ public String getText() {
+ return domNode.getTextContent();
+ }
+
+ @Override
+ public String toString() {
+ switch (domNode.getNodeType()) {
+ case Node.TEXT_NODE:
+ case Node.ATTRIBUTE_NODE:
+ return domNode.getTextContent();
+ case Node.DOCUMENT_NODE:
+ case Node.ELEMENT_NODE:
+ return render();
+ default:
+ return domNode.toString();
+ }
+ }
+
+ private String render() {
+ try {
+ Transformer transformer = TRANSFORMER_CACHE.get();
+ StreamResult result = new StreamResult(new StringWriter());
+ Source source = getSourceForTransform(domNode);
+ transformer.transform(source, result);
+ return result.getWriter().toString();
+ } catch (Exception e) {
+ return throwUnchecked(e, String.class);
+ }
+ }
+
+ private static final Class DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();
+
+ @SuppressWarnings("unchecked")
+ private static Class getDom2SaxAvailability() {
+ try {
+ return (Class)
+ Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ // This nasty little hack attempts to ensure no exception is thrown when attempting to print an
+ // XML node with
+ // unbound namespace prefixes (which can happen when you've selected an element via XPath whose
+ // namespaces are declared in a parent element).
+ // For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
+ private static Source getSourceForTransform(Node node) {
+ if (DOM2SAX_XMLREADER_CLASS != null) {
+ try {
+ Constructor constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
+ XMLReader dom2SAX = constructor.newInstance(node);
+ SAXSource saxSource = new SAXSource();
+ saxSource.setXMLReader(dom2SAX);
+ return saxSource;
+ } catch (NoSuchMethodException
+ | InstantiationException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ return new DOMSource(node);
+ }
+ }
+
+ return new DOMSource(node);
+ }
+}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java
index e07d17eb79..7704cfcd42 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java
@@ -20,24 +20,16 @@
import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
import com.github.tomakehurst.wiremock.common.ListOrSingle;
-import java.io.StringWriter;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.Map;
-import javax.xml.transform.*;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.sax.SAXSource;
-import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathEvaluationResult;
import javax.xml.xpath.XPathFactory;
-import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.XMLReader;
-public class XmlNode {
+public abstract class XmlNode {
protected static final ThreadLocal XPATH_CACHE =
ThreadLocal.withInitial(
@@ -74,106 +66,25 @@ public class XmlNode {
}
});
- private static final Class DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();
+ public abstract Map getAttributes();
- private static Class getDom2SaxAvailability() {
- try {
- return (Class)
- Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
- } catch (ClassNotFoundException e) {
- return null;
- }
- }
-
- private final Node domNode;
- private final Map attributes;
-
- public XmlNode(Node domNode) {
- this.domNode = domNode;
- attributes =
- domNode.hasAttributes()
- ? convertAttributeMap(domNode.getAttributes())
- : Collections.emptyMap();
- }
-
- private static Map convertAttributeMap(NamedNodeMap namedNodeMap) {
- Map map = new HashMap<>();
- for (int i = 0; i < namedNodeMap.getLength(); i++) {
- Node node = namedNodeMap.item(i);
- map.put(node.getNodeName(), node.getNodeValue());
- }
-
- return Collections.unmodifiableMap(map);
- }
-
- public Map getAttributes() {
- return attributes;
- }
-
- protected static ListOrSingle toListOrSingle(NodeList nodeList) {
- ListOrSingle nodes = new ListOrSingle<>();
- for (int i = 0; i < nodeList.getLength(); i++) {
- nodes.add(new XmlNode(nodeList.item(i)));
- }
+ @SuppressWarnings("unchecked")
+ protected static ListOrSingle toListOrSingle(XPathEvaluationResult> evaluationResult) {
+ ListOrSingle xmlNodes = new ListOrSingle<>();
- return nodes;
- }
-
- public String getName() {
- return domNode.getNodeName();
- }
-
- public String getText() {
- return domNode.getTextContent();
- }
-
- @Override
- public String toString() {
- switch (domNode.getNodeType()) {
- case Node.TEXT_NODE:
- case Node.ATTRIBUTE_NODE:
- return domNode.getTextContent();
- case Node.DOCUMENT_NODE:
- case Node.ELEMENT_NODE:
- return render();
+ switch (evaluationResult.type()) {
+ case NODESET:
+ Iterable nodes = (Iterable) evaluationResult.value();
+ nodes.forEach(node -> xmlNodes.add(new XmlDomNode(node)));
+ break;
+ case NODE:
+ xmlNodes.add(new XmlDomNode((Node) evaluationResult.value()));
+ break;
default:
- return domNode.toString();
- }
- }
-
- private String render() {
- try {
- Transformer transformer = TRANSFORMER_CACHE.get();
- StreamResult result = new StreamResult(new StringWriter());
- Source source = getSourceForTransform(domNode);
- transformer.transform(source, result);
- return result.getWriter().toString();
- } catch (Exception e) {
- return throwUnchecked(e, String.class);
- }
- }
-
- // This nasty little hack attempts to ensure no exception is thrown when attempting to print an
- // XML node with
- // unbound namespace prefixes (which can happen when you've selected an element via XPath whose
- // namespaces are declared in a parent element).
- // For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
- private static Source getSourceForTransform(Node node) {
- if (DOM2SAX_XMLREADER_CLASS != null) {
- try {
- Constructor constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
- XMLReader dom2SAX = constructor.newInstance(node);
- SAXSource saxSource = new SAXSource();
- saxSource.setXMLReader(dom2SAX);
- return saxSource;
- } catch (NoSuchMethodException
- | InstantiationException
- | IllegalAccessException
- | InvocationTargetException e) {
- return new DOMSource(node);
- }
+ xmlNodes.add(new XmlPrimitiveNode<>(evaluationResult.value()));
+ break;
}
- return new DOMSource(node);
+ return xmlNodes;
}
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlPrimitiveNode.java b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlPrimitiveNode.java
new file mode 100644
index 0000000000..092dcb1de2
--- /dev/null
+++ b/src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlPrimitiveNode.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 Thomas Akehurst
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.tomakehurst.wiremock.common.xml;
+
+import java.text.NumberFormat;
+import java.util.Collections;
+import java.util.Map;
+
+public class XmlPrimitiveNode extends XmlNode {
+
+ private final T value;
+
+ public XmlPrimitiveNode(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return value instanceof Number ? NumberFormat.getInstance().format(value) : value.toString();
+ }
+
+ @Override
+ public Map getAttributes() {
+ return Collections.emptyMap();
+ }
+}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/Options.java b/src/main/java/com/github/tomakehurst/wiremock/core/Options.java
index 9f3f979e25..2388ca03fb 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/core/Options.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/core/Options.java
@@ -47,6 +47,9 @@ enum ChunkedEncodingPolicy {
int DEFAULT_TIMEOUT = 300_000;
int DEFAULT_CONTAINER_THREADS = 25;
String DEFAULT_BIND_ADDRESS = "0.0.0.0";
+ int DEFAULT_MAX_HTTP_CONNECTIONS = 1000;
+ boolean DEFAULT_DISABLE_CONNECTION_REUSE = true;
+ Long DEFAULT_MAX_TEMPLATE_CACHE_ENTRIES = 1000L;
int portNumber();
@@ -94,6 +97,8 @@ enum ChunkedEncodingPolicy {
boolean shouldPreserveHostHeader();
+ boolean shouldPreserveUserAgentProxyHeader();
+
String proxyHostHeader();
HttpServerFactory httpServerFactory();
@@ -138,9 +143,7 @@ default Function getNotMatchedRendererFactory()
int proxyTimeout();
- default int getMaxHttpClientConnections() {
- return 1000;
- }
+ int getMaxHttpClientConnections();
boolean getResponseTemplatingEnabled();
@@ -152,7 +155,7 @@ default int getMaxHttpClientConnections() {
boolean getTemplateEscapingDisabled();
- default Set getSupportedProxyEncodings() {
- return null;
- }
+ Set getSupportedProxyEncodings();
+
+ boolean getDisableConnectionReuse();
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java
index 7c5f2cabcb..e2ef4ed5ca 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java
@@ -69,6 +69,7 @@ public class WireMockApp implements StubServer, Admin {
private final Recorder recorder;
private final List globalSettingsListeners;
private final Map mappingsLoaderExtensions;
+ private final Map serveEventListeners;
private Options options;
@@ -115,6 +116,12 @@ public WireMockApp(Options options, Container container) {
Map customMatchers =
extensions.ofType(RequestMatcherExtension.class);
+ Map concatenatedMap =
+ new HashMap<>(extensions.ofType(ServeEventListener.class));
+ concatenatedMap.put("wiremock-gui", new GuiServeEventListener());
+
+ serveEventListeners = Collections.unmodifiableMap(concatenatedMap);
+
requestJournal =
options.requestJournalDisabled()
? new DisabledRequestJournal()
@@ -132,7 +139,8 @@ public WireMockApp(Options options, Container container) {
extensions.ofType(ResponseDefinitionTransformer.class),
extensions.ofType(ResponseDefinitionTransformerV2.class),
stores.getFilesBlobStore(),
- List.copyOf(extensions.ofType(StubLifecycleListener.class).values()));
+ List.copyOf(extensions.ofType(StubLifecycleListener.class).values()),
+ serveEventListeners);
nearMissCalculator =
new NearMissCalculator(stubMappings, requestJournal, scenarios, customMatchers);
recorder =
@@ -141,6 +149,7 @@ public WireMockApp(Options options, Container container) {
this.mappingsLoaderExtensions = extensions.ofType(MappingsLoaderExtension.class);
this.container = container;
+ extensions.startAll();
loadDefaultMappings();
}
@@ -172,6 +181,9 @@ public WireMockApp(
: new StoreBackedRequestJournal(
maxRequestJournalEntries, requestMatchers, stores.getRequestJournalStore());
scenarios = new InMemoryScenarios(stores.getScenariosStore());
+
+ serveEventListeners = Map.of("wiremock-gui", new GuiServeEventListener());
+
stubMappings =
new StoreBackedStubMappings(
stores.getStubStore(),
@@ -180,7 +192,8 @@ public WireMockApp(
transformers,
v2transformers,
stores.getFilesBlobStore(),
- Collections.emptyList());
+ Collections.emptyList(),
+ serveEventListeners);
this.container = container;
nearMissCalculator =
new NearMissCalculator(stubMappings, requestJournal, scenarios, requestMatchers);
@@ -206,14 +219,6 @@ public AdminRequestHandler buildAdminRequestHandler() {
public StubRequestHandler buildStubRequestHandler() {
Map postServeActions = extensions.ofType(PostServeAction.class);
-
- Map concatenatedMap =
- new HashMap<>(extensions.ofType(ServeEventListener.class));
- concatenatedMap.put("wiremock-gui", new GuiServeEventListener());
-
- Map serveEventListeners =
- Collections.unmodifiableMap(concatenatedMap);
-
BrowserProxySettings browserProxySettings = options.browserProxySettings();
final com.github.tomakehurst.wiremock.http.client.HttpClientFactory httpClientFactory =
@@ -563,6 +568,7 @@ public Extensions getExtensions() {
@Override
public void shutdownServer() {
+ extensions.stopAll();
stores.stop();
container.shutdown();
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java
index 205a3f71da..f6f20d1d2a 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java
@@ -101,6 +101,7 @@ public class WireMockConfiguration implements Options {
private List matchingHeaders = emptyList();
private boolean preserveHostHeader;
+ private boolean preserveUserAgentProxyHeader;
private String proxyHostHeader;
private HttpServerFactory httpServerFactory = new JettyHttpServerFactory();
private HttpClientFactory httpClientFactory = new ApacheHttpClientFactory();
@@ -140,10 +141,13 @@ public class WireMockConfiguration implements Options {
private int proxyTimeout = DEFAULT_TIMEOUT;
+ private int maxHttpClientConnections = DEFAULT_MAX_HTTP_CONNECTIONS;
+ private boolean disableConnectionReuse = DEFAULT_DISABLE_CONNECTION_REUSE;
+
private boolean templatingEnabled = true;
private boolean globalTemplating = false;
private Set permittedSystemKeys = null;
- private Long maxTemplateCacheEntries = null;
+ private Long maxTemplateCacheEntries = DEFAULT_MAX_TEMPLATE_CACHE_ENTRIES;
private boolean templateEscapingDisabled = true;
private Set supportedProxyEncodings = null;
@@ -404,6 +408,11 @@ public WireMockConfiguration preserveHostHeader(boolean preserveHostHeader) {
return this;
}
+ public WireMockConfiguration preserveUserAgentProxyHeader(boolean preserveUserAgentProxyHeader) {
+ this.preserveUserAgentProxyHeader = preserveUserAgentProxyHeader;
+ return this;
+ }
+
public WireMockConfiguration proxyHostHeader(String hostHeaderValue) {
this.proxyHostHeader = hostHeaderValue;
return this;
@@ -540,6 +549,16 @@ public WireMockConfiguration proxyTimeout(int proxyTimeout) {
return this;
}
+ public WireMockConfiguration maxHttpClientConnections(int maxHttpClientConnections) {
+ this.maxHttpClientConnections = maxHttpClientConnections;
+ return this;
+ }
+
+ public WireMockConfiguration disableConnectionReuse(boolean disableConnectionReuse) {
+ this.disableConnectionReuse = disableConnectionReuse;
+ return this;
+ }
+
public WireMockConfiguration templatingEnabled(boolean templatingEnabled) {
this.templatingEnabled = templatingEnabled;
return this;
@@ -711,6 +730,11 @@ public boolean shouldPreserveHostHeader() {
return preserveHostHeader;
}
+ @Override
+ public boolean shouldPreserveUserAgentProxyHeader() {
+ return preserveUserAgentProxyHeader;
+ }
+
@Override
public String proxyHostHeader() {
return proxyHostHeader;
@@ -824,6 +848,16 @@ public int proxyTimeout() {
return proxyTimeout;
}
+ @Override
+ public int getMaxHttpClientConnections() {
+ return maxHttpClientConnections;
+ }
+
+ @Override
+ public boolean getDisableConnectionReuse() {
+ return disableConnectionReuse;
+ }
+
@Override
public boolean getResponseTemplatingEnabled() {
return templatingEnabled;
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/Extension.java b/src/main/java/com/github/tomakehurst/wiremock/extension/Extension.java
index de987a6c28..d5f6677a4b 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/Extension.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/Extension.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014-2021 Thomas Akehurst
+ * Copyright (C) 2014-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,4 +18,8 @@
public interface Extension {
String getName();
+
+ default void start() {}
+
+ default void stop() {}
}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/Extensions.java b/src/main/java/com/github/tomakehurst/wiremock/extension/Extensions.java
index 3e106fed73..b805e33bc3 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/Extensions.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/Extensions.java
@@ -128,15 +128,16 @@ private void configureTemplating() {
.flatMap(Set::stream)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+ final List templateModelProviders =
+ new ArrayList<>(ofType(TemplateModelDataProviderExtension.class).values());
+
templateEngine =
new TemplateEngine(
helpers,
options.getMaxTemplateCacheEntries(),
options.getTemplatePermittedSystemKeys(),
- options.getTemplateEscapingDisabled());
-
- final List templateModelProviders =
- new ArrayList<>(ofType(TemplateModelDataProviderExtension.class).values());
+ options.getTemplateEscapingDisabled(),
+ templateModelProviders);
if (options.getResponseTemplatingEnabled()) {
final ResponseTemplateTransformer responseTemplateTransformer =
@@ -233,6 +234,14 @@ public static Extension load(Class extends Extension> extensionClass) {
}
}
+ public void startAll() {
+ loadedExtensions.values().forEach(Extension::start);
+ }
+
+ public void stopAll() {
+ loadedExtensions.values().forEach(Extension::stop);
+ }
+
@SuppressWarnings("unchecked")
public Map ofType(Class extensionType) {
return (Map)
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListenerUtils.java b/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListenerUtils.java
new file mode 100644
index 0000000000..0d45f26512
--- /dev/null
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListenerUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Thomas Akehurst
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.tomakehurst.wiremock.extension;
+
+import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier;
+
+import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
+import java.util.List;
+import java.util.Map;
+
+public class ServeEventListenerUtils {
+ public static void triggerListeners(
+ Map serveEventListeners,
+ ServeEventListener.RequestPhase requestPhase,
+ ServeEvent serveEvent) {
+
+ serveEventListeners.values().stream()
+ .filter(ServeEventListener::applyGlobally)
+ .forEach(listener -> listener.onEvent(requestPhase, serveEvent, Parameters.empty()));
+
+ List serveEventListenerDefinitions =
+ serveEvent.getServeEventListeners();
+ for (ServeEventListenerDefinition listenerDef : serveEventListenerDefinitions) {
+ ServeEventListener listener = serveEventListeners.get(listenerDef.getName());
+ if (listener != null
+ && !listener.applyGlobally()
+ && listenerDef.shouldFireFor(requestPhase)) {
+ Parameters parameters = listenerDef.getParameters();
+ listener.onEvent(requestPhase, serveEvent, parameters);
+ } else {
+ notifier().error("No per-stub listener was found named \"" + listenerDef.getName() + "\"");
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/LazyTemplateEngine.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/LazyTemplateEngine.java
index 791fa7152c..ace35557c3 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/LazyTemplateEngine.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/LazyTemplateEngine.java
@@ -16,6 +16,8 @@
package com.github.tomakehurst.wiremock.extension.responsetemplating;
import com.github.tomakehurst.wiremock.common.Lazy;
+import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
+import java.util.Map;
import java.util.function.Supplier;
public class LazyTemplateEngine extends TemplateEngine {
@@ -35,6 +37,11 @@ public HandlebarsOptimizedTemplate getUncachedTemplate(String content) {
return templateEngineLazy.get().getUncachedTemplate(content);
}
+ @Override
+ public Map buildModelForRequest(ServeEvent serveEvent) {
+ return templateEngineLazy.get().buildModelForRequest(serveEvent);
+ }
+
@Override
public long getCacheSize() {
return templateEngineLazy.get().getCacheSize();
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestLine.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestLine.java
index 5db0198c73..5c7966286e 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestLine.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestLine.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017-2023 Thomas Akehurst
+ * Copyright (C) 2017-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
import com.github.tomakehurst.wiremock.common.ListOrSingle;
import com.github.tomakehurst.wiremock.common.Urls;
-import com.github.tomakehurst.wiremock.common.url.PathTemplate;
+import com.github.tomakehurst.wiremock.common.url.PathParams;
import com.github.tomakehurst.wiremock.http.QueryParameter;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.RequestMethod;
@@ -26,10 +26,6 @@
import java.util.Map.Entry;
import java.util.stream.Collectors;
-@Deprecated
-/**
- * @deprecated Use the accessors on {@link RequestTemplateModel}
- */
public class RequestLine {
private final RequestMethod method;
private final String scheme;
@@ -39,7 +35,7 @@ public class RequestLine {
private final String url;
private final String clientIp;
- private final PathTemplate pathTemplate;
+ private final PathParams pathParams;
private RequestLine(
RequestMethod method,
@@ -49,7 +45,7 @@ private RequestLine(
String url,
String clientIp,
Map> query,
- PathTemplate pathTemplate) {
+ PathParams pathParams) {
this.method = method;
this.scheme = scheme;
this.host = host;
@@ -57,10 +53,10 @@ private RequestLine(
this.url = url;
this.clientIp = clientIp;
this.query = query;
- this.pathTemplate = pathTemplate;
+ this.pathParams = pathParams;
}
- public static RequestLine fromRequest(final Request request, final PathTemplate pathTemplate) {
+ public static RequestLine fromRequest(final Request request) {
URI url = URI.create(request.getUrl());
Map rawQuery = Urls.splitQuery(url);
Map> adaptedQuery =
@@ -76,7 +72,7 @@ public static RequestLine fromRequest(final Request request, final PathTemplate
request.getUrl(),
request.getClientIp(),
adaptedQuery,
- pathTemplate);
+ request.getPathParameters());
}
public RequestMethod getMethod() {
@@ -84,7 +80,7 @@ public RequestMethod getMethod() {
}
public Object getPathSegments() {
- return pathTemplate == null ? new UrlPath(url) : new TemplatedUrlPath(url, pathTemplate);
+ return pathParams.isEmpty() ? new UrlPath(url) : new TemplatedUrlPath(url, pathParams);
}
public String getPath() {
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestPartTemplateModel.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestPartTemplateModel.java
new file mode 100644
index 0000000000..44c46bded8
--- /dev/null
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestPartTemplateModel.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 Thomas Akehurst
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.tomakehurst.wiremock.extension.responsetemplating;
+
+import com.github.tomakehurst.wiremock.common.ListOrSingle;
+import com.github.tomakehurst.wiremock.http.Body;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.TreeMap;
+
+public class RequestPartTemplateModel {
+
+ private final String name;
+ private final Map> headers;
+ private final Body body;
+
+ public RequestPartTemplateModel(
+ String name, Map> headers, Body body) {
+ this.name = name;
+ this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ this.headers.putAll(headers);
+ this.body = body;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map> getHeaders() {
+ return headers;
+ }
+
+ public String getBody() {
+ return body.asString();
+ }
+
+ public String getBodyAsBase64() {
+ return body.asBase64();
+ }
+
+ public boolean isBinary() {
+ return body.isBinary();
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", "[", "]")
+ .add("name='" + name + "'")
+ .add("headers=" + headers)
+ .add("body=" + body.asString())
+ .toString();
+ }
+}
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestTemplateModel.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestTemplateModel.java
index 6b5a724b51..b336b7e33e 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestTemplateModel.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RequestTemplateModel.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2023 Thomas Akehurst
+ * Copyright (C) 2016-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,48 +16,46 @@
package com.github.tomakehurst.wiremock.extension.responsetemplating;
import com.github.tomakehurst.wiremock.common.ListOrSingle;
-import com.github.tomakehurst.wiremock.common.url.PathTemplate;
-import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.http.Body;
import com.github.tomakehurst.wiremock.http.RequestMethod;
-import com.google.common.collect.Maps;
import java.util.Map;
-import java.util.TreeMap;
public class RequestTemplateModel {
+ private final String id;
private final RequestLine requestLine;
private final Map> headers;
private final Map> cookies;
- private final String body;
+
+ private final boolean isMultipart;
+ private final Body body;
+ private final Map parts;
protected RequestTemplateModel(
+ String id,
RequestLine requestLine,
Map> headers,
Map> cookies,
- String body) {
+ boolean isMultipart,
+ Body body,
+ Map parts) {
+ this.id = id;
this.requestLine = requestLine;
this.headers = headers;
this.cookies = cookies;
+ this.isMultipart = isMultipart;
this.body = body;
+ this.parts = parts;
}
- public static RequestTemplateModel from(final Request request) {
- return from(request, null);
- }
-
- public static RequestTemplateModel from(final Request request, final PathTemplate pathTemplate) {
- RequestLine requestLine = RequestLine.fromRequest(request, pathTemplate);
- Map> adaptedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- adaptedHeaders.putAll(
- Maps.toMap(
- request.getAllHeaderKeys(), input -> ListOrSingle.of(request.header(input).values())));
- Map> adaptedCookies =
- Maps.transformValues(request.getCookies(), cookie -> ListOrSingle.of(cookie.getValues()));
-
- return new RequestTemplateModel(
- requestLine, adaptedHeaders, adaptedCookies, request.getBodyAsString());
+ public String getId() {
+ return id;
}
+ @Deprecated
+ /**
+ * @deprecated Use the direct accessors
+ */
public RequestLine getRequestLine() {
return requestLine;
}
@@ -107,7 +105,23 @@ public Map> getCookies() {
}
public String getBody() {
- return body;
+ return body.asString();
+ }
+
+ public String getBodyAsBase64() {
+ return body.asBase64();
+ }
+
+ public boolean isBinary() {
+ return body.isBinary();
+ }
+
+ public boolean isMultipart() {
+ return isMultipart;
+ }
+
+ public Map getParts() {
+ return parts;
}
public String getClientIp() {
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java
index 94a83f49f3..c93dee0664 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java
@@ -22,7 +22,6 @@
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.common.FileSource;
import com.github.tomakehurst.wiremock.common.TextFile;
-import com.github.tomakehurst.wiremock.common.url.PathTemplate;
import com.github.tomakehurst.wiremock.extension.*;
import com.github.tomakehurst.wiremock.http.*;
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
@@ -75,20 +74,8 @@ public ResponseDefinition transform(ServeEvent serveEvent) {
ResponseDefinitionBuilder newResponseDefBuilder =
ResponseDefinitionBuilder.like(responseDefinition);
- final PathTemplate pathTemplate =
- serveEvent.getStubMapping().getRequest().getUrlMatcher().getPathTemplate();
-
- final Map additionalModelData =
- templateModelDataProviders.stream()
- .map(provider -> provider.provideTemplateModelData(serveEvent).entrySet())
- .flatMap(Set::stream)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- final Map model = new HashMap<>();
- model.put("parameters", parameters);
- model.put("request", RequestTemplateModel.from(request, pathTemplate));
+ final Map model = templateEngine.buildModelForRequest(serveEvent);
model.putAll(addExtraModelElements(request, responseDefinition, files, parameters));
- model.putAll(additionalModelData);
if (responseDefinition.specifiesTextBodyContent()) {
boolean isJsonBody = responseDefinition.getReponseBody().isJson();
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplateEngine.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplateEngine.java
index 06d9468a64..458c5672ad 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplateEngine.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplateEngine.java
@@ -15,6 +15,8 @@
*/
package com.github.tomakehurst.wiremock.extension.responsetemplating;
+import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull;
+import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import com.github.jknack.handlebars.EscapingStrategy;
@@ -25,13 +27,22 @@
import com.github.jknack.handlebars.helper.NumberHelper;
import com.github.jknack.handlebars.helper.StringHelpers;
import com.github.tomakehurst.wiremock.common.Exceptions;
+import com.github.tomakehurst.wiremock.common.ListOrSingle;
+import com.github.tomakehurst.wiremock.extension.Parameters;
+import com.github.tomakehurst.wiremock.extension.TemplateModelDataProviderExtension;
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.SystemValueHelper;
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.WireMockHelpers;
+import com.github.tomakehurst.wiremock.http.Body;
+import com.github.tomakehurst.wiremock.http.HttpHeader;
+import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.http.ResponseDefinition;
+import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import java.util.Map;
-import java.util.Set;
+import com.google.common.collect.Maps;
+import java.util.*;
import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
public class TemplateEngine {
@@ -39,20 +50,24 @@ public class TemplateEngine {
private final Cache