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
12 changed files with 1287 additions and 0 deletions
@ -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 @@ |
|||||||
|
<?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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
<?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 @@ |
|||||||
|
<?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