Skip to content

Commit b86efe0

Browse files
authored
Add support for SOAP Envelope declarations to be inherited (OpenFeign#1141)
Fixes OpenFeign#1127 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 support this a new `useFirstChild` option has been added to the `SOAPDecoder` builder that will 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.
1 parent 819b2df commit b86efe0

File tree

3 files changed

+88
-12
lines changed

3 files changed

+88
-12
lines changed

soap/src/main/java/feign/soap/SOAPDecoder.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import java.lang.reflect.ParameterizedType;
1818
import java.lang.reflect.Type;
1919
import javax.xml.bind.JAXBException;
20+
import javax.xml.bind.Unmarshaller;
2021
import javax.xml.soap.MessageFactory;
22+
import javax.xml.soap.SOAPBody;
2123
import javax.xml.soap.SOAPConstants;
2224
import javax.xml.soap.SOAPException;
2325
import javax.xml.soap.SOAPMessage;
@@ -84,15 +86,18 @@ public class SOAPDecoder implements Decoder {
8486

8587
private final JAXBContextFactory jaxbContextFactory;
8688
private final String soapProtocol;
89+
private final boolean useFirstChild;
8790

8891
public SOAPDecoder(JAXBContextFactory jaxbContextFactory) {
8992
this.jaxbContextFactory = jaxbContextFactory;
9093
this.soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL;
94+
this.useFirstChild = false;
9195
}
9296

9397
private SOAPDecoder(Builder builder) {
9498
this.soapProtocol = builder.soapProtocol;
9599
this.jaxbContextFactory = builder.jaxbContextFactory;
100+
this.useFirstChild = builder.useFirstChild;
96101
}
97102

98103
@Override
@@ -119,8 +124,13 @@ public Object decode(Response response, Type type) throws IOException {
119124
throw new SOAPFaultException(message.getSOAPBody().getFault());
120125
}
121126

122-
return jaxbContextFactory.createUnmarshaller((Class<?>) type)
123-
.unmarshal(message.getSOAPBody().extractContentAsDocument());
127+
Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class<?>) type);
128+
129+
if (this.useFirstChild) {
130+
return unmarshaller.unmarshal(message.getSOAPBody().getFirstChild());
131+
} else {
132+
return unmarshaller.unmarshal(message.getSOAPBody().extractContentAsDocument());
133+
}
124134
}
125135
} catch (SOAPException | JAXBException e) {
126136
throw new DecodeException(response.status(), e.toString(), response.request(), e);
@@ -137,6 +147,7 @@ public Object decode(Response response, Type type) throws IOException {
137147
public static class Builder {
138148
String soapProtocol = SOAPConstants.DEFAULT_SOAP_PROTOCOL;
139149
JAXBContextFactory jaxbContextFactory;
150+
boolean useFirstChild = false;
140151

141152
public Builder withJAXBContextFactory(JAXBContextFactory jaxbContextFactory) {
142153
this.jaxbContextFactory = jaxbContextFactory;
@@ -157,6 +168,17 @@ public Builder withSOAPProtocol(String soapProtocol) {
157168
return this;
158169
}
159170

171+
/**
172+
* Alters the behavior of the code to use the {@link SOAPBody#getFirstChild()} in place of
173+
* {@link SOAPBody#extractContentAsDocument()}.
174+
*
175+
* @return the builder instance.
176+
*/
177+
public Builder useFirstChild() {
178+
this.useFirstChild = true;
179+
return this;
180+
}
181+
160182
public SOAPDecoder build() {
161183
if (jaxbContextFactory == null) {
162184
throw new IllegalStateException("JAXBContextFactory must be non-null");

soap/src/test/java/feign/soap/SOAPCodecTest.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.junit.Assert.assertEquals;
1919
import java.lang.reflect.Type;
2020
import java.nio.charset.Charset;
21+
import java.nio.charset.StandardCharsets;
2122
import java.util.Collection;
2223
import java.util.Collections;
2324
import java.util.Map;
@@ -45,7 +46,7 @@ public class SOAPCodecTest {
4546
public final ExpectedException thrown = ExpectedException.none();
4647

4748
@Test
48-
public void encodesSoap() throws Exception {
49+
public void encodesSoap() {
4950
Encoder encoder = new SOAPEncoder.Builder()
5051
.withJAXBContextFactory(new JAXBContextFactory.Builder().build())
5152
.build();
@@ -89,14 +90,14 @@ class ParameterizedHolder {
8990

9091

9192
@Test
92-
public void encodesSoapWithCustomJAXBMarshallerEncoding() throws Exception {
93+
public void encodesSoapWithCustomJAXBMarshallerEncoding() {
9394
JAXBContextFactory jaxbContextFactory =
9495
new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build();
9596

9697
Encoder encoder = new SOAPEncoder.Builder()
9798
// .withWriteXmlDeclaration(true)
9899
.withJAXBContextFactory(jaxbContextFactory)
99-
.withCharsetEncoding(Charset.forName("UTF-16"))
100+
.withCharsetEncoding(StandardCharsets.UTF_16)
100101
.build();
101102

102103
GetPrice mock = new GetPrice();
@@ -115,13 +116,13 @@ public void encodesSoapWithCustomJAXBMarshallerEncoding() throws Exception {
115116
"</GetPrice>" +
116117
"</SOAP-ENV:Body>" +
117118
"</SOAP-ENV:Envelope>";
118-
byte[] utf16Bytes = soapEnvelop.getBytes("UTF-16LE");
119+
byte[] utf16Bytes = soapEnvelop.getBytes(StandardCharsets.UTF_16LE);
119120
assertThat(template).hasBody(utf16Bytes);
120121
}
121122

122123

123124
@Test
124-
public void encodesSoapWithCustomJAXBSchemaLocation() throws Exception {
125+
public void encodesSoapWithCustomJAXBSchemaLocation() {
125126
JAXBContextFactory jaxbContextFactory =
126127
new JAXBContextFactory.Builder()
127128
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
@@ -149,7 +150,7 @@ public void encodesSoapWithCustomJAXBSchemaLocation() throws Exception {
149150

150151

151152
@Test
152-
public void encodesSoapWithCustomJAXBNoSchemaLocation() throws Exception {
153+
public void encodesSoapWithCustomJAXBNoSchemaLocation() {
153154
JAXBContextFactory jaxbContextFactory =
154155
new JAXBContextFactory.Builder()
155156
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
@@ -176,7 +177,7 @@ public void encodesSoapWithCustomJAXBNoSchemaLocation() throws Exception {
176177
}
177178

178179
@Test
179-
public void encodesSoapWithCustomJAXBFormattedOuput() throws Exception {
180+
public void encodesSoapWithCustomJAXBFormattedOuput() {
180181
Encoder encoder = new SOAPEncoder.Builder().withFormattedOutput(true)
181182
.withJAXBContextFactory(new JAXBContextFactory.Builder()
182183
.build())
@@ -232,6 +233,40 @@ public void decodesSoap() throws Exception {
232233
assertEquals(mock, decoder.decode(response, GetPrice.class));
233234
}
234235

236+
@Test
237+
public void decodesSoapWithSchemaOnEnvelope() throws Exception {
238+
GetPrice mock = new GetPrice();
239+
mock.item = new Item();
240+
mock.item.value = "Apples";
241+
242+
String mockSoapEnvelop = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
243+
+ "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" "
244+
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\" "
245+
+ "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
246+
+ "<SOAP-ENV:Header/>"
247+
+ "<SOAP-ENV:Body>"
248+
+ "<GetPrice>"
249+
+ "<Item xsi:type=\"xsd:string\">Apples</Item>"
250+
+ "</GetPrice>"
251+
+ "</SOAP-ENV:Body>"
252+
+ "</SOAP-ENV:Envelope>";
253+
254+
Response response = Response.builder()
255+
.status(200)
256+
.reason("OK")
257+
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
258+
.headers(Collections.emptyMap())
259+
.body(mockSoapEnvelop, UTF_8)
260+
.build();
261+
262+
SOAPDecoder decoder = new SOAPDecoder.Builder()
263+
.withJAXBContextFactory(new JAXBContextFactory.Builder().build())
264+
.useFirstChild()
265+
.build();
266+
267+
assertEquals(mock, decoder.decode(response, GetPrice.class));
268+
}
269+
235270
@Test
236271
public void decodesSoap1_2Protocol() throws Exception {
237272
GetPrice mock = new GetPrice();
@@ -281,7 +316,7 @@ class ParameterizedHolder {
281316
.status(200)
282317
.reason("OK")
283318
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
284-
.headers(Collections.<String, Collection<String>>emptyMap())
319+
.headers(Collections.emptyMap())
285320
.body("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
286321
+ "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">"
287322
+ "<Header/>"
@@ -326,7 +361,7 @@ public void decodeAnnotatedParameterizedTypes() throws Exception {
326361
.status(200)
327362
.reason("OK")
328363
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
329-
.headers(Collections.<String, Collection<String>>emptyMap())
364+
.headers(Collections.emptyMap())
330365
.body(template.body())
331366
.build();
332367

@@ -343,7 +378,7 @@ public void notFoundDecodesToNull() throws Exception {
343378
.status(404)
344379
.reason("NOT FOUND")
345380
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8))
346-
.headers(Collections.<String, Collection<String>>emptyMap())
381+
.headers(Collections.emptyMap())
347382
.build();
348383
assertThat((byte[]) new JAXBDecoder(new JAXBContextFactory.Builder().build())
349384
.decode(response, byte[].class)).isNull();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright 2012-2019 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
@XmlSchema(
15+
elementFormDefault = XmlNsForm.UNQUALIFIED)
16+
package feign.soap;
17+
18+
import javax.xml.bind.annotation.XmlNsForm;
19+
import javax.xml.bind.annotation.XmlSchema;

0 commit comments

Comments
 (0)