Browse Source
* soap-jakarta module like soap, but with jaxb-jakarta * require java 11 * add SOAP Jakarta and JAXB Jakarta to the feature list --------- Co-authored-by: Marvin Froeder <velo@users.noreply.github.com>pull/2106/head
Severin Kistler
1 year ago
committed by
GitHub
12 changed files with 1287 additions and 0 deletions
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
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"); |
||||
``` |
||||
|
||||
In certain situations the declarations on the SOAP envelope are not inherited by JAXB when reading the documents. This is particularly |
||||
troublesome when it is not possible to correct the XML at the source. |
||||
|
||||
To account for this situation, use the `useFirstChild` option on the `SOAPDecoder` builder. This will instruct JAX be to use `SOAPBody#getFirstChild()` |
||||
instead of `SOAPBody#extractContentAsDocument()`. This will allow users to supply a `package-info.java` to manage the element namespaces |
||||
explicitly and define what should occur if the namespace declarations are missing. |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!-- |
||||
|
||||
Copyright 2012-2023 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. |
||||
|
||||
--> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<parent> |
||||
<groupId>io.github.openfeign</groupId> |
||||
<artifactId>parent</artifactId> |
||||
<version>12.4-SNAPSHOT</version> |
||||
</parent> |
||||
|
||||
<artifactId>feign-soap-jakarta</artifactId> |
||||
<name>Feign SOAP Jakarta</name> |
||||
<description>Feign SOAP CoDec</description> |
||||
|
||||
<properties> |
||||
<main.java.version>11</main.java.version> |
||||
<maven.compiler.source>11</maven.compiler.source> |
||||
<maven.compiler.target>11</maven.compiler.target> |
||||
<main.basedir>${project.basedir}/..</main.basedir> |
||||
|
||||
<moditect.skip>true</moditect.skip> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>${project.groupId}</groupId> |
||||
<artifactId>feign-core</artifactId> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>${project.groupId}</groupId> |
||||
<artifactId>feign-jaxb-jakarta</artifactId> |
||||
<version>${project.version}</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>${project.groupId}</groupId> |
||||
<artifactId>feign-core</artifactId> |
||||
<type>test-jar</type> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>jakarta.xml.ws</groupId> |
||||
<artifactId>jakarta.xml.ws-api</artifactId> |
||||
<version>4.0.0</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>jakarta.xml.soap</groupId> |
||||
<artifactId>jakarta.xml.soap-api</artifactId> |
||||
<version>3.0.0</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>jakarta.xml.bind</groupId> |
||||
<artifactId>jakarta.xml.bind-api</artifactId> |
||||
<version>4.0.0</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.sun.xml.messaging.saaj</groupId> |
||||
<artifactId>saaj-impl</artifactId> |
||||
<version>3.0.2</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.sun.xml.bind</groupId> |
||||
<artifactId>jaxb-impl</artifactId> |
||||
<version>4.0.3</version> |
||||
<scope>runtime</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
|
||||
</project> |
@ -0,0 +1,179 @@
@@ -0,0 +1,179 @@
|
||||
/* |
||||
* Copyright 2012-2023 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 feign.Response; |
||||
import feign.Util; |
||||
import feign.codec.DecodeException; |
||||
import feign.codec.Decoder; |
||||
import feign.jaxb.JAXBContextFactory; |
||||
import jakarta.xml.bind.JAXBException; |
||||
import jakarta.xml.bind.Unmarshaller; |
||||
import jakarta.xml.soap.*; |
||||
import jakarta.xml.ws.soap.SOAPFaultException; |
||||
import java.io.IOException; |
||||
import java.lang.reflect.ParameterizedType; |
||||
import java.lang.reflect.Type; |
||||
|
||||
/** |
||||
* Decodes SOAP responses using SOAPMessage and JAXB for the body part. <br> |
||||
* |
||||
* <p> |
||||
* The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. |
||||
* |
||||
* <p> |
||||
* 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. |
||||
* |
||||
* <pre> |
||||
* |
||||
* 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()); |
||||
* } |
||||
* </pre> |
||||
* |
||||
* @see SOAPErrorDecoder |
||||
* @see SOAPFaultException |
||||
*/ |
||||
public class SOAPDecoder implements Decoder { |
||||
|
||||
private final JAXBContextFactory jaxbContextFactory; |
||||
private final String soapProtocol; |
||||
private final boolean useFirstChild; |
||||
|
||||
public SOAPDecoder(JAXBContextFactory jaxbContextFactory) { |
||||
this.jaxbContextFactory = jaxbContextFactory; |
||||
this.soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL; |
||||
this.useFirstChild = false; |
||||
} |
||||
|
||||
private SOAPDecoder(Builder builder) { |
||||
this.soapProtocol = builder.soapProtocol; |
||||
this.jaxbContextFactory = builder.jaxbContextFactory; |
||||
this.useFirstChild = builder.useFirstChild; |
||||
} |
||||
|
||||
@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()); |
||||
} |
||||
|
||||
Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class<?>) type); |
||||
|
||||
if (this.useFirstChild) { |
||||
return unmarshaller.unmarshal(message.getSOAPBody().getFirstChild()); |
||||
} else { |
||||
return unmarshaller.unmarshal(message.getSOAPBody().extractContentAsDocument()); |
||||
} |
||||
} |
||||
} catch (SOAPException | JAXBException e) { |
||||
throw new DecodeException(response.status(), e.toString(), response.request(), 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; |
||||
boolean useFirstChild = false; |
||||
|
||||
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; |
||||
} |
||||
|
||||
/** |
||||
* Alters the behavior of the code to use the {@link SOAPBody#getFirstChild()} in place of |
||||
* {@link SOAPBody#extractContentAsDocument()}. |
||||
* |
||||
* @return the builder instance. |
||||
*/ |
||||
public Builder useFirstChild() { |
||||
this.useFirstChild = true; |
||||
return this; |
||||
} |
||||
|
||||
public SOAPDecoder build() { |
||||
if (jaxbContextFactory == null) { |
||||
throw new IllegalStateException("JAXBContextFactory must be non-null"); |
||||
} |
||||
return new SOAPDecoder(this); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,224 @@
@@ -0,0 +1,224 @@
|
||||
/* |
||||
* Copyright 2012-2023 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 feign.RequestTemplate; |
||||
import feign.codec.EncodeException; |
||||
import feign.codec.Encoder; |
||||
import feign.jaxb.JAXBContextFactory; |
||||
import jakarta.xml.bind.JAXBException; |
||||
import jakarta.xml.bind.Marshaller; |
||||
import jakarta.xml.soap.MessageFactory; |
||||
import jakarta.xml.soap.SOAPConstants; |
||||
import jakarta.xml.soap.SOAPException; |
||||
import jakarta.xml.soap.SOAPMessage; |
||||
import org.w3c.dom.Document; |
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.lang.reflect.Type; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import javax.xml.parsers.DocumentBuilderFactory; |
||||
import javax.xml.parsers.ParserConfigurationException; |
||||
import javax.xml.transform.*; |
||||
import javax.xml.transform.dom.DOMSource; |
||||
import javax.xml.transform.stream.StreamResult; |
||||
|
||||
/** |
||||
* Encodes requests using SOAPMessage and JAXB for the body part. <br> |
||||
* |
||||
* <p> |
||||
* Basic example with Feign.Builder: |
||||
* |
||||
* <pre> |
||||
* |
||||
* 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()); |
||||
* } |
||||
* </pre> |
||||
* |
||||
* <p> |
||||
* 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; |
||||
|
||||
public 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 = StandardCharsets.UTF_8; |
||||
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); |
||||
|
||||
soapMessage = modifySOAPMessage(soapMessage); |
||||
|
||||
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(bos.toString()); |
||||
} catch (SOAPException |
||||
| JAXBException |
||||
| ParserConfigurationException |
||||
| IOException |
||||
| TransformerFactoryConfigurationError |
||||
| TransformerException e) { |
||||
throw new EncodeException(e.toString(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Override this in order to modify the SOAP message object before it's finally encoded. <br> |
||||
* This might be useful to add SOAP Headers, which are not supported by this SOAPEncoder directly. |
||||
* <br> |
||||
* This is an example of how to add a security header: <code> |
||||
* protected SOAPMessage modifySOAPMessage(SOAPMessage soapMessage) throws SOAPException { |
||||
* SOAPFactory soapFactory = SOAPFactory.newInstance(); |
||||
* String uri = "http://schemas.xmlsoap.org/ws/2002/12/secext"; |
||||
* String prefix = "wss"; |
||||
* SOAPElement security = soapFactory.createElement("Security", prefix, uri); |
||||
* SOAPElement usernameToken = soapFactory.createElement("UsernameToken", prefix, uri); |
||||
* usernameToken.addChildElement("Username", prefix, uri).setValue("test"); |
||||
* usernameToken.addChildElement("Password", prefix, uri).setValue("test"); |
||||
* security.addChildElement(usernameToken); |
||||
* soapMessage.getSOAPHeader().addChildElement(security); |
||||
* return soapMessage; |
||||
* } |
||||
* </code> |
||||
*/ |
||||
protected SOAPMessage modifySOAPMessage(SOAPMessage soapMessage) throws SOAPException { |
||||
// Intentionally blank
|
||||
return soapMessage; |
||||
} |
||||
|
||||
/** Creates instances of {@link SOAPEncoder}. */ |
||||
public static class Builder { |
||||
|
||||
public boolean formattedOutput = false; |
||||
private JAXBContextFactory jaxbContextFactory; |
||||
private boolean writeXmlDeclaration = true; |
||||
private Charset charsetEncoding = StandardCharsets.UTF_8; |
||||
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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
/* |
||||
* Copyright 2012-2023 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 feign.Response; |
||||
import feign.codec.ErrorDecoder; |
||||
import jakarta.xml.soap.*; |
||||
import jakarta.xml.ws.soap.SOAPFaultException; |
||||
import java.io.IOException; |
||||
|
||||
/** |
||||
* 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}. |
||||
* |
||||
* <p> |
||||
* 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); |
||||
} |
||||
} |
@ -0,0 +1,485 @@
@@ -0,0 +1,485 @@
|
||||
/* |
||||
* Copyright 2012-2023 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.junit.Assert.assertEquals; |
||||
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 jakarta.xml.bind.annotation.*; |
||||
import jakarta.xml.soap.SOAPElement; |
||||
import jakarta.xml.soap.SOAPException; |
||||
import jakarta.xml.soap.SOAPFactory; |
||||
import jakarta.xml.soap.SOAPMessage; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.ExpectedException; |
||||
import java.lang.reflect.Type; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
public class SOAPCodecTest { |
||||
|
||||
@Rule |
||||
public final ExpectedException thrown = ExpectedException.none(); |
||||
|
||||
@Test |
||||
public void encodesSoap() { |
||||
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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + |
||||
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" + |
||||
"<SOAP-ENV:Header/>" + |
||||
"<SOAP-ENV:Body>" + |
||||
"<GetPrice>" + |
||||
"<Item>Apples</Item>" + |
||||
"</GetPrice>" + |
||||
"</SOAP-ENV:Body>" + |
||||
"</SOAP-ENV:Envelope>"; |
||||
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<java.lang.String, ?>"); |
||||
|
||||
class ParameterizedHolder { |
||||
|
||||
@SuppressWarnings("unused") |
||||
Map<String, ?> 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() { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); |
||||
|
||||
Encoder encoder = new SOAPEncoder.Builder() |
||||
// .withWriteXmlDeclaration(true)
|
||||
.withJAXBContextFactory(jaxbContextFactory) |
||||
.withCharsetEncoding(StandardCharsets.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 = "<?xml version=\"1.0\" encoding=\"UTF-16\" ?>" + |
||||
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" + |
||||
"<SOAP-ENV:Header/>" + |
||||
"<SOAP-ENV:Body>" + |
||||
"<GetPrice>" + |
||||
"<Item>Apples</Item>" + |
||||
"</GetPrice>" + |
||||
"</SOAP-ENV:Body>" + |
||||
"</SOAP-ENV:Envelope>"; |
||||
byte[] utf16Bytes = soapEnvelop.getBytes(StandardCharsets.UTF_16LE); |
||||
assertThat(template).hasBody(utf16Bytes); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void encodesSoapWithCustomJAXBSchemaLocation() { |
||||
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("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ "<SOAP-ENV:Header/>" |
||||
+ "<SOAP-ENV:Body>" |
||||
+ "<GetPrice xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://apihost http://apihost/schema.xsd\">" |
||||
+ "<Item>Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</SOAP-ENV:Body>" |
||||
+ "</SOAP-ENV:Envelope>"); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void encodesSoapWithCustomJAXBNoSchemaLocation() { |
||||
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("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ "<SOAP-ENV:Header/>" |
||||
+ "<SOAP-ENV:Body>" |
||||
+ "<GetPrice xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\">" |
||||
+ "<Item>Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</SOAP-ENV:Body>" |
||||
+ "</SOAP-ENV:Envelope>"); |
||||
} |
||||
|
||||
@Test |
||||
public void encodesSoapWithCustomJAXBFormattedOuput() { |
||||
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( |
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + System.lineSeparator() + |
||||
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ System.lineSeparator() + |
||||
" <SOAP-ENV:Header/>" + System.lineSeparator() + |
||||
" <SOAP-ENV:Body>" + System.lineSeparator() + |
||||
" <GetPrice>" + System.lineSeparator() + |
||||
" <Item>Apples</Item>" + System.lineSeparator() + |
||||
" </GetPrice>" + System.lineSeparator() + |
||||
" </SOAP-ENV:Body>" + System.lineSeparator() + |
||||
"</SOAP-ENV:Envelope>" + System.lineSeparator() + |
||||
""); |
||||
} |
||||
|
||||
@Test |
||||
public void decodesSoap() throws Exception { |
||||
GetPrice mock = new GetPrice(); |
||||
mock.item = new Item(); |
||||
mock.item.value = "Apples"; |
||||
|
||||
String mockSoapEnvelop = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ "<SOAP-ENV:Header/>" |
||||
+ "<SOAP-ENV:Body>" |
||||
+ "<GetPrice xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\">" |
||||
+ "<Item>Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</SOAP-ENV:Body>" |
||||
+ "</SOAP-ENV:Envelope>"; |
||||
|
||||
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 decodesSoapWithSchemaOnEnvelope() throws Exception { |
||||
GetPrice mock = new GetPrice(); |
||||
mock.item = new Item(); |
||||
mock.item.value = "Apples"; |
||||
|
||||
String mockSoapEnvelop = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" " |
||||
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\" " |
||||
+ "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" |
||||
+ "<SOAP-ENV:Header/>" |
||||
+ "<SOAP-ENV:Body>" |
||||
+ "<GetPrice>" |
||||
+ "<Item xsi:type=\"xsd:string\">Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</SOAP-ENV:Body>" |
||||
+ "</SOAP-ENV:Envelope>"; |
||||
|
||||
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.Builder() |
||||
.withJAXBContextFactory(new JAXBContextFactory.Builder().build()) |
||||
.useFirstChild() |
||||
.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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ "<SOAP-ENV:Header/>" |
||||
+ "<SOAP-ENV:Body>" |
||||
+ "<GetPrice xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\">" |
||||
+ "<Item>Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</SOAP-ENV:Body>" |
||||
+ "</SOAP-ENV:Envelope>"; |
||||
|
||||
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<String, ?> 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("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" |
||||
+ "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
||||
+ "<Header/>" |
||||
+ "<Body>" |
||||
+ "<GetPrice>" |
||||
+ "<Item>Apples</Item>" |
||||
+ "</GetPrice>" |
||||
+ "</Body>" |
||||
+ "</Envelope>", UTF_8) |
||||
.build(); |
||||
|
||||
new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeAnnotatedParameterizedTypes() throws Exception { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); |
||||
|
||||
Encoder encoder = new SOAPEncoder(jaxbContextFactory); |
||||
|
||||
Box<String> boxStr = new Box<>(); |
||||
boxStr.set("hello"); |
||||
Box<Box<String>> 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#dismiss404()} |
||||
*/ |
||||
@Test |
||||
public void notFoundDecodesToNull() 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 SOAPDecoder(new JAXBContextFactory.Builder().build()) |
||||
.decode(response, byte[].class)).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void changeSoapProtocolAndSetHeader() { |
||||
Encoder encoder = |
||||
new ChangedProtocolAndHeaderSOAPEncoder(new JAXBContextFactory.Builder().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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + |
||||
"<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">" + |
||||
"<env:Header>" + |
||||
(System.getProperty("java.version").startsWith("1.8") |
||||
? "<wss:Security xmlns:wss=\"http://schemas.xmlsoap.org/ws/2002/12/secext\">" |
||||
: "<wss:Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:wss=\"http://schemas.xmlsoap.org/ws/2002/12/secext\">") |
||||
+ |
||||
"<wss:UsernameToken>" + |
||||
"<wss:Username>test</wss:Username>" + |
||||
"<wss:Password>test</wss:Password>" + |
||||
"</wss:UsernameToken>" + |
||||
"</wss:Security>" + |
||||
"</env:Header>" + |
||||
"<env:Body>" + |
||||
"<GetPrice>" + |
||||
"<Item>Apples</Item>" + |
||||
"</GetPrice>" + |
||||
"</env:Body>" + |
||||
"</env:Envelope>"; |
||||
assertThat(template).hasBody(soapEnvelop); |
||||
} |
||||
|
||||
@XmlRootElement |
||||
static class Box<T> { |
||||
|
||||
@XmlElement |
||||
private T t; |
||||
|
||||
public void set(T t) { |
||||
this.t = t; |
||||
} |
||||
|
||||
} |
||||
|
||||
static class ChangedProtocolAndHeaderSOAPEncoder extends SOAPEncoder { |
||||
|
||||
public ChangedProtocolAndHeaderSOAPEncoder(JAXBContextFactory jaxbContextFactory) { |
||||
super(new SOAPEncoder.Builder() |
||||
.withSOAPProtocol("SOAP 1.2 Protocol") |
||||
.withJAXBContextFactory(jaxbContextFactory)); |
||||
} |
||||
|
||||
@Override |
||||
protected SOAPMessage modifySOAPMessage(SOAPMessage soapMessage) throws SOAPException { |
||||
SOAPFactory soapFactory = SOAPFactory.newInstance(); |
||||
String uri = "http://schemas.xmlsoap.org/ws/2002/12/secext"; |
||||
String prefix = "wss"; |
||||
SOAPElement security = soapFactory.createElement("Security", prefix, uri); |
||||
SOAPElement usernameToken = soapFactory.createElement("UsernameToken", prefix, uri); |
||||
usernameToken.addChildElement("Username", prefix, uri).setValue("test"); |
||||
usernameToken.addChildElement("Password", prefix, uri).setValue("test"); |
||||
security.addChildElement(usernameToken); |
||||
soapMessage.getSOAPHeader().addChildElement(security); |
||||
return soapMessage; |
||||
} |
||||
} |
||||
|
||||
@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; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2012-2023 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 feign.FeignException; |
||||
import feign.Request; |
||||
import feign.Request.HttpMethod; |
||||
import feign.Response; |
||||
import feign.Util; |
||||
import feign.jaxb.JAXBContextFactory; |
||||
import jakarta.xml.soap.SOAPConstants; |
||||
import jakarta.xml.ws.soap.SOAPFaultException; |
||||
import org.assertj.core.api.Assertions; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.ExpectedException; |
||||
import java.io.DataInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.util.Collections; |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
public class SOAPFaultDecoderTest { |
||||
|
||||
@Rule |
||||
public final ExpectedException thrown = ExpectedException.none(); |
||||
|
||||
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; |
||||
} |
||||
|
||||
@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( |
||||
"[503 Service Unavailable] during [GET] to [/api] [Service#foo()]: [Service Unavailable]"); |
||||
} |
||||
|
||||
@Test |
||||
public void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException { |
||||
String responseBody = "<?xml version = '1.0' encoding = 'UTF-8'?>\n" + |
||||
"<SOAP-ENV:Envelope\n" + |
||||
" xmlns:SOAP-ENV = \"http://schemas.xmlsoap.org/soap/envelope/\"\n" + |
||||
" xmlns:xsi = \"http://www.w3.org/1999/XMLSchema-instance\"\n" + |
||||
" xmlns:xsd = \"http://www.w3.org/1999/XMLSchema\">\n" + |
||||
" <SOAP-ENV:Body>\n" + |
||||
" </SOAP-ENV:Body>\n" + |
||||
"</SOAP-ENV:Envelope>"; |
||||
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(responseBody, UTF_8) |
||||
.build(); |
||||
|
||||
Exception error = |
||||
new SOAPErrorDecoder().decode("Service#foo()", response); |
||||
|
||||
Assertions.assertThat(error).isInstanceOf(FeignException.class) |
||||
.hasMessage("[500 Internal Server Error] during [GET] to [/api] [Service#foo()]: [" |
||||
+ responseBody + "]"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
/* |
||||
* Copyright 2012-2023 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. |
||||
*/ |
||||
@XmlSchema(elementFormDefault = XmlNsForm.UNQUALIFIED) |
||||
package feign.soap; |
||||
|
||||
import jakarta.xml.bind.annotation.XmlNsForm; |
||||
import jakarta.xml.bind.annotation.XmlSchema; |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
<?xml version = '1.0' encoding = 'UTF-8'?> |
||||
<SOAP-ENV:Envelope |
||||
xmlns:SOAP-ENV = "http://schemas.xmlsoap.org/soap/envelope/" |
||||
xmlns:xsi = "http://www.w3.org/1999/XMLSchema-instance" |
||||
xmlns:xsd = "http://www.w3.org/1999/XMLSchema"> |
||||
|
||||
<SOAP-ENV:Body> |
||||
<SOAP-ENV:Fault> |
||||
<faultcode xsi:type = "xsd:string">SOAP-ENV:Client</faultcode> |
||||
<faultstring xsi:type = "xsd:string">Message was not SOAP 1.1 compliant</faultstring> |
||||
</SOAP-ENV:Fault> |
||||
</SOAP-ENV:Body> |
||||
</SOAP-ENV:Envelope> |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0"?> |
||||
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"> |
||||
<env:Body> |
||||
<env:Fault> |
||||
<env:Code> |
||||
<env:Value>env:Sender</env:Value> |
||||
<env:Subcode> |
||||
<env:Value>rpc:BadArguments</env:Value> |
||||
</env:Subcode> |
||||
</env:Code> |
||||
<env:Reason> |
||||
<env:Text xml:lang="en-US">Processing error</env:Text> |
||||
</env:Reason> |
||||
<env:Detail> |
||||
<e:myFaultDetails |
||||
xmlns:e="http://travelcompany.example.org/faults"> |
||||
<e:message>Name does not match card number</e:message> |
||||
<e:errorcode>999</e:errorcode> |
||||
</e:myFaultDetails> |
||||
</env:Detail> |
||||
</env:Fault> |
||||
</env:Body> |
||||
</env:Envelope> |
Loading…
Reference in new issue