/* * Copyright 2005-2014 the original author or 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 org.springframework.ws.soap.saaj; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import javax.xml.soap.MessageFactory; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPConstants; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xml.sax.SAXParseException; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.ws.InvalidXmlException; import org.springframework.ws.soap.SoapMessageCreationException; import org.springframework.ws.soap.SoapMessageFactory; import org.springframework.ws.soap.SoapVersion; import org.springframework.ws.soap.saaj.support.SaajUtils; import org.springframework.ws.transport.TransportConstants; import org.springframework.ws.transport.TransportInputStream; /** * SAAJ-specific implementation of the {@link org.springframework.ws.WebServiceMessageFactory WebServiceMessageFactory}. * Wraps a SAAJ {@link MessageFactory}. This factory will use SAAJ 1.3 when found, or fall back to SAAJ 1.2 or even * 1.1. * * <p>A SAAJ {@link MessageFactory} can be injected to the {@link #SaajSoapMessageFactory(javax.xml.soap.MessageFactory) * constructor}, or by the {@link #setMessageFactory(javax.xml.soap.MessageFactory)} property. When a SAAJ message * factory is injected, the {@link #setSoapVersion(org.springframework.ws.soap.SoapVersion)} property is ignored. * * @author Arjen Poutsma * @see org.springframework.ws.soap.saaj.SaajSoapMessage * @since 1.0.0 */ public class SaajSoapMessageFactory implements SoapMessageFactory, InitializingBean { private static final Log logger = LogFactory.getLog(SaajSoapMessageFactory.class); private MessageFactory messageFactory; private String messageFactoryProtocol; private boolean langAttributeOnSoap11FaultString = true; private Map<String, ?> messageProperties; /** Default, empty constructor. */ public SaajSoapMessageFactory() { } /** Constructor that takes a message factory as an argument. */ public SaajSoapMessageFactory(MessageFactory messageFactory) { this.messageFactory = messageFactory; } /** Returns the SAAJ {@code MessageFactory} used. */ public MessageFactory getMessageFactory() { return messageFactory; } /** Sets the SAAJ {@code MessageFactory}. */ public void setMessageFactory(MessageFactory messageFactory) { this.messageFactory = messageFactory; } /** * Sets the SAAJ message properties. These properties will be set on created messages. * @see javax.xml.soap.SOAPMessage#setProperty(String, Object) */ public void setMessageProperties(Map<String, ?> messageProperties) { this.messageProperties = messageProperties; } /** * Defines whether a {@code xml:lang} attribute should be set on SOAP 1.1 {@code <faultstring>} elements. * * <p>The default is {@code true}, to comply with WS-I, but this flag can be set to {@code false} to the older W3C SOAP * 1.1 specification. * * @see <a href="http://www.ws-i.org/Profiles/BasicProfile-1.1.html#SOAP_Fault_Language">WS-I Basic Profile 1.1</a> */ public void setLangAttributeOnSoap11FaultString(boolean langAttributeOnSoap11FaultString) { this.langAttributeOnSoap11FaultString = langAttributeOnSoap11FaultString; } @Override public void setSoapVersion(SoapVersion version) { if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) { if (SoapVersion.SOAP_11 == version) { messageFactoryProtocol = SOAPConstants.SOAP_1_1_PROTOCOL; } else if (SoapVersion.SOAP_12 == version) { messageFactoryProtocol = SOAPConstants.SOAP_1_2_PROTOCOL; } else { throw new IllegalArgumentException( "Invalid version [" + version + "]. Expected the SOAP_11 or SOAP_12 constant"); } } else if (SoapVersion.SOAP_11 != version) { throw new IllegalArgumentException("SAAJ 1.1 and 1.2 only support SOAP 1.1"); } } @Override public void afterPropertiesSet() { if (messageFactory == null) { try { if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) { if (!StringUtils.hasLength(messageFactoryProtocol)) { messageFactoryProtocol = SOAPConstants.SOAP_1_1_PROTOCOL; } if (logger.isInfoEnabled()) { logger.info("Creating SAAJ 1.3 MessageFactory with " + messageFactoryProtocol); } messageFactory = MessageFactory.newInstance(messageFactoryProtocol); } else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_12) { logger.info("Creating SAAJ 1.2 MessageFactory"); messageFactory = MessageFactory.newInstance(); } else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_11) { logger.info("Creating SAAJ 1.1 MessageFactory"); messageFactory = MessageFactory.newInstance(); } else { throw new IllegalStateException( "SaajSoapMessageFactory requires SAAJ 1.1, which was not found on the classpath"); } } catch (NoSuchMethodError ex) { throw new SoapMessageCreationException( "Could not create SAAJ MessageFactory. Is the version of the SAAJ specification interfaces [" + SaajUtils.getSaajVersionString() + "] the same as the version supported by the application server?", ex); } catch (SOAPException ex) { throw new SoapMessageCreationException("Could not create SAAJ MessageFactory: " + ex.getMessage(), ex); } } if (logger.isDebugEnabled()) { logger.debug("Using MessageFactory class [" + messageFactory.getClass().getName() + "]"); } } @Override public SaajSoapMessage createWebServiceMessage() { try { SOAPMessage saajMessage = messageFactory.createMessage(); postProcess(saajMessage); return new SaajSoapMessage(saajMessage, langAttributeOnSoap11FaultString, messageFactory); } catch (SOAPException ex) { throw new SoapMessageCreationException("Could not create empty message: " + ex.getMessage(), ex); } } @Override public SaajSoapMessage createWebServiceMessage(InputStream inputStream) throws IOException { MimeHeaders mimeHeaders = parseMimeHeaders(inputStream); try { inputStream = checkForUtf8ByteOrderMark(inputStream); SOAPMessage saajMessage = messageFactory.createMessage(mimeHeaders, inputStream); saajMessage.getSOAPPart().getEnvelope(); postProcess(saajMessage); return new SaajSoapMessage(saajMessage, langAttributeOnSoap11FaultString, messageFactory); } catch (SOAPException ex) { // SAAJ 1.3 RI has a issue with handling multipart XOP content types which contain "startinfo" rather than // "start-info", so let's try and do something about it String contentType = StringUtils .arrayToCommaDelimitedString(mimeHeaders.getHeader(TransportConstants.HEADER_CONTENT_TYPE)); if (contentType.contains("startinfo")) { contentType = contentType.replace("startinfo", "start-info"); mimeHeaders.setHeader(TransportConstants.HEADER_CONTENT_TYPE, contentType); try { SOAPMessage saajMessage = messageFactory.createMessage(mimeHeaders, inputStream); postProcess(saajMessage); return new SaajSoapMessage(saajMessage, langAttributeOnSoap11FaultString); } catch (SOAPException e) { // fall-through } } SAXParseException parseException = getSAXParseException(ex); if (parseException != null) { throw new InvalidXmlException("Could not parse XML", parseException); } else { throw new SoapMessageCreationException( "Could not create message from InputStream: " + ex.getMessage(), ex); } } } private SAXParseException getSAXParseException(Throwable ex) { if (ex instanceof SAXParseException) { return (SAXParseException) ex; } else if (ex.getCause() != null) { return getSAXParseException(ex.getCause()); } else { return null; } } private MimeHeaders parseMimeHeaders(InputStream inputStream) throws IOException { MimeHeaders mimeHeaders = new MimeHeaders(); if (inputStream instanceof TransportInputStream) { TransportInputStream transportInputStream = (TransportInputStream) inputStream; for (Iterator<String> headerNames = transportInputStream.getHeaderNames(); headerNames.hasNext();) { String headerName = headerNames.next(); for (Iterator<String> headerValues = transportInputStream.getHeaders(headerName); headerValues.hasNext();) { String headerValue = headerValues.next(); StringTokenizer tokenizer = new StringTokenizer(headerValue, ","); while (tokenizer.hasMoreTokens()) { mimeHeaders.addHeader(headerName, tokenizer.nextToken().trim()); } } } } return mimeHeaders; } /** * Checks for the UTF-8 Byte Order Mark, and removes it if present. The SAAJ RI cannot cope with these BOMs. * * @see <a href="http://jira.springframework.org/browse/SWS-393">SWS-393</a> * @see <a href="http://unicode.org/faq/utf_bom.html#22">UTF-8 BOMs</a> */ private InputStream checkForUtf8ByteOrderMark(InputStream inputStream) throws IOException { PushbackInputStream pushbackInputStream = new PushbackInputStream(new BufferedInputStream(inputStream), 3); byte[] bytes = new byte[3]; int bytesRead = 0; while (bytesRead < bytes.length) { int n = pushbackInputStream.read(bytes, bytesRead, bytes.length - bytesRead); if (n > 0) { bytesRead += n; } else { break; } } if (bytesRead > 0) { // check for the UTF-8 BOM, and remove it if there. See SWS-393 if (!isByteOrderMark(bytes)) { pushbackInputStream.unread(bytes, 0, bytesRead); } } return pushbackInputStream; } private boolean isByteOrderMark(byte[] bytes) { return bytes.length == 3 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF; } /** * Template method that allows for post-processing of the given {@link SOAPMessage}. * <p>Default implementation sets {@linkplain SOAPMessage#setProperty(String, Object) message properties}, if any. * @param soapMessage the message to post process * @see #setMessageProperties(java.util.Map) */ protected void postProcess(SOAPMessage soapMessage) throws SOAPException { if (!CollectionUtils.isEmpty(messageProperties)) { for (Map.Entry<String, ?> entry : messageProperties.entrySet()) { soapMessage.setProperty(entry.getKey(), entry.getValue()); } } } public String toString() { StringBuilder builder = new StringBuilder("SaajSoapMessageFactory["); builder.append(SaajUtils.getSaajVersionString()); if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) { builder.append(','); builder.append(messageFactoryProtocol); } builder.append(']'); return builder.toString(); } }