From 1cf88516e4a61e40ff749ef58672c2d77399b2d6 Mon Sep 17 00:00:00 2001
From: Pierre Lakreb
Date: Tue, 11 Sep 2018 18:51:28 +0200
Subject: [PATCH] Adding SOAP CoDec (+ JAXB modifications)
---
README.md | 22 +
.../java/feign/jaxb/JAXBContextFactory.java | 41 +-
.../src/main/java/feign/jaxb/JAXBDecoder.java | 17 +-
.../src/main/java/feign/jaxb/JAXBEncoder.java | 2 +-
pom.xml | 5 +
soap/README.md | 50 +++
soap/pom.xml | 89 ++++
.../src/main/java/feign/soap/SOAPDecoder.java | 168 ++++++++
.../src/main/java/feign/soap/SOAPEncoder.java | 199 +++++++++
.../java/feign/soap/SOAPErrorDecoder.java | 80 ++++
.../services/javax.xml.soap.SAAJMetaFactory | 1 +
.../test/java/feign/soap/SOAPCodecTest.java | 395 ++++++++++++++++++
.../java/feign/soap/SOAPFaultDecoderTest.java | 122 ++++++
.../test/resources/samples/SOAP_1_1_FAULT.xml | 13 +
.../test/resources/samples/SOAP_1_2_FAULT.xml | 23 +
15 files changed, 1200 insertions(+), 27 deletions(-)
create mode 100644 soap/README.md
create mode 100644 soap/pom.xml
create mode 100644 soap/src/main/java/feign/soap/SOAPDecoder.java
create mode 100644 soap/src/main/java/feign/soap/SOAPEncoder.java
create mode 100644 soap/src/main/java/feign/soap/SOAPErrorDecoder.java
create mode 100644 soap/src/main/resources/services/javax.xml.soap.SAAJMetaFactory
create mode 100644 soap/src/test/java/feign/soap/SOAPCodecTest.java
create mode 100644 soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java
create mode 100644 soap/src/test/resources/samples/SOAP_1_1_FAULT.xml
create mode 100644 soap/src/test/resources/samples/SOAP_1_2_FAULT.xml
diff --git a/README.md b/README.md
index 182e5d5781..8d8c994da8 100644
--- a/README.md
+++ b/README.md
@@ -342,6 +342,28 @@ public class Example {
}
```
+### SOAP
+[SOAP](./soap) includes an encoder and decoder you can use with an XML API.
+
+
+This module adds support for encoding and decoding SOAP Body objects via JAXB and SOAPMessage. It also provides SOAPFault decoding capabilities by wrapping them into the original `javax.xml.ws.soap.SOAPFaultException`, so that you'll only need to catch `SOAPFaultException` in order to handle SOAPFault.
+
+Add `SOAPEncoder` and/or `SOAPDecoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ Api api = Feign.builder()
+ .encoder(new SOAPEncoder(jaxbFactory))
+ .decoder(new SOAPDecoder(jaxbFactory))
+ .errorDecoder(new SOAPErrorDecoder())
+ .target(MyApi.class, "http://api");
+ }
+}
+```
+
+NB: you may also need to add `SOAPErrorDecoder` if SOAP Faults are returned in response with error http codes (4xx, 5xx, ...)
+
### SLF4J
[SLF4JModule](./slf4j) allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.)
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
index 61254e5812..e1e30db767 100644
--- a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
+++ b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
@@ -14,9 +14,9 @@
package feign.jaxb;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@@ -31,8 +31,8 @@
*/
public final class JAXBContextFactory {
- private final ConcurrentHashMap jaxbContexts =
- new ConcurrentHashMap(64);
+ private final ConcurrentHashMap, JAXBContext> jaxbContexts =
+ new ConcurrentHashMap<>(64);
private final Map properties;
private JAXBContextFactory(Map properties) {
@@ -43,26 +43,21 @@ private JAXBContextFactory(Map properties) {
* Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class.
*/
public Unmarshaller createUnmarshaller(Class> clazz) throws JAXBException {
- JAXBContext ctx = getContext(clazz);
- return ctx.createUnmarshaller();
+ return getContext(clazz).createUnmarshaller();
}
/**
* Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class.
*/
public Marshaller createMarshaller(Class> clazz) throws JAXBException {
- JAXBContext ctx = getContext(clazz);
- Marshaller marshaller = ctx.createMarshaller();
+ Marshaller marshaller = getContext(clazz).createMarshaller();
setMarshallerProperties(marshaller);
return marshaller;
}
private void setMarshallerProperties(Marshaller marshaller) throws PropertyException {
- Iterator keys = properties.keySet().iterator();
-
- while (keys.hasNext()) {
- String key = keys.next();
- marshaller.setProperty(key, properties.get(key));
+ for (Entry en : properties.entrySet()) {
+ marshaller.setProperty(en.getKey(), en.getValue());
}
}
@@ -90,11 +85,11 @@ private void preloadContextCache(List> classes) throws JAXBException {
}
/**
- * Creates instances of {@link feign.jaxb.JAXBContextFactory}
+ * Creates instances of {@link feign.jaxb.JAXBContextFactory}.
*/
public static class Builder {
- private final Map properties = new HashMap(5);
+ private final Map properties = new HashMap<>(10);
/**
* Sets the jaxb.encoding property of any Marshaller created by this factory.
@@ -136,6 +131,24 @@ public Builder withMarshallerFragment(Boolean value) {
return this;
}
+ /**
+ * Sets the given property of any Marshaller created by this factory.
+ *
+ *
+ * Example :
+ *
+ *
+ * new JAXBContextFactory.Builder()
+ * .withProperty("com.sun.xml.internal.bind.xmlHeaders", "<!DOCTYPE Example SYSTEM \"example.dtd\">")
+ * .build();
+ *
+ *
+ */
+ public Builder withProperty(String key, Object value) {
+ properties.put(key, value);
+ return this;
+ }
+
/**
* Creates a new {@link feign.jaxb.JAXBContextFactory} instance with a lazy loading cached
* context
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
index aa0a6a66fb..4485abcb6c 100644
--- a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
+++ b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
@@ -14,13 +14,11 @@
package feign.jaxb;
import java.io.IOException;
-import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
-import javax.xml.transform.Source;
import javax.xml.transform.sax.SAXSource;
import feign.Response;
import feign.Util;
@@ -90,15 +88,10 @@ public Object decode(Response response, Type type) throws IOException {
false);
saxParserFactory.setNamespaceAware(namespaceAware);
- Source source = new SAXSource(saxParserFactory.newSAXParser().getXMLReader(),
- new InputSource(response.body().asInputStream()));
- Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type);
- return unmarshaller.unmarshal(source);
- } catch (JAXBException e) {
- throw new DecodeException(e.toString(), e);
- } catch (ParserConfigurationException e) {
- throw new DecodeException(e.toString(), e);
- } catch (SAXException e) {
+ return jaxbContextFactory.createUnmarshaller((Class>) type).unmarshal(new SAXSource(
+ saxParserFactory.newSAXParser().getXMLReader(),
+ new InputSource(response.body().asInputStream())));
+ } catch (JAXBException | ParserConfigurationException | SAXException e) {
throw new DecodeException(e.toString(), e);
} finally {
if (response.body() != null) {
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java
index 5bb30eac19..c4d8f79047 100644
--- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java
+++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java
@@ -56,7 +56,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) {
"JAXB only supports encoding raw types. Found " + bodyType);
}
try {
- Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType);
+ Marshaller marshaller = jaxbContextFactory.createMarshaller((Class>) bodyType);
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
template.body(stringWriter.toString());
diff --git a/pom.xml b/pom.xml
index 1e00e02021..ad557a398a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,7 @@
ribbon
sax
slf4j
+ soap
reactive
example-github
example-wikipedia
@@ -52,6 +53,9 @@
UTF-8
UTF-8
+
+ -Duser.language=en
+
1.8
java18
@@ -317,6 +321,7 @@
true
false
+ ${jvm.options}
diff --git a/soap/README.md b/soap/README.md
new file mode 100644
index 0000000000..c6e804b96c
--- /dev/null
+++ b/soap/README.md
@@ -0,0 +1,50 @@
+SOAP Codec
+===================
+
+This module adds support for encoding and decoding SOAP Body objects via JAXB and SOAPMessage. It also provides SOAPFault decoding capabilities by wrapping them into the original `javax.xml.ws.soap.SOAPFaultException`, so that you'll only need to catch `SOAPFaultException` in order to handle SOAPFault.
+
+Add `SOAPEncoder` and/or `SOAPDecoder` to your `Feign.Builder` like so:
+
+```java
+public interface MyApi {
+
+ @RequestLine("POST /getObject")
+ @Headers({
+ "SOAPAction: getObject",
+ "Content-Type: text/xml"
+ })
+ MyJaxbObjectResponse getObject(MyJaxbObjectRequest request);
+
+ }
+
+ ...
+
+ JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ .withMarshallerJAXBEncoding("UTF-8")
+ .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ .build();
+
+ api = Feign.builder()
+ .encoder(new SOAPEncoder(jaxbFactory))
+ .decoder(new SOAPDecoder(jaxbFactory))
+ .target(MyApi.class, "http://api");
+
+ ...
+
+ try {
+ api.getObject(new MyJaxbObjectRequest());
+ } catch (SOAPFaultException faultException) {
+ log.info(faultException.getFault().getFaultString());
+ }
+
+```
+
+Because a SOAP Fault can be returned as well with a 200 http code than a 4xx or 5xx HTTP error code (depending on the used API), you may also use `SOAPErrorDecoder` in your API configuration, in order to be able to catch `SOAPFaultException` in case of SOAP Fault. Add it, like below:
+
+```java
+api = Feign.builder()
+ .encoder(new SOAPEncoder(jaxbFactory))
+ .decoder(new SOAPDecoder(jaxbFactory))
+ .errorDecoder(new SOAPErrorDecoder())
+ .target(MyApi.class, "http://api");
+```
\ No newline at end of file
diff --git a/soap/pom.xml b/soap/pom.xml
new file mode 100644
index 0000000000..b534b2634d
--- /dev/null
+++ b/soap/pom.xml
@@ -0,0 +1,89 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 10.0.2-SNAPSHOT
+
+
+ feign-soap
+ Feign SOAP
+ Feign SOAP CoDec
+
+
+ ${project.basedir}/..
+
+
+
+
+ ${project.groupId}
+ feign-core
+
+
+
+ ${project.groupId}
+ feign-jaxb
+
+
+
+ ${project.groupId}
+ feign-core
+ test-jar
+ test
+
+
+
+
+
+
+ 11
+
+
+
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.3.1
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+ 2.4.0-b180830.0438
+ test
+
+
+ javax.xml.ws
+ jaxws-api
+ 2.3.1
+
+
+ com.sun.xml.messaging.saaj
+ saaj-impl
+ 1.5.0
+
+
+
+
+
+
diff --git a/soap/src/main/java/feign/soap/SOAPDecoder.java b/soap/src/main/java/feign/soap/SOAPDecoder.java
new file mode 100644
index 0000000000..bfb19237e0
--- /dev/null
+++ b/soap/src/main/java/feign/soap/SOAPDecoder.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright 2012-2018 The Feign Authors
+ *
+ * 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 feign.soap;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import javax.xml.bind.JAXBException;
+import javax.xml.soap.MessageFactory;
+import javax.xml.soap.SOAPConstants;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+import javax.xml.ws.soap.SOAPFaultException;
+import feign.Response;
+import feign.Util;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+import feign.jaxb.JAXBContextFactory;
+
+/**
+ * Decodes SOAP responses using SOAPMessage and JAXB for the body part.
+ *
+ *
+ * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts.
+ *
+ *
+ *
+ * A SOAP Fault can be returned with a 200 HTTP code. Hence, faults could be handled with no error
+ * on the HTTP layer. In this case, you'll certainly have to catch {@link SOAPFaultException} to get
+ * fault from your API client service. In the other case (Faults are returned with 4xx or 5xx HTTP
+ * error code), you may use {@link SOAPErrorDecoder} in your API configuration.
+ *
+ *
+ *
+ * public interface MyApi {
+ *
+ * @RequestLine("POST /getObject")
+ * @Headers({
+ * "SOAPAction: getObject",
+ * "Content-Type: text/xml"
+ * })
+ * MyJaxbObjectResponse getObject(MyJaxbObjectRequest request);
+ *
+ * }
+ *
+ * ...
+ *
+ * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ * .withMarshallerJAXBEncoding("UTF-8")
+ * .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ * .build();
+ *
+ * api = Feign.builder()
+ * .decoder(new SOAPDecoder(jaxbFactory))
+ * .target(MyApi.class, "http://api");
+ *
+ * ...
+ *
+ * try {
+ * api.getObject(new MyJaxbObjectRequest());
+ * } catch (SOAPFaultException faultException) {
+ * log.info(faultException.getFault().getFaultString());
+ * }
+ *
+ *
+ *
+ *
+ * @see SOAPErrorDecoder
+ * @see SOAPFaultException
+ */
+public class SOAPDecoder implements Decoder {
+
+
+ private final JAXBContextFactory jaxbContextFactory;
+ private final String soapProtocol;
+
+ public SOAPDecoder(JAXBContextFactory jaxbContextFactory) {
+ this.jaxbContextFactory = jaxbContextFactory;
+ this.soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL;
+ }
+
+ private SOAPDecoder(Builder builder) {
+ this.soapProtocol = builder.soapProtocol;
+ this.jaxbContextFactory = builder.jaxbContextFactory;
+ }
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ if (response.status() == 404)
+ return Util.emptyValueOf(type);
+ if (response.body() == null)
+ return null;
+ while (type instanceof ParameterizedType) {
+ ParameterizedType ptype = (ParameterizedType) type;
+ type = ptype.getRawType();
+ }
+ if (!(type instanceof Class)) {
+ throw new UnsupportedOperationException(
+ "SOAP only supports decoding raw types. Found " + type);
+ }
+
+ try {
+ SOAPMessage message =
+ MessageFactory.newInstance(soapProtocol).createMessage(null,
+ response.body().asInputStream());
+ if (message.getSOAPBody() != null) {
+ if (message.getSOAPBody().hasFault()) {
+ throw new SOAPFaultException(message.getSOAPBody().getFault());
+ }
+
+ return jaxbContextFactory.createUnmarshaller((Class>) type)
+ .unmarshal(message.getSOAPBody().extractContentAsDocument());
+ }
+ } catch (SOAPException | JAXBException e) {
+ throw new DecodeException(e.toString(), e);
+ } finally {
+ if (response.body() != null) {
+ response.body().close();
+ }
+ }
+ return Util.emptyValueOf(type);
+
+ }
+
+
+ public static class Builder {
+ String soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL;
+ JAXBContextFactory jaxbContextFactory;
+
+ public Builder withJAXBContextFactory(JAXBContextFactory jaxbContextFactory) {
+ this.jaxbContextFactory = jaxbContextFactory;
+ return this;
+ }
+
+ /**
+ * The protocol used to create message factory. Default is "SOAP 1.1 Protocol".
+ *
+ * @param soapProtocol a string constant representing the MessageFactory protocol.
+ * @see SOAPConstants#SOAP_1_1_PROTOCOL
+ * @see SOAPConstants#SOAP_1_2_PROTOCOL
+ * @see SOAPConstants#DYNAMIC_SOAP_PROTOCOL
+ * @see MessageFactory#newInstance(String)
+ */
+ public Builder withSOAPProtocol(String soapProtocol) {
+ this.soapProtocol = soapProtocol;
+ return this;
+ }
+
+ public SOAPDecoder build() {
+ if (jaxbContextFactory == null) {
+ throw new IllegalStateException("JAXBContextFactory must be non-null");
+ }
+ return new SOAPDecoder(this);
+ }
+ }
+
+}
diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java
new file mode 100644
index 0000000000..c67fce2a1f
--- /dev/null
+++ b/soap/src/main/java/feign/soap/SOAPEncoder.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright 2012-2018 The Feign Authors
+ *
+ * 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 feign.soap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.soap.MessageFactory;
+import javax.xml.soap.SOAPConstants;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import feign.RequestTemplate;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import feign.jaxb.JAXBContextFactory;
+
+
+/**
+ * Encodes requests using SOAPMessage and JAXB for the body part.
+ *
+ * Basic example with with Feign.Builder:
+ *
+ *
+ *
+ *
+ * public interface MyApi {
+ *
+ * @RequestLine("POST /getObject")
+ * @Headers({
+ * "SOAPAction: getObject",
+ * "Content-Type: text/xml"
+ * })
+ * MyJaxbObjectResponse getObject(MyJaxbObjectRequest request);
+ *
+ * }
+ *
+ * ...
+ *
+ * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ * .withMarshallerJAXBEncoding("UTF-8")
+ * .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ * .build();
+ *
+ * api = Feign.builder()
+ * .encoder(new SOAPEncoder(jaxbFactory))
+ * .target(MyApi.class, "http://api");
+ *
+ * ...
+ *
+ * try {
+ * api.getObject(new MyJaxbObjectRequest());
+ * } catch (SOAPFaultException faultException) {
+ * log.info(faultException.getFault().getFaultString());
+ * }
+ *
+ *
+ *
+ * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts.
+ *
+ */
+public class SOAPEncoder implements Encoder {
+
+ private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL;
+
+ private final boolean writeXmlDeclaration;
+ private final boolean formattedOutput;
+ private final Charset charsetEncoding;
+ private final JAXBContextFactory jaxbContextFactory;
+ private final String soapProtocol;
+
+ private SOAPEncoder(Builder builder) {
+ this.jaxbContextFactory = builder.jaxbContextFactory;
+ this.writeXmlDeclaration = builder.writeXmlDeclaration;
+ this.charsetEncoding = builder.charsetEncoding;
+ this.soapProtocol = builder.soapProtocol;
+ this.formattedOutput = builder.formattedOutput;
+ }
+
+ public SOAPEncoder(JAXBContextFactory jaxbContextFactory) {
+ this.jaxbContextFactory = jaxbContextFactory;
+ this.writeXmlDeclaration = true;
+ this.formattedOutput = false;
+ this.charsetEncoding = Charset.defaultCharset();
+ this.soapProtocol = DEFAULT_SOAP_PROTOCOL;
+ }
+
+ @Override
+ public void encode(Object object, Type bodyType, RequestTemplate template) {
+ if (!(bodyType instanceof Class)) {
+ throw new UnsupportedOperationException(
+ "SOAP only supports encoding raw types. Found " + bodyType);
+ }
+ try {
+ Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ Marshaller marshaller = jaxbContextFactory.createMarshaller((Class>) bodyType);
+ marshaller.marshal(object, document);
+ SOAPMessage soapMessage = MessageFactory.newInstance(soapProtocol).createMessage();
+ soapMessage.setProperty(SOAPMessage.WRITE_XML_DECLARATION,
+ Boolean.toString(writeXmlDeclaration));
+ soapMessage.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, charsetEncoding.displayName());
+ soapMessage.getSOAPBody().addDocument(document);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ if (formattedOutput) {
+ Transformer t = TransformerFactory.newInstance().newTransformer();
+ t.setOutputProperty(OutputKeys.INDENT, "yes");
+ t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+ t.transform(new DOMSource(soapMessage.getSOAPPart()), new StreamResult(bos));
+ } else {
+ soapMessage.writeTo(bos);
+ }
+ template.body(new String(bos.toByteArray()));
+ } catch (SOAPException | JAXBException | ParserConfigurationException | IOException
+ | TransformerFactoryConfigurationError | TransformerException e) {
+ throw new EncodeException(e.toString(), e);
+ }
+ }
+
+ /**
+ * Creates instances of {@link SOAPEncoder}.
+ */
+ public static class Builder {
+
+ private JAXBContextFactory jaxbContextFactory;
+ public boolean formattedOutput = false;
+ private boolean writeXmlDeclaration = true;
+ private Charset charsetEncoding = Charset.defaultCharset();
+ private String soapProtocol = DEFAULT_SOAP_PROTOCOL;
+
+ /** The {@link JAXBContextFactory} for body part. */
+ public Builder withJAXBContextFactory(JAXBContextFactory jaxbContextFactory) {
+ this.jaxbContextFactory = jaxbContextFactory;
+ return this;
+ }
+
+ /** Output format indent if true. Default is false */
+ public Builder withFormattedOutput(boolean formattedOutput) {
+ this.formattedOutput = formattedOutput;
+ return this;
+ }
+
+ /** Write the xml declaration if true. Default is true */
+ public Builder withWriteXmlDeclaration(boolean writeXmlDeclaration) {
+ this.writeXmlDeclaration = writeXmlDeclaration;
+ return this;
+ }
+
+ /** Specify the charset encoding. Default is {@link Charset#defaultCharset()}. */
+ public Builder withCharsetEncoding(Charset charsetEncoding) {
+ this.charsetEncoding = charsetEncoding;
+ return this;
+ }
+
+ /**
+ * The protocol used to create message factory. Default is "SOAP 1.1 Protocol".
+ *
+ * @param soapProtocol a string constant representing the MessageFactory protocol.
+ *
+ * @see SOAPConstants#SOAP_1_1_PROTOCOL
+ * @see SOAPConstants#SOAP_1_2_PROTOCOL
+ * @see SOAPConstants#DYNAMIC_SOAP_PROTOCOL
+ * @see MessageFactory#newInstance(String)
+ */
+ public Builder withSOAPProtocol(String soapProtocol) {
+ this.soapProtocol = soapProtocol;
+ return this;
+ }
+
+ public SOAPEncoder build() {
+ if (jaxbContextFactory == null) {
+ throw new IllegalStateException("JAXBContextFactory must be non-null");
+ }
+ return new SOAPEncoder(this);
+ }
+ }
+}
diff --git a/soap/src/main/java/feign/soap/SOAPErrorDecoder.java b/soap/src/main/java/feign/soap/SOAPErrorDecoder.java
new file mode 100644
index 0000000000..af400042f3
--- /dev/null
+++ b/soap/src/main/java/feign/soap/SOAPErrorDecoder.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2012-2018 The Feign Authors
+ *
+ * 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 feign.soap;
+
+import java.io.IOException;
+import javax.xml.soap.MessageFactory;
+import javax.xml.soap.SOAPConstants;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPFault;
+import javax.xml.soap.SOAPMessage;
+import javax.xml.ws.soap.SOAPFaultException;
+import feign.Response;
+import feign.codec.ErrorDecoder;
+
+/**
+ * Wraps the returned {@link SOAPFault} if present into a {@link SOAPFaultException}. So you need to
+ * catch {@link SOAPFaultException} to retrieve the reason of the {@link SOAPFault}.
+ *
+ *
+ * If no faults is returned then the default {@link ErrorDecoder} is used to return exception and
+ * eventually retry the call.
+ *
+ *
+ */
+public class SOAPErrorDecoder implements ErrorDecoder {
+
+ private final String soapProtocol;
+
+ public SOAPErrorDecoder() {
+ this.soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL;
+ }
+
+ /**
+ * SOAPErrorDecoder constructor allowing you to specify the SOAP protocol.
+ *
+ * @param soapProtocol a string constant representing the MessageFactory protocol.
+ *
+ * @see SOAPConstants#SOAP_1_1_PROTOCOL
+ * @see SOAPConstants#SOAP_1_2_PROTOCOL
+ * @see SOAPConstants#DYNAMIC_SOAP_PROTOCOL
+ * @see MessageFactory#newInstance(String)
+ */
+ public SOAPErrorDecoder(String soapProtocol) {
+ this.soapProtocol = soapProtocol;
+ }
+
+ @Override
+ public Exception decode(String methodKey, Response response) {
+ if (response.body() == null || response.status() == 503)
+ return defaultErrorDecoder(methodKey, response);
+
+ SOAPMessage message;
+ try {
+ message = MessageFactory.newInstance(soapProtocol).createMessage(null,
+ response.body().asInputStream());
+ if (message.getSOAPBody() != null && message.getSOAPBody().hasFault()) {
+ return new SOAPFaultException(message.getSOAPBody().getFault());
+ }
+ } catch (SOAPException | IOException e) {
+ // ignored
+ }
+ return defaultErrorDecoder(methodKey, response);
+ }
+
+ private Exception defaultErrorDecoder(String methodKey, Response response) {
+ return new ErrorDecoder.Default().decode(methodKey, response);
+ }
+
+}
diff --git a/soap/src/main/resources/services/javax.xml.soap.SAAJMetaFactory b/soap/src/main/resources/services/javax.xml.soap.SAAJMetaFactory
new file mode 100644
index 0000000000..a09cd2b139
--- /dev/null
+++ b/soap/src/main/resources/services/javax.xml.soap.SAAJMetaFactory
@@ -0,0 +1 @@
+com.sun.xml.messaging.saaj.soap.SAAJMetaFactoryImpl
diff --git a/soap/src/test/java/feign/soap/SOAPCodecTest.java b/soap/src/test/java/feign/soap/SOAPCodecTest.java
new file mode 100644
index 0000000000..0cca8cddea
--- /dev/null
+++ b/soap/src/test/java/feign/soap/SOAPCodecTest.java
@@ -0,0 +1,395 @@
+/**
+ * Copyright 2012-2018 The Feign Authors
+ *
+ * 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 feign.soap;
+
+import static feign.Util.UTF_8;
+import static feign.assertj.FeignAssertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlValue;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import feign.Request;
+import feign.Request.HttpMethod;
+import feign.RequestTemplate;
+import feign.Response;
+import feign.Util;
+import feign.codec.Encoder;
+import feign.jaxb.JAXBContextFactory;
+import feign.jaxb.JAXBDecoder;
+
+public class SOAPCodecTest {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void encodesSoap() throws Exception {
+ Encoder encoder = new SOAPEncoder.Builder()
+ .withJAXBContextFactory(new JAXBContextFactory.Builder().build())
+ .build();
+
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(mock, GetPrice.class, template);
+
+ String soapEnvelop = "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "- Apples
" +
+ "" +
+ "" +
+ "";
+ assertThat(template).hasBody(soapEnvelop);
+ }
+
+ @Test
+ public void doesntEncodeParameterizedTypes() throws Exception {
+ thrown.expect(UnsupportedOperationException.class);
+ thrown.expectMessage(
+ "SOAP only supports encoding raw types. Found java.util.Map");
+
+ class ParameterizedHolder {
+
+ @SuppressWarnings("unused")
+ Map field;
+ }
+ Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType();
+
+ RequestTemplate template = new RequestTemplate();
+ new SOAPEncoder(new JAXBContextFactory.Builder().build())
+ .encode(Collections.emptyMap(), parameterized, template);
+ }
+
+
+ @Test
+ public void encodesSoapWithCustomJAXBMarshallerEncoding() throws Exception {
+ JAXBContextFactory jaxbContextFactory =
+ new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build();
+
+ Encoder encoder = new SOAPEncoder.Builder()
+ // .withWriteXmlDeclaration(true)
+ .withJAXBContextFactory(jaxbContextFactory)
+ .withCharsetEncoding(Charset.forName("UTF-16"))
+ .build();
+
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(mock, GetPrice.class, template);
+
+ String soapEnvelop = "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "- Apples
" +
+ "" +
+ "" +
+ "";
+ byte[] utf16Bytes = soapEnvelop.getBytes("UTF-16LE");
+ assertThat(template).hasBody(utf16Bytes);
+ }
+
+
+ @Test
+ public void encodesSoapWithCustomJAXBSchemaLocation() throws Exception {
+ JAXBContextFactory jaxbContextFactory =
+ new JAXBContextFactory.Builder()
+ .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ .build();
+
+ Encoder encoder = new SOAPEncoder(jaxbContextFactory);
+
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(mock, GetPrice.class, template);
+
+ assertThat(template).hasBody(""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "- Apples
"
+ + ""
+ + ""
+ + "");
+ }
+
+
+ @Test
+ public void encodesSoapWithCustomJAXBNoSchemaLocation() throws Exception {
+ JAXBContextFactory jaxbContextFactory =
+ new JAXBContextFactory.Builder()
+ .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
+ .build();
+
+ Encoder encoder = new SOAPEncoder(jaxbContextFactory);
+
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(mock, GetPrice.class, template);
+
+ assertThat(template).hasBody(""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "- Apples
"
+ + ""
+ + ""
+ + "");
+ }
+
+ @Test
+ public void encodesSoapWithCustomJAXBFormattedOuput() throws Exception {
+ Encoder encoder = new SOAPEncoder.Builder().withFormattedOutput(true)
+ .withJAXBContextFactory(new JAXBContextFactory.Builder()
+ .build())
+ .build();
+
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(mock, GetPrice.class, template);
+
+ assertThat(template).hasBody("\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " - Apples
\n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ "");
+ }
+
+ @Test
+ public void decodesSoap() throws Exception {
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ String mockSoapEnvelop = ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "- Apples
"
+ + ""
+ + ""
+ + "";
+
+ Response response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body(mockSoapEnvelop, UTF_8)
+ .build();
+
+ SOAPDecoder decoder = new SOAPDecoder(new JAXBContextFactory.Builder().build());
+
+ assertEquals(mock, decoder.decode(response, GetPrice.class));
+ }
+
+ @Test
+ public void decodesSoap1_2Protocol() throws Exception {
+ GetPrice mock = new GetPrice();
+ mock.item = new Item();
+ mock.item.value = "Apples";
+
+ String mockSoapEnvelop = ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "- Apples
"
+ + ""
+ + ""
+ + "";
+
+ Response response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body(mockSoapEnvelop, UTF_8)
+ .build();
+
+ SOAPDecoder decoder = new SOAPDecoder(new JAXBContextFactory.Builder().build());
+
+ assertEquals(mock, decoder.decode(response, GetPrice.class));
+ }
+
+
+ @Test
+ public void doesntDecodeParameterizedTypes() throws Exception {
+ thrown.expect(feign.codec.DecodeException.class);
+ thrown.expectMessage(
+ "java.util.Map is an interface, and JAXB can't handle interfaces.\n"
+ + "\tthis problem is related to the following location:\n"
+ + "\t\tat java.util.Map");
+
+ class ParameterizedHolder {
+
+ @SuppressWarnings("unused")
+ Map field;
+ }
+ Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType();
+
+ Response response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.>emptyMap())
+ .body(""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "- Apples
"
+ + ""
+ + ""
+ + "", UTF_8)
+ .build();
+
+ new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized);
+ }
+
+ @XmlRootElement
+ static class Box {
+
+ @XmlElement
+ private T t;
+
+ public void set(T t) {
+ this.t = t;
+ }
+
+ }
+
+ @Test
+ public void decodeAnnotatedParameterizedTypes() throws Exception {
+ JAXBContextFactory jaxbContextFactory =
+ new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build();
+
+ Encoder encoder = new SOAPEncoder(jaxbContextFactory);
+
+ Box boxStr = new Box<>();
+ boxStr.set("hello");
+ Box> boxBoxStr = new Box<>();
+ boxBoxStr.set(boxStr);
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(boxBoxStr, Box.class, template);
+
+ Response response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.>emptyMap())
+ .body(template.body())
+ .build();
+
+ new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class);
+
+ }
+
+ /**
+ * Enabled via {@link feign.Feign.Builder#decode404()}
+ */
+ @Test
+ public void notFoundDecodesToEmpty() throws Exception {
+ Response response = Response.builder()
+ .status(404)
+ .reason("NOT FOUND")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.>emptyMap())
+ .build();
+ assertThat((byte[]) new JAXBDecoder(new JAXBContextFactory.Builder().build())
+ .decode(response, byte[].class)).isEmpty();
+ }
+
+
+ @XmlRootElement(name = "GetPrice")
+ @XmlAccessorType(XmlAccessType.FIELD)
+ static class GetPrice {
+
+ @XmlElement(name = "Item")
+ private Item item;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof GetPrice) {
+ GetPrice getPrice = (GetPrice) obj;
+ return item.value.equals(getPrice.item.value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return item.value != null ? item.value.hashCode() : 0;
+ }
+ }
+
+ @XmlRootElement(name = "Item")
+ @XmlAccessorType(XmlAccessType.FIELD)
+ static class Item {
+
+ @XmlValue
+ private String value;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Item) {
+ Item item = (Item) obj;
+ return value.equals(item.value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return value != null ? value.hashCode() : 0;
+ }
+ }
+
+}
diff --git a/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java b/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java
new file mode 100644
index 0000000000..04c57ec20e
--- /dev/null
+++ b/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright 2012-2018 The Feign Authors
+ *
+ * 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 feign.soap;
+
+import static feign.Util.UTF_8;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import javax.xml.soap.SOAPConstants;
+import javax.xml.ws.soap.SOAPFaultException;
+import org.assertj.core.api.Assertions;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import feign.FeignException;
+import feign.Request;
+import feign.Request.HttpMethod;
+import feign.Response;
+import feign.Util;
+import feign.jaxb.JAXBContextFactory;
+
+public class SOAPFaultDecoderTest {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void soapDecoderThrowsSOAPFaultException() throws IOException {
+
+ thrown.expect(SOAPFaultException.class);
+ thrown.expectMessage("Processing error");
+
+ Response response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body(getResourceBytes("/samples/SOAP_1_2_FAULT.xml"))
+ .build();
+
+ new SOAPDecoder.Builder().withSOAPProtocol(SOAPConstants.SOAP_1_2_PROTOCOL)
+ .withJAXBContextFactory(new JAXBContextFactory.Builder().build()).build()
+ .decode(response, Object.class);
+ }
+
+ @Test
+ public void errorDecoderReturnsSOAPFaultException() throws IOException {
+ Response response = Response.builder()
+ .status(400)
+ .reason("BAD REQUEST")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body(getResourceBytes("/samples/SOAP_1_1_FAULT.xml"))
+ .build();
+
+ Exception error =
+ new SOAPErrorDecoder().decode("Service#foo()", response);
+ Assertions.assertThat(error).isInstanceOf(SOAPFaultException.class)
+ .hasMessage("Message was not SOAP 1.1 compliant");
+ }
+
+ @Test
+ public void errorDecoderReturnsFeignExceptionOn503Status() throws IOException {
+ Response response = Response.builder()
+ .status(503)
+ .reason("Service Unavailable")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body("Service Unavailable", UTF_8)
+ .build();
+
+ Exception error =
+ new SOAPErrorDecoder().decode("Service#foo()", response);
+
+ Assertions.assertThat(error).isInstanceOf(FeignException.class)
+ .hasMessage("status 503 reading Service#foo()");
+ }
+
+ @Test
+ public void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException {
+ Response response = Response.builder()
+ .status(500)
+ .reason("Internal Server Error")
+ .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body("\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ "", UTF_8)
+ .build();
+
+ Exception error =
+ new SOAPErrorDecoder().decode("Service#foo()", response);
+
+ Assertions.assertThat(error).isInstanceOf(FeignException.class)
+ .hasMessage("status 500 reading Service#foo()");
+ }
+
+ private static byte[] getResourceBytes(String resourcePath) throws IOException {
+ InputStream resourceAsStream = SOAPFaultDecoderTest.class.getResourceAsStream(resourcePath);
+ byte[] bytes = new byte[resourceAsStream.available()];
+ new DataInputStream(resourceAsStream).readFully(bytes);
+ return bytes;
+ }
+
+}
diff --git a/soap/src/test/resources/samples/SOAP_1_1_FAULT.xml b/soap/src/test/resources/samples/SOAP_1_1_FAULT.xml
new file mode 100644
index 0000000000..5f7fe979fa
--- /dev/null
+++ b/soap/src/test/resources/samples/SOAP_1_1_FAULT.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ SOAP-ENV:Client
+ Message was not SOAP 1.1 compliant
+
+
+
\ No newline at end of file
diff --git a/soap/src/test/resources/samples/SOAP_1_2_FAULT.xml b/soap/src/test/resources/samples/SOAP_1_2_FAULT.xml
new file mode 100644
index 0000000000..0b39989e4a
--- /dev/null
+++ b/soap/src/test/resources/samples/SOAP_1_2_FAULT.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ env:Sender
+
+ rpc:BadArguments
+
+
+
+ Processing error
+
+
+
+ Name does not match card number
+ 999
+
+
+
+
+