View Javadoc

1   /*
2    * Copyright 2005-2012 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.ws.soap.saaj;
18  
19  import java.io.BufferedInputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.PushbackInputStream;
23  import java.util.Iterator;
24  import java.util.Map;
25  import java.util.StringTokenizer;
26  import javax.xml.soap.MessageFactory;
27  import javax.xml.soap.MimeHeaders;
28  import javax.xml.soap.SOAPConstants;
29  import javax.xml.soap.SOAPException;
30  import javax.xml.soap.SOAPMessage;
31  
32  import org.springframework.beans.factory.InitializingBean;
33  import org.springframework.util.CollectionUtils;
34  import org.springframework.util.StringUtils;
35  import org.springframework.ws.InvalidXmlException;
36  import org.springframework.ws.soap.SoapMessageCreationException;
37  import org.springframework.ws.soap.SoapMessageFactory;
38  import org.springframework.ws.soap.SoapVersion;
39  import org.springframework.ws.soap.saaj.support.SaajUtils;
40  import org.springframework.ws.transport.TransportConstants;
41  import org.springframework.ws.transport.TransportInputStream;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  import org.xml.sax.SAXParseException;
46  
47  /**
48   * SAAJ-specific implementation of the {@link org.springframework.ws.WebServiceMessageFactory WebServiceMessageFactory}.
49   * Wraps a SAAJ {@link MessageFactory}. This factory will use SAAJ 1.3 when found, or fall back to SAAJ 1.2 or even
50   * 1.1.
51   * <p/>
52   * A SAAJ {@link MessageFactory} can be injected to the {@link #SaajSoapMessageFactory(javax.xml.soap.MessageFactory)
53   * constructor}, or by the {@link #setMessageFactory(javax.xml.soap.MessageFactory)} property. When a SAAJ message
54   * factory is injected, the {@link #setSoapVersion(org.springframework.ws.soap.SoapVersion)} property is ignored.
55   *
56   * @author Arjen Poutsma
57   * @see org.springframework.ws.soap.saaj.SaajSoapMessage
58   * @since 1.0.0
59   */
60  public class SaajSoapMessageFactory implements SoapMessageFactory, InitializingBean {
61  
62      private static final Log logger = LogFactory.getLog(SaajSoapMessageFactory.class);
63  
64      private MessageFactory messageFactory;
65  
66      private String messageFactoryProtocol;
67  
68      private boolean langAttributeOnSoap11FaultString = true;
69  
70      private Map<String, ?> messageProperties;
71  
72      /** Default, empty constructor. */
73      public SaajSoapMessageFactory() {
74      }
75  
76      /** Constructor that takes a message factory as an argument. */
77      public SaajSoapMessageFactory(MessageFactory messageFactory) {
78          this.messageFactory = messageFactory;
79      }
80  
81      /** Returns the SAAJ <code>MessageFactory</code> used. */
82      public MessageFactory getMessageFactory() {
83          return messageFactory;
84      }
85  
86      /** Sets the SAAJ <code>MessageFactory</code>. */
87      public void setMessageFactory(MessageFactory messageFactory) {
88          this.messageFactory = messageFactory;
89      }
90  
91      /**
92       * Sets the SAAJ message properties. These properties will be set on created messages.
93       * @see javax.xml.soap.SOAPMessage#setProperty(String, Object)
94       */
95      public void setMessageProperties(Map<String, ?> messageProperties) {
96          this.messageProperties = messageProperties;
97      }
98  
99      /**
100      * Defines whether a {@code xml:lang} attribute should be set on SOAP 1.1 {@code <faultstring>} elements.
101      * <p/>
102      * The default is {@code true}, to comply with WS-I, but this flag can be set to {@code false} to the older W3C SOAP
103      * 1.1 specification.
104      *
105      * @see <a href="http://www.ws-i.org/Profiles/BasicProfile-1.1.html#SOAP_Fault_Language">WS-I Basic Profile 1.1</a>
106      */
107     public void setLangAttributeOnSoap11FaultString(boolean langAttributeOnSoap11FaultString) {
108         this.langAttributeOnSoap11FaultString = langAttributeOnSoap11FaultString;
109     }
110 
111     public void setSoapVersion(SoapVersion version) {
112         if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) {
113             if (SoapVersion.SOAP_11 == version) {
114                 messageFactoryProtocol = SOAPConstants.SOAP_1_1_PROTOCOL;
115             }
116             else if (SoapVersion.SOAP_12 == version) {
117                 messageFactoryProtocol = SOAPConstants.SOAP_1_2_PROTOCOL;
118             }
119             else {
120                 throw new IllegalArgumentException(
121                         "Invalid version [" + version + "]. Expected the SOAP_11 or SOAP_12 constant");
122             }
123         }
124         else if (SoapVersion.SOAP_11 != version) {
125             throw new IllegalArgumentException("SAAJ 1.1 and 1.2 only support SOAP 1.1");
126         }
127     }
128 
129     public void afterPropertiesSet() {
130         if (messageFactory == null) {
131             try {
132                 if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) {
133                     if (!StringUtils.hasLength(messageFactoryProtocol)) {
134                         messageFactoryProtocol = SOAPConstants.SOAP_1_1_PROTOCOL;
135                     }
136                     if (logger.isInfoEnabled()) {
137                         logger.info("Creating SAAJ 1.3 MessageFactory with " + messageFactoryProtocol);
138                     }
139                     messageFactory = MessageFactory.newInstance(messageFactoryProtocol);
140                 }
141                 else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_12) {
142                     logger.info("Creating SAAJ 1.2 MessageFactory");
143                     messageFactory = MessageFactory.newInstance();
144                 }
145                 else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_11) {
146                     logger.info("Creating SAAJ 1.1 MessageFactory");
147                     messageFactory = MessageFactory.newInstance();
148                 }
149                 else {
150                     throw new IllegalStateException(
151                             "SaajSoapMessageFactory requires SAAJ 1.1, which was not found on the classpath");
152                 }
153             }
154             catch (NoSuchMethodError ex) {
155                 throw new SoapMessageCreationException(
156                         "Could not create SAAJ MessageFactory. Is the version of the SAAJ specification interfaces [" +
157                                 SaajUtils.getSaajVersionString() +
158                                 "] the same as the version supported by the application server?", ex);
159             }
160             catch (SOAPException ex) {
161                 throw new SoapMessageCreationException("Could not create SAAJ MessageFactory: " + ex.getMessage(), ex);
162             }
163         }
164         if (logger.isDebugEnabled()) {
165             logger.debug("Using MessageFactory class [" + messageFactory.getClass().getName() + "]");
166         }
167     }
168 
169     public SaajSoapMessage createWebServiceMessage() {
170         try {
171             SOAPMessage saajMessage = messageFactory.createMessage();
172             postProcess(saajMessage);
173             return new SaajSoapMessage(saajMessage, langAttributeOnSoap11FaultString, messageFactory);
174         }
175         catch (SOAPException ex) {
176             throw new SoapMessageCreationException("Could not create empty message: " + ex.getMessage(), ex);
177         }
178     }
179 
180     public SaajSoapMessage createWebServiceMessage(InputStream inputStream) throws IOException {
181         MimeHeaders mimeHeaders = parseMimeHeaders(inputStream);
182         try {
183             inputStream = checkForUtf8ByteOrderMark(inputStream);
184             SOAPMessage saajMessage = messageFactory.createMessage(mimeHeaders, inputStream);
185             postProcess(saajMessage);
186             return new SaajSoapMessage(saajMessage, langAttributeOnSoap11FaultString, messageFactory);
187         }
188         catch (SOAPException ex) {
189             // SAAJ 1.3 RI has a issue with handling multipart XOP content types which contain "startinfo" rather than
190             // "start-info", so let's try and do something about it
191             String contentType = StringUtils
192                     .arrayToCommaDelimitedString(mimeHeaders.getHeader(TransportConstants.HEADER_CONTENT_TYPE));
193             if (contentType.contains("startinfo")) {
194                 contentType = contentType.replace("startinfo", "start-info");
195                 mimeHeaders.setHeader(TransportConstants.HEADER_CONTENT_TYPE, contentType);
196                 try {
197                     SOAPMessage saajMessage = messageFactory.createMessage(mimeHeaders, inputStream);
198                     postProcess(saajMessage);
199                     return new SaajSoapMessage(saajMessage,
200                             langAttributeOnSoap11FaultString);
201                 }
202                 catch (SOAPException e) {
203                     // fall-through
204                 }
205             }
206             throw new SoapMessageCreationException("Could not create message from InputStream: " + ex.getMessage(), ex);
207         } catch (SaajSoapEnvelopeException ex) {
208             SAXParseException parseException = getSAXParseException(ex);
209             if (parseException != null) {
210                 throw new InvalidXmlException("Could not parse XML", parseException);
211             } else {
212                 throw ex;
213             }
214         }
215     }
216 
217     private SAXParseException getSAXParseException(Throwable ex) {
218         if (ex instanceof SAXParseException) {
219             return (SAXParseException) ex;
220         } else if (ex.getCause() != null) {
221             return getSAXParseException(ex.getCause());
222         } else {
223             return null;
224         }
225     }
226 
227     private MimeHeaders parseMimeHeaders(InputStream inputStream) throws IOException {
228         MimeHeaders mimeHeaders = new MimeHeaders();
229         if (inputStream instanceof TransportInputStream) {
230             TransportInputStream transportInputStream = (TransportInputStream) inputStream;
231             for (Iterator<String> headerNames = transportInputStream.getHeaderNames(); headerNames.hasNext();) {
232                 String headerName = headerNames.next();
233                 for (Iterator<String> headerValues = transportInputStream.getHeaders(headerName); headerValues.hasNext();) {
234                     String headerValue = headerValues.next();
235                     StringTokenizer tokenizer = new StringTokenizer(headerValue, ",");
236                     while (tokenizer.hasMoreTokens()) {
237                         mimeHeaders.addHeader(headerName, tokenizer.nextToken().trim());
238                     }
239                 }
240             }
241         }
242         return mimeHeaders;
243     }
244 
245     /**
246      * Checks for the UTF-8 Byte Order Mark, and removes it if present. The SAAJ RI cannot cope with these BOMs.
247      *
248      * @see <a href="http://jira.springframework.org/browse/SWS-393">SWS-393</a>
249      * @see <a href="http://unicode.org/faq/utf_bom.html#22">UTF-8 BOMs</a>
250      */
251     private InputStream checkForUtf8ByteOrderMark(InputStream inputStream) throws IOException {
252         PushbackInputStream pushbackInputStream = new PushbackInputStream(new BufferedInputStream(inputStream), 3);
253         byte[] bytes = new byte[3];
254         int bytesRead = 0;
255 	    while (bytesRead < bytes.length) {
256 		    int n = pushbackInputStream.read(bytes, bytesRead, bytes.length - bytesRead);
257 		    if (n > 0) {
258 			    bytesRead += n;
259 		    } else {
260 			    break;
261 		    }
262 	    }
263         if (bytesRead > 0) {
264             // check for the UTF-8 BOM, and remove it if there. See SWS-393
265             if (!isByteOrderMark(bytes)) {
266                 pushbackInputStream.unread(bytes, 0, bytesRead);
267             }
268         }
269         return pushbackInputStream;
270     }
271 
272     private boolean isByteOrderMark(byte[] bytes) {
273         return bytes.length == 3 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF;
274     }
275 
276     /**
277      * Template method that allows for post-processing of the given {@link SOAPMessage}.
278      * <p>Default implementation sets {@linkplain SOAPMessage#setProperty(String, Object) message properties}, if any.
279      * @param soapMessage the message to post process
280      * @see #setMessageProperties(java.util.Map)
281      */
282     protected void postProcess(SOAPMessage soapMessage) throws SOAPException {
283         if (!CollectionUtils.isEmpty(messageProperties)) {
284             for (Map.Entry<String, ?> entry : messageProperties.entrySet()) {
285                 soapMessage.setProperty(entry.getKey(), entry.getValue());
286             }
287         }
288     }
289 
290     public String toString() {
291         StringBuilder builder = new StringBuilder("SaajSoapMessageFactory[");
292         builder.append(SaajUtils.getSaajVersionString());
293         if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) {
294             builder.append(',');
295             builder.append(messageFactoryProtocol);
296         }
297         builder.append(']');
298         return builder.toString();
299     }
300 }