/** * Copyright 2012-2020 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package feign.soap; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.soap.MessageFactory; import javax.xml.soap.SOAPConstants; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; /** * Encodes requests using SOAPMessage and JAXB for the body part.
*

* Basic example with with Feign.Builder: *

* *
 * 
 * public interface MyApi {
 * 
 *    @RequestLine("POST /getObject")
 *    @Headers({
 *      "SOAPAction: getObject",
 *      "Content-Type: text/xml"
 *    })
 *    MyJaxbObjectResponse getObject(MyJaxbObjectRequest request);
 *    
 * }
 * 
 * ...
 * 
 * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
 *     .withMarshallerJAXBEncoding("UTF-8")
 *     .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
 *     .build();
 *
 * api = Feign.builder()
 *     .encoder(new SOAPEncoder(jaxbFactory))
 *     .target(MyApi.class, "http://api");
 *     
 * ...
 *
 * try {
 *    api.getObject(new MyJaxbObjectRequest());
 * } catch (SOAPFaultException faultException) {
 *    log.info(faultException.getFault().getFaultString());
 * }
 * 
* *

* The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. *

*/ public class SOAPEncoder implements Encoder { private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL; private final boolean writeXmlDeclaration; private final boolean formattedOutput; private final Charset charsetEncoding; private final JAXBContextFactory jaxbContextFactory; private final String soapProtocol; private SOAPEncoder(Builder builder) { this.jaxbContextFactory = builder.jaxbContextFactory; this.writeXmlDeclaration = builder.writeXmlDeclaration; this.charsetEncoding = builder.charsetEncoding; this.soapProtocol = builder.soapProtocol; this.formattedOutput = builder.formattedOutput; } public SOAPEncoder(JAXBContextFactory jaxbContextFactory) { this.jaxbContextFactory = jaxbContextFactory; this.writeXmlDeclaration = true; this.formattedOutput = false; this.charsetEncoding = 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); ByteArrayOutputStream bos = new ByteArrayOutputStream(); if (formattedOutput) { Transformer t = TransformerFactory.newInstance().newTransformer(); t.setOutputProperty(OutputKeys.INDENT, "yes"); t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); t.transform(new DOMSource(soapMessage.getSOAPPart()), new StreamResult(bos)); } else { soapMessage.writeTo(bos); } template.body(new String(bos.toByteArray())); } catch (SOAPException | JAXBException | ParserConfigurationException | IOException | TransformerFactoryConfigurationError | TransformerException e) { throw new EncodeException(e.toString(), e); } } /** * Creates instances of {@link SOAPEncoder}. */ public static class Builder { private JAXBContextFactory jaxbContextFactory; public boolean formattedOutput = false; private boolean writeXmlDeclaration = true; private Charset charsetEncoding = 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); } } }