/* * 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.axiom; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Locale; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.axiom.attachments.Attachments; import org.apache.axiom.om.OMAbstractFactory; import org.apache.axiom.om.OMException; import org.apache.axiom.om.impl.MTOMConstants; import org.apache.axiom.soap.SOAP11Constants; import org.apache.axiom.soap.SOAP11Version; import org.apache.axiom.soap.SOAP12Constants; import org.apache.axiom.soap.SOAP12Version; import org.apache.axiom.soap.SOAPFactory; import org.apache.axiom.soap.SOAPMessage; import org.apache.axiom.soap.impl.builder.MTOMStAXSOAPModelBuilder; import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor; import org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping; import org.springframework.ws.soap.SoapMessageFactory; import org.springframework.ws.soap.SoapVersion; import org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor; import org.springframework.ws.soap.server.endpoint.mapping.SoapActionAnnotationMethodEndpointMapping; import org.springframework.ws.soap.support.SoapUtils; import org.springframework.ws.transport.TransportConstants; import org.springframework.ws.transport.TransportInputStream; /** * Axiom-specific implementation of the {@link org.springframework.ws.WebServiceMessageFactory WebServiceMessageFactory} * interface. Creates {@link org.springframework.ws.soap.axiom.AxiomSoapMessage AxiomSoapMessages}. * * <p>To increase reading performance on the SOAP request created by this message factory, you can set the {@link * #setPayloadCaching(boolean) payloadCaching} property to {@code false} (default is {@code true}). This this * will read the contents of the body directly from the stream. However, <strong>when this setting is enabled, the * payload can only be read once</strong>. This means that any endpoint mappings or interceptors which are based on the * message payload (such as the {@link PayloadRootAnnotationMethodEndpointMapping}, the {@link PayloadValidatingInterceptor}, or * the {@link PayloadLoggingInterceptor}) cannot be used. Instead, use an endpoint mapping that does not consume the * payload (i.e. the {@link SoapActionAnnotationMethodEndpointMapping}). * * <p>Additionally, this message factory can cache large attachments to disk by setting the {@link * #setAttachmentCaching(boolean) attachmentCaching} property to {@code true} (default is {@code false}). * Optionally, the location where attachments are stored can be defined via the {@link #setAttachmentCacheDir(File) * attachmentCacheDir} property (defaults to the system temp file path). * * <p>Mostly derived from {@code org.apache.axis2.transport.http.HTTPTransportUtils} and * {@code org.apache.axis2.transport.TransportUtils}, which we cannot use since they are not part of the Axiom * distribution. * * @author Arjen Poutsma * @see AxiomSoapMessage * @see #setPayloadCaching(boolean) * @since 1.0.0 */ public class AxiomSoapMessageFactory implements SoapMessageFactory, InitializingBean { private static final String CHARSET_PARAMETER = "charset"; private static final String DEFAULT_CHARSET_ENCODING = "UTF-8"; private static final String MULTI_PART_RELATED_CONTENT_TYPE = "multipart/related"; private static final Log logger = LogFactory.getLog(AxiomSoapMessageFactory.class); private XMLInputFactory inputFactory; private boolean payloadCaching = true; private boolean attachmentCaching = false; private File attachmentCacheDir; private int attachmentCacheThreshold = 4096; // use SOAP 1.1 by default private SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory(); private boolean langAttributeOnSoap11FaultString = true; private boolean replacingEntityReferences = false; private boolean supportingExternalEntities = false; /** * Indicates whether the SOAP Body payload should be cached or not. Default is {@code true}. * * <p>Setting this to {@code false} will increase performance, but also result in the fact that the message * payload can only be read once. */ public void setPayloadCaching(boolean payloadCaching) { this.payloadCaching = payloadCaching; } /** * Indicates whether SOAP attachments should be cached or not. Default is {@code false}. * * <p>Setting this to {@code true} will cause Axiom to store larger attachments on disk, rather than in memory. * This decreases memory consumption, but decreases performance. */ public void setAttachmentCaching(boolean attachmentCaching) { this.attachmentCaching = attachmentCaching; } /** * Sets the directory where SOAP attachments will be stored. Only used when {@link #setAttachmentCaching(boolean) * attachmentCaching} is set to {@code true}. * * <p>The parameter should be an existing, writable directory. This property defaults to the temporary directory of the * operating system (i.e. the value of the {@code java.io.tmpdir} system property). */ public void setAttachmentCacheDir(File attachmentCacheDir) { Assert.notNull(attachmentCacheDir, "'attachmentCacheDir' must not be null"); Assert.isTrue(attachmentCacheDir.isDirectory(), "'attachmentCacheDir' must be a directory"); Assert.isTrue(attachmentCacheDir.canWrite(), "'attachmentCacheDir' must be writable"); this.attachmentCacheDir = attachmentCacheDir; } /** * Sets the threshold for attachments caching, in bytes. Attachments larger than this threshold will be cached in * the {@link #setAttachmentCacheDir(File) attachment cache directory}. Only used when {@link * #setAttachmentCaching(boolean) attachmentCaching} is set to {@code true}. * * <p>Defaults to 4096 bytes (i.e. 4 kilobytes). */ public void setAttachmentCacheThreshold(int attachmentCacheThreshold) { Assert.isTrue(attachmentCacheThreshold > 0, "'attachmentCacheThreshold' must be larger than 0"); this.attachmentCacheThreshold = attachmentCacheThreshold; } @Override public void setSoapVersion(SoapVersion version) { if (SoapVersion.SOAP_11 == version) { soapFactory = OMAbstractFactory.getSOAP11Factory(); } else if (SoapVersion.SOAP_12 == version) { soapFactory = OMAbstractFactory.getSOAP12Factory(); } else { throw new IllegalArgumentException( "Invalid version [" + version + "]. " + "Expected the SOAP_11 or SOAP_12 constant"); } } /** * 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; } /** * Sets whether internal entity references should be replaced with their replacement * text and report them as characters. * @see XMLInputFactory#IS_REPLACING_ENTITY_REFERENCES */ public void setReplacingEntityReferences(boolean replacingEntityReferences) { this.replacingEntityReferences = replacingEntityReferences; } /** * Sets whether external parsed entities should be resolved. * @see XMLInputFactory#IS_SUPPORTING_EXTERNAL_ENTITIES */ public void setSupportingExternalEntities(boolean supportingExternalEntities) { this.supportingExternalEntities = supportingExternalEntities; } @Override public void afterPropertiesSet() throws Exception { if (logger.isInfoEnabled()) { logger.info(payloadCaching ? "Enabled payload caching" : "Disabled payload caching"); } if (attachmentCacheDir == null) { String tempDir = System.getProperty("java.io.tmpdir"); setAttachmentCacheDir(new File(tempDir)); } inputFactory = createXmlInputFactory(); } @Override public AxiomSoapMessage createWebServiceMessage() { return new AxiomSoapMessage(soapFactory, payloadCaching, langAttributeOnSoap11FaultString); } @Override public AxiomSoapMessage createWebServiceMessage(InputStream inputStream) throws IOException { Assert.isInstanceOf(TransportInputStream.class, inputStream, "AxiomSoapMessageFactory requires a TransportInputStream"); if (inputFactory == null) { inputFactory = createXmlInputFactory(); } TransportInputStream transportInputStream = (TransportInputStream) inputStream; String contentType = getHeaderValue(transportInputStream, TransportConstants.HEADER_CONTENT_TYPE); if (!StringUtils.hasLength(contentType)) { if (logger.isDebugEnabled()) { logger.debug("TransportInputStream has no Content-Type header; defaulting to \"" + SoapVersion.SOAP_11.getContentType() + "\""); } contentType = SoapVersion.SOAP_11.getContentType(); } String soapAction = getHeaderValue(transportInputStream, TransportConstants.HEADER_SOAP_ACTION); if (!StringUtils.hasLength(soapAction)) { soapAction = SoapUtils.extractActionFromContentType(contentType); } try { if (isMultiPartRelated(contentType)) { return createMultiPartAxiomSoapMessage(inputStream, contentType, soapAction); } else { return createAxiomSoapMessage(inputStream, contentType, soapAction); } } catch (XMLStreamException ex) { throw new AxiomSoapMessageCreationException("Could not parse request: " + ex.getMessage(), ex); } catch (OMException ex) { throw new AxiomSoapMessageCreationException("Could not create message: " + ex.getMessage(), ex); } } private String getHeaderValue(TransportInputStream transportInputStream, String header) throws IOException { String contentType = null; Iterator<String> iterator = transportInputStream.getHeaders(header); if (iterator.hasNext()) { contentType = iterator.next(); } return contentType; } private boolean isMultiPartRelated(String contentType) { contentType = contentType.toLowerCase(Locale.ENGLISH); return contentType.contains(MULTI_PART_RELATED_CONTENT_TYPE); } @SuppressWarnings("deprecation") /** Creates an AxiomSoapMessage without attachments. */ private AxiomSoapMessage createAxiomSoapMessage(InputStream inputStream, String contentType, String soapAction) throws XMLStreamException { XMLStreamReader reader = inputFactory.createXMLStreamReader(inputStream, getCharSetEncoding(contentType)); String envelopeNamespace = getSoapEnvelopeNamespace(contentType); StAXSOAPModelBuilder builder = new StAXSOAPModelBuilder(reader, soapFactory, envelopeNamespace); SOAPMessage soapMessage = builder.getSoapMessage(); return new AxiomSoapMessage(soapMessage, soapAction, payloadCaching, langAttributeOnSoap11FaultString); } @SuppressWarnings("deprecation") /** Creates an AxiomSoapMessage with attachments. */ private AxiomSoapMessage createMultiPartAxiomSoapMessage(InputStream inputStream, String contentType, String soapAction) throws XMLStreamException { Attachments attachments = new Attachments(inputStream, contentType, attachmentCaching, attachmentCacheDir.getAbsolutePath(), Integer.toString(attachmentCacheThreshold)); XMLStreamReader reader = inputFactory.createXMLStreamReader(attachments.getRootPartInputStream(), getCharSetEncoding(attachments.getRootPartContentType())); StAXSOAPModelBuilder builder; String envelopeNamespace = getSoapEnvelopeNamespace(contentType); if (MTOMConstants.SWA_TYPE.equals(attachments.getAttachmentSpecType()) || MTOMConstants.SWA_TYPE_12.equals(attachments.getAttachmentSpecType())) { builder = new StAXSOAPModelBuilder(reader, soapFactory, envelopeNamespace); } else if (MTOMConstants.MTOM_TYPE.equals(attachments.getAttachmentSpecType())) { builder = new MTOMStAXSOAPModelBuilder(reader, attachments, envelopeNamespace); } else { throw new AxiomSoapMessageCreationException( "Unknown attachment type: [" + attachments.getAttachmentSpecType() + "]"); } return new AxiomSoapMessage(builder.getSoapMessage(), attachments, soapAction, payloadCaching, langAttributeOnSoap11FaultString); } private String getSoapEnvelopeNamespace(String contentType) { if (contentType.contains(SOAP11Constants.SOAP_11_CONTENT_TYPE)) { return SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI; } else if (contentType.contains(SOAP12Constants.SOAP_12_CONTENT_TYPE)) { return SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI; } else { throw new AxiomSoapMessageCreationException("Unknown content type '" + contentType + "'"); } } /** * Returns the character set from the given content type. Mostly copied * * @return the character set encoding */ protected String getCharSetEncoding(String contentType) { int charSetIdx = contentType.indexOf(CHARSET_PARAMETER); if (charSetIdx == -1) { return DEFAULT_CHARSET_ENCODING; } int eqIdx = contentType.indexOf("=", charSetIdx); int indexOfSemiColon = contentType.indexOf(";", eqIdx); String value; if (indexOfSemiColon > 0) { value = contentType.substring(eqIdx + 1, indexOfSemiColon); } else { value = contentType.substring(eqIdx + 1, contentType.length()).trim(); } if (value.startsWith("\"")) { value = value.substring(1); } if (value.endsWith("\"")) { return value.substring(0, value.length() - 1); } if ("null".equalsIgnoreCase(value)) { return DEFAULT_CHARSET_ENCODING; } else { return value.trim(); } } /** * Create a {@code XMLInputFactory} that this resolver will use to create {@link XMLStreamReader} objects. * * <p>Can be overridden in subclasses, adding further initialization of the factory. The resulting factory is cached, * so this method will only be called once. * * <p>By default this method creates a standard {@link XMLInputFactory} and configures it * based on the {@link #setReplacingEntityReferences(boolean) replacingEntityReferences} * and {@link #setSupportingExternalEntities(boolean) supportingExternalEntities} properties. * * @return the created factory */ protected XMLInputFactory createXmlInputFactory() { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, replacingEntityReferences); inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, supportingExternalEntities); return inputFactory; } public String toString() { StringBuilder builder = new StringBuilder("AxiomSoapMessageFactory["); if (soapFactory.getSOAPVersion() == SOAP11Version.getSingleton()) { builder.append("SOAP 1.1"); } else if (soapFactory.getSOAPVersion() == SOAP12Version.getSingleton()) { builder.append("SOAP 1.2"); } builder.append(','); if (payloadCaching) { builder.append("PayloadCaching enabled"); } else { builder.append("PayloadCaching disabled"); } builder.append(']'); return builder.toString(); } }