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.axiom;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.util.Iterator;
23  import java.util.Locale;
24  import javax.xml.stream.XMLInputFactory;
25  import javax.xml.stream.XMLStreamException;
26  import javax.xml.stream.XMLStreamReader;
27  
28  import org.springframework.beans.factory.InitializingBean;
29  import org.springframework.util.Assert;
30  import org.springframework.util.StringUtils;
31  import org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor;
32  import org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping;
33  import org.springframework.ws.soap.SoapMessageFactory;
34  import org.springframework.ws.soap.SoapVersion;
35  import org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor;
36  import org.springframework.ws.soap.server.endpoint.mapping.SoapActionEndpointMapping;
37  import org.springframework.ws.soap.support.SoapUtils;
38  import org.springframework.ws.transport.TransportConstants;
39  import org.springframework.ws.transport.TransportInputStream;
40  
41  import org.apache.axiom.attachments.Attachments;
42  import org.apache.axiom.om.OMAbstractFactory;
43  import org.apache.axiom.om.OMException;
44  import org.apache.axiom.om.impl.MTOMConstants;
45  import org.apache.axiom.soap.SOAP11Constants;
46  import org.apache.axiom.soap.SOAP11Version;
47  import org.apache.axiom.soap.SOAP12Constants;
48  import org.apache.axiom.soap.SOAP12Version;
49  import org.apache.axiom.soap.SOAPFactory;
50  import org.apache.axiom.soap.SOAPMessage;
51  import org.apache.axiom.soap.impl.builder.MTOMStAXSOAPModelBuilder;
52  import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder;
53  import org.apache.commons.logging.Log;
54  import org.apache.commons.logging.LogFactory;
55  
56  /**
57   * Axiom-specific implementation of the {@link org.springframework.ws.WebServiceMessageFactory WebServiceMessageFactory}
58   * interface. Creates {@link org.springframework.ws.soap.axiom.AxiomSoapMessage AxiomSoapMessages}.
59   * <p/>
60   * To increase reading performance on the the SOAP request created by this message factory, you can set the {@link
61   * #setPayloadCaching(boolean) payloadCaching} property to <code>false</code> (default is <code>true</code>). This this
62   * will read the contents of the body directly from the stream. However, <strong>when this setting is enabled, the
63   * payload can only be read once</strong>. This means that any endpoint mappings or interceptors which are based on the
64   * message payload (such as the {@link PayloadRootQNameEndpointMapping}, the {@link PayloadValidatingInterceptor}, or
65   * the {@link PayloadLoggingInterceptor}) cannot be used. Instead, use an endpoint mapping that does not consume the
66   * payload (i.e. the {@link SoapActionEndpointMapping}).
67   * <p/>
68   * Additionally, this message factory can cache large attachments to disk by setting the {@link
69   * #setAttachmentCaching(boolean) attachmentCaching} property to <code>true</code> (default is <code>false</code>).
70   * Optionally, the location where attachments are stored can be defined via the {@link #setAttachmentCacheDir(File)
71   * attachmentCacheDir} property (defaults to the system temp file path).
72   * <p/>
73   * Mostly derived from <code>org.apache.axis2.transport.http.HTTPTransportUtils</code> and
74   * <code>org.apache.axis2.transport.TransportUtils</code>, which we cannot use since they are not part of the Axiom
75   * distribution.
76   *
77   * @author Arjen Poutsma
78   * @see AxiomSoapMessage
79   * @see #setPayloadCaching(boolean)
80   * @since 1.0.0
81   */
82  public class AxiomSoapMessageFactory implements SoapMessageFactory, InitializingBean {
83  
84      private static final String CHARSET_PARAMETER = "charset";
85  
86      private static final String DEFAULT_CHARSET_ENCODING = "UTF-8";
87  
88      private static final String MULTI_PART_RELATED_CONTENT_TYPE = "multipart/related";
89  
90      private static final Log logger = LogFactory.getLog(AxiomSoapMessageFactory.class);
91  
92      private final XMLInputFactory inputFactory = createXmlInputFactory();
93  
94      private boolean payloadCaching = true;
95  
96      private boolean attachmentCaching = false;
97  
98      private File attachmentCacheDir;
99  
100     private int attachmentCacheThreshold = 4096;
101 
102     // use SOAP 1.1 by default
103     private SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory();
104 
105     private boolean langAttributeOnSoap11FaultString = true;
106 
107 
108     /**
109      * Indicates whether the SOAP Body payload should be cached or not. Default is <code>true</code>.
110      * <p/>
111      * Setting this to <code>false</code> will increase performance, but also result in the fact that the message
112      * payload can only be read once.
113      */
114     public void setPayloadCaching(boolean payloadCaching) {
115         this.payloadCaching = payloadCaching;
116     }
117 
118     /**
119      * Indicates whether SOAP attachments should be cached or not. Default is <code>false</code>.
120      * <p/>
121      * Setting this to <code>true</code> will cause Axiom to store larger attachments on disk, rather than in memory.
122      * This decreases memory consumption, but decreases performance.
123      */
124     public void setAttachmentCaching(boolean attachmentCaching) {
125         this.attachmentCaching = attachmentCaching;
126     }
127 
128     /**
129      * Sets the directory where SOAP attachments will be stored. Only used when {@link #setAttachmentCaching(boolean)
130      * attachmentCaching} is set to <code>true</code>.
131      * <p/>
132      * The parameter should be an existing, writable directory. This property defaults to the temporary directory of the
133      * operating system (i.e. the value of the <code>java.io.tmpdir</code> system property).
134      */
135     public void setAttachmentCacheDir(File attachmentCacheDir) {
136         Assert.notNull(attachmentCacheDir, "'attachmentCacheDir' must not be null");
137         Assert.isTrue(attachmentCacheDir.isDirectory(), "'attachmentCacheDir' must be a directory");
138         Assert.isTrue(attachmentCacheDir.canWrite(), "'attachmentCacheDir' must be writable");
139         this.attachmentCacheDir = attachmentCacheDir;
140     }
141 
142     /**
143      * Sets the threshold for attachments caching, in bytes. Attachments larger than this threshold will be cached in
144      * the {@link #setAttachmentCacheDir(File) attachment cache directory}. Only used when {@link
145      * #setAttachmentCaching(boolean) attachmentCaching} is set to <code>true</code>.
146      * <p/>
147      * Defaults to 4096 bytes (i.e. 4 kilobytes).
148      */
149     public void setAttachmentCacheThreshold(int attachmentCacheThreshold) {
150         Assert.isTrue(attachmentCacheThreshold > 0, "'attachmentCacheThreshold' must be larger than 0");
151         this.attachmentCacheThreshold = attachmentCacheThreshold;
152     }
153 
154     public void setSoapVersion(SoapVersion version) {
155         if (SoapVersion.SOAP_11 == version) {
156             soapFactory = OMAbstractFactory.getSOAP11Factory();
157         }
158         else if (SoapVersion.SOAP_12 == version) {
159             soapFactory = OMAbstractFactory.getSOAP12Factory();
160         }
161         else {
162             throw new IllegalArgumentException(
163                     "Invalid version [" + version + "]. " + "Expected the SOAP_11 or SOAP_12 constant");
164         }
165     }
166 
167     /**
168      * Defines whether a {@code xml:lang} attribute should be set on SOAP 1.1 {@code <faultstring>} elements.
169      * <p/>
170      * The default is {@code true}, to comply with WS-I, but this flag can be set to {@code false} to the older W3C SOAP
171      * 1.1 specification.
172      *
173      * @see <a href="http://www.ws-i.org/Profiles/BasicProfile-1.1.html#SOAP_Fault_Language">WS-I Basic Profile 1.1</a>
174      */
175     public void setLangAttributeOnSoap11FaultString(boolean langAttributeOnSoap11FaultString) {
176         this.langAttributeOnSoap11FaultString = langAttributeOnSoap11FaultString;
177     }
178 
179     public void afterPropertiesSet() throws Exception {
180         if (logger.isInfoEnabled()) {
181             logger.info(payloadCaching ? "Enabled payload caching" : "Disabled payload caching");
182         }
183         if (attachmentCacheDir == null) {
184             String tempDir = System.getProperty("java.io.tmpdir");
185             setAttachmentCacheDir(new File(tempDir));
186         }
187     }
188 
189     public AxiomSoapMessage createWebServiceMessage() {
190         return new AxiomSoapMessage(soapFactory, payloadCaching, langAttributeOnSoap11FaultString);
191     }
192 
193     public AxiomSoapMessage createWebServiceMessage(InputStream inputStream) throws IOException {
194         Assert.isInstanceOf(TransportInputStream.class, inputStream,
195                 "AxiomSoapMessageFactory requires a TransportInputStream");
196         TransportInputStream transportInputStream = (TransportInputStream) inputStream;
197         String contentType = getHeaderValue(transportInputStream, TransportConstants.HEADER_CONTENT_TYPE);
198         if (!StringUtils.hasLength(contentType)) {
199             if (logger.isDebugEnabled()) {
200                 logger.debug("TransportInputStream has no Content-Type header; defaulting to \"" +
201                         SoapVersion.SOAP_11.getContentType() + "\"");
202             }
203             contentType = SoapVersion.SOAP_11.getContentType();
204         }
205         String soapAction = getHeaderValue(transportInputStream, TransportConstants.HEADER_SOAP_ACTION);
206         if (!StringUtils.hasLength(soapAction)) {
207             soapAction = SoapUtils.extractActionFromContentType(contentType);
208         }
209         try {
210             if (isMultiPartRelated(contentType)) {
211                 return createMultiPartAxiomSoapMessage(inputStream, contentType, soapAction);
212             }
213             else {
214                 return createAxiomSoapMessage(inputStream, contentType, soapAction);
215             }
216         }
217         catch (XMLStreamException ex) {
218             throw new AxiomSoapMessageCreationException("Could not parse request: " + ex.getMessage(), ex);
219         }
220         catch (OMException ex) {
221             throw new AxiomSoapMessageCreationException("Could not create message: " + ex.getMessage(), ex);
222         }
223     }
224 
225     private String getHeaderValue(TransportInputStream transportInputStream, String header) throws IOException {
226         String contentType = null;
227         Iterator<String> iterator = transportInputStream.getHeaders(header);
228         if (iterator.hasNext()) {
229             contentType = iterator.next();
230         }
231         return contentType;
232     }
233 
234     private boolean isMultiPartRelated(String contentType) {
235         contentType = contentType.toLowerCase(Locale.ENGLISH);
236         return contentType.contains(MULTI_PART_RELATED_CONTENT_TYPE);
237     }
238 
239     /** Creates an AxiomSoapMessage without attachments. */
240     private AxiomSoapMessage createAxiomSoapMessage(InputStream inputStream, String contentType, String soapAction)
241             throws XMLStreamException {
242         XMLStreamReader reader = inputFactory.createXMLStreamReader(inputStream, getCharSetEncoding(contentType));
243         String envelopeNamespace = getSoapEnvelopeNamespace(contentType);
244         StAXSOAPModelBuilder builder = new StAXSOAPModelBuilder(reader, soapFactory, envelopeNamespace);
245         SOAPMessage soapMessage = builder.getSoapMessage();
246         return new AxiomSoapMessage(soapMessage, soapAction, payloadCaching, langAttributeOnSoap11FaultString);
247     }
248 
249     /** Creates an AxiomSoapMessage with attachments. */
250     private AxiomSoapMessage createMultiPartAxiomSoapMessage(InputStream inputStream,
251                                                              String contentType,
252                                                              String soapAction) throws XMLStreamException {
253         Attachments attachments =
254                 new Attachments(inputStream, contentType, attachmentCaching, attachmentCacheDir.getAbsolutePath(),
255                         Integer.toString(attachmentCacheThreshold));
256         XMLStreamReader reader = inputFactory.createXMLStreamReader(attachments.getRootPartInputStream(),
257                 getCharSetEncoding(attachments.getRootPartContentType()));
258         StAXSOAPModelBuilder builder;
259         String envelopeNamespace = getSoapEnvelopeNamespace(contentType);
260         if (MTOMConstants.SWA_TYPE.equals(attachments.getAttachmentSpecType()) ||
261                 MTOMConstants.SWA_TYPE_12.equals(attachments.getAttachmentSpecType())) {
262             builder = new StAXSOAPModelBuilder(reader, soapFactory, envelopeNamespace);
263         }
264         else if (MTOMConstants.MTOM_TYPE.equals(attachments.getAttachmentSpecType())) {
265             builder = new MTOMStAXSOAPModelBuilder(reader, attachments, envelopeNamespace);
266         }
267         else {
268             throw new AxiomSoapMessageCreationException(
269                     "Unknown attachment type: [" + attachments.getAttachmentSpecType() + "]");
270         }
271         return new AxiomSoapMessage(builder.getSoapMessage(), attachments, soapAction, payloadCaching,
272                 langAttributeOnSoap11FaultString);
273     }
274 
275     private String getSoapEnvelopeNamespace(String contentType) {
276         if (contentType.contains(SOAP11Constants.SOAP_11_CONTENT_TYPE)) {
277             return SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI;
278         }
279         else if (contentType.contains(SOAP12Constants.SOAP_12_CONTENT_TYPE)) {
280             return SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI;
281         }
282         else {
283             throw new AxiomSoapMessageCreationException("Unknown content type '" + contentType + "'");
284         }
285 
286     }
287 
288     /**
289      * Returns the character set from the given content type. Mostly copied
290      *
291      * @return the character set encoding
292      */
293     protected String getCharSetEncoding(String contentType) {
294         int charSetIdx = contentType.indexOf(CHARSET_PARAMETER);
295         if (charSetIdx == -1) {
296             return DEFAULT_CHARSET_ENCODING;
297         }
298         int eqIdx = contentType.indexOf("=", charSetIdx);
299 
300         int indexOfSemiColon = contentType.indexOf(";", eqIdx);
301         String value;
302 
303         if (indexOfSemiColon > 0) {
304             value = contentType.substring(eqIdx + 1, indexOfSemiColon);
305         }
306         else {
307             value = contentType.substring(eqIdx + 1, contentType.length()).trim();
308         }
309         if (value.startsWith("\"")) {
310             value = value.substring(1);
311         }
312         if (value.endsWith("\"")) {
313             return value.substring(0, value.length() - 1);
314         }
315         if ("null".equalsIgnoreCase(value)) {
316             return DEFAULT_CHARSET_ENCODING;
317         }
318         else {
319             return value.trim();
320         }
321     }
322 
323     /**
324      * Create a {@code XMLInputFactory} that this resolver will use to create {@link XMLStreamReader} objects.
325      * <p/>
326      * Can be overridden in subclasses, adding further initialization of the factory. The resulting factory is cached,
327      * so this method will only be called once.
328      *
329      * @return the created factory
330      */
331     protected XMLInputFactory createXmlInputFactory() {
332         return XMLInputFactory.newInstance();
333     }
334 
335 
336     public String toString() {
337         StringBuilder builder = new StringBuilder("AxiomSoapMessageFactory[");
338         if (soapFactory.getSOAPVersion() == SOAP11Version.getSingleton()) {
339             builder.append("SOAP 1.1");
340         }
341         else if (soapFactory.getSOAPVersion() == SOAP12Version.getSingleton()) {
342             builder.append("SOAP 1.2");
343         }
344         builder.append(',');
345         if (payloadCaching) {
346             builder.append("PayloadCaching enabled");
347         }
348         else {
349             builder.append("PayloadCaching disabled");
350         }
351         builder.append(']');
352         return builder.toString();
353     }
354 }