/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.axis2.saaj; import org.apache.axiom.attachments.Attachments; import org.apache.axiom.mime.ContentType; import org.apache.axiom.mime.MediaType; import org.apache.axiom.om.OMException; import org.apache.axiom.om.OMOutputFormat; import org.apache.axiom.om.impl.OMMultipartWriter; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axiom.soap.SOAPFactory; import org.apache.axiom.soap.SOAPVersion; import org.apache.axiom.util.UIDGenerator; import org.apache.axis2.saaj.util.SAAJUtil; import org.apache.axis2.transport.http.HTTPConstants; import javax.xml.soap.AttachmentPart; import javax.xml.soap.MimeHeader; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPBody; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPMessage; import javax.xml.soap.SOAPPart; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; public class SOAPMessageImpl extends SOAPMessage { private SOAPPart soapPart; private Collection<AttachmentPart> attachmentParts = new ArrayList<AttachmentPart>(); private MimeHeaders mimeHeaders; private Map<String,Object> props = new Hashtable<String,Object>(); private boolean saveRequired; public SOAPMessageImpl(SOAPEnvelopeImpl soapEnvelope) { this.mimeHeaders = new MimeHeaders(); this.mimeHeaders.addHeader("content-type", ((SOAPFactory)soapEnvelope.omTarget.getOMFactory()).getSOAPVersion().getMediaType().toString()); soapPart = new SOAPPartImpl(this, soapEnvelope); } public SOAPMessageImpl(InputStream inputstream, MimeHeaders mimeHeaders, boolean processMTOM) throws SOAPException { String contentType = null; String tmpContentType = ""; if (mimeHeaders != null) { String contentTypes[] = mimeHeaders.getHeader(HTTPConstants.HEADER_CONTENT_TYPE); if (contentTypes != null && contentTypes.length > 0) { tmpContentType = contentTypes[0]; contentType = SAAJUtil.normalizeContentType(tmpContentType); } } if (HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED.equals(contentType)) { try { Attachments attachments = new Attachments(inputstream, tmpContentType, false, "", ""); // Axiom doesn't give us access to the MIME headers of the individual // parts of the SOAP message package. We need to reconstruct them from // the available information. MimeHeaders soapPartHeaders = new MimeHeaders(); soapPartHeaders.addHeader(HTTPConstants.HEADER_CONTENT_TYPE, attachments.getRootPartContentType()); String soapPartContentId = attachments.getRootPartContentID(); soapPartHeaders.addHeader("Content-ID", "<" + soapPartContentId + ">"); soapPart = new SOAPPartImpl(this, attachments.getRootPartInputStream(), soapPartHeaders, processMTOM ? attachments : null); for (String contentId : attachments.getAllContentIDs()) { if (!contentId.equals(soapPartContentId)) { AttachmentPart ap = createAttachmentPart(attachments.getDataHandler(contentId)); ap.setContentId("<" + contentId + ">"); attachmentParts.add(ap); } } } catch (OMException e) { throw new SOAPException(e); } } else { initCharsetEncodingFromContentType(tmpContentType); soapPart = new SOAPPartImpl(this, inputstream, mimeHeaders, null); } this.mimeHeaders = (mimeHeaders == null) ? new MimeHeaders() : SAAJUtil.copyMimeHeaders(mimeHeaders); } /** * Retrieves a description of this <CODE>SOAPMessage</CODE> object's content. * * @return a <CODE>String</CODE> describing the content of this message or <CODE>null</CODE> if * no description has been set * @see #setContentDescription(String) setContentDescription(java.lang.String) */ public String getContentDescription() { String values[] = mimeHeaders.getHeader(HTTPConstants.HEADER_CONTENT_DESCRIPTION); if (values != null && values.length > 0) { return values[0]; } return null; } /** * Sets the description of this <CODE>SOAPMessage</CODE> object's content with the given * description. * * @param description a <CODE>String</CODE> describing the content of this message * @see #getContentDescription() getContentDescription() */ public void setContentDescription(String description) { mimeHeaders.setHeader(HTTPConstants.HEADER_CONTENT_DESCRIPTION, description); } /** * Gets the SOAP part of this <CODE>SOAPMessage</CODE> object. * <p/> * <p/> * <P>If a <CODE>SOAPMessage</CODE> object contains one or more attachments, the SOAP Part must * be the first MIME body part in the message.</P> * * @return the <CODE>SOAPPart</CODE> object for this <CODE> SOAPMessage</CODE> object */ public SOAPPart getSOAPPart() { return soapPart; } /** * Removes all <CODE>AttachmentPart</CODE> objects that have been added to this * <CODE>SOAPMessage</CODE> object. * <p/> * <P>This method does not touch the SOAP part.</P> */ public void removeAllAttachments() { attachmentParts.clear(); saveRequired = true; } /** * Gets a count of the number of attachments in this message. This count does not include the * SOAP part. * * @return the number of <CODE>AttachmentPart</CODE> objects that are part of this * <CODE>SOAPMessage</CODE> object */ public int countAttachments() { return attachmentParts.size(); } /** * Retrieves all the <CODE>AttachmentPart</CODE> objects that are part of this * <CODE>SOAPMessage</CODE> object. * * @return an iterator over all the attachments in this message */ public Iterator getAttachments() { return attachmentParts.iterator(); } /** * Retrieves all the AttachmentPart objects that have header entries that match the specified * headers. Note that a returned attachment could have headers in addition to those specified. * * @param headers a {@link javax.xml.soap.MimeHeaders} object containing the MIME headers for * which to search * @return an iterator over all attachments({@link javax.xml.soap.AttachmentPart}) that have a * header that matches one of the given headers */ public Iterator getAttachments(javax.xml.soap.MimeHeaders headers) { Collection<AttachmentPart> matchingAttachmentParts = new ArrayList<AttachmentPart>(); Iterator iterator = getAttachments(); { AttachmentPartImpl part; while (iterator.hasNext()) { part = (AttachmentPartImpl)iterator.next(); if (part.matches(headers)) { matchingAttachmentParts.add(part); } } } return matchingAttachmentParts.iterator(); } /** * Adds the given <CODE>AttachmentPart</CODE> object to this <CODE>SOAPMessage</CODE> object. An * <CODE> AttachmentPart</CODE> object must be created before it can be added to a message. * * @param attachmentPart an <CODE> AttachmentPart</CODE> object that is to become part of this * <CODE>SOAPMessage</CODE> object * @throws IllegalArgumentException * */ public void addAttachmentPart(AttachmentPart attachmentPart) { if (attachmentPart != null) { attachmentParts.add(attachmentPart); mimeHeaders.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, "multipart/related"); saveRequired = true; } } /** * Creates a new empty <CODE>AttachmentPart</CODE> object. Note that the method * <CODE>addAttachmentPart</CODE> must be called with this new <CODE>AttachmentPart</CODE> * object as the parameter in order for it to become an attachment to this * <CODE>SOAPMessage</CODE> object. * * @return a new <CODE>AttachmentPart</CODE> object that can be populated and added to this * <CODE>SOAPMessage</CODE> object */ public AttachmentPart createAttachmentPart() { return new AttachmentPartImpl(); } /** * Returns all the transport-specific MIME headers for this <CODE>SOAPMessage</CODE> object in a * transport-independent fashion. * * @return a <CODE>MimeHeaders</CODE> object containing the <CODE>MimeHeader</CODE> objects */ public javax.xml.soap.MimeHeaders getMimeHeaders() { return mimeHeaders; } /** * Updates this <CODE>SOAPMessage</CODE> object with all the changes that have been made to it. * This method is called automatically when a message is sent or written to by the methods * <CODE>ProviderConnection.send</CODE>, <CODE> SOAPConnection.call</CODE>, or <CODE> * SOAPMessage.writeTo</CODE>. However, if changes are made to a message that was received or to * one that has already been sent, the method <CODE>saveChanges</CODE> needs to be called * explicitly in order to save the changes. The method <CODE>saveChanges</CODE> also generates * any changes that can be read back (for example, a MessageId in profiles that support a * message id). All MIME headers in a message that is created for sending purposes are * guaranteed to have valid values only after <CODE>saveChanges</CODE> has been called. * <p/> * <P>In addition, this method marks the point at which the data from all constituent * <CODE>AttachmentPart</CODE> objects are pulled into the message.</P> * * @throws SOAPException if there was a problem saving changes to this message. */ public void saveChanges() throws SOAPException { try { String contentTypeValue = getSingleHeaderValue(HTTPConstants.HEADER_CONTENT_TYPE); ContentType.Builder contentType; if (isEmptyString(contentTypeValue)) { contentType = ContentType.builder().setMediaType(attachmentParts.size() > 0 ? MediaType.MULTIPART_RELATED : getMediaType()); } else { contentType = new ContentType(contentTypeValue).toBuilder(); //Use configures the baseType with multipart/related while no attachment exists or all the attachments are removed if (contentType.getMediaType().equals(MediaType.MULTIPART_RELATED) && attachmentParts.size() == 0) { contentType.setMediaType(getMediaType()); contentType.clearParameters(); } } //If it is of multipart/related, initialize those required values in the content-type, including boundary etc. if (contentType.getMediaType().equals(MediaType.MULTIPART_RELATED)) { //Configure boundary String boundaryParam = contentType.getParameter("boundary"); if (isEmptyString(boundaryParam)) { contentType.setParameter("boundary", UIDGenerator.generateMimeBoundary()); } //Configure start content id, always get it from soapPart in case it is changed String soapPartContentId = soapPart.getContentId(); if (isEmptyString(soapPartContentId)) { soapPartContentId = "<" + UIDGenerator.generateContentId() + ">"; soapPart.setContentId(soapPartContentId); } contentType.setParameter("start", soapPartContentId); //Configure contentId for each attachments for(AttachmentPart attachmentPart : attachmentParts) { if(isEmptyString(attachmentPart.getContentId())) { attachmentPart.setContentId("<" + UIDGenerator.generateContentId() + ">"); } } //Configure type contentType.setParameter("type", getMediaType().toString()); //Configure charset String soapPartContentTypeValue = getSingleHeaderValue(soapPart.getMimeHeader(HTTPConstants.HEADER_CONTENT_TYPE)); ContentType.Builder soapPartContentType; if (isEmptyString(soapPartContentTypeValue)) { soapPartContentType = new ContentType(soapPartContentTypeValue).toBuilder(); } else { soapPartContentType = ContentType.builder().setMediaType(getMediaType()); } setCharsetParameter(soapPartContentType); } else { //Configure charset setCharsetParameter(contentType); } mimeHeaders.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, contentType.build().toString()); } catch (ParseException e) { throw new SOAPException("Invalid Content Type Field in the Mime Message", e); } saveRequired = false; } public void setSaveRequired() { this.saveRequired = true; } /** * Indicates whether this <CODE>SOAPMessage</CODE> object has had the method {@link * #saveChanges()} called on it. * * @return <CODE>true</CODE> if <CODE>saveChanges</CODE> has been called on this message at * least once; <CODE> false</CODE> otherwise. */ public boolean saveRequired() { return saveRequired; } /** * Writes this <CODE>SOAPMessage</CODE> object to the given output stream. The externalization * format is as defined by the SOAP 1.1 with Attachments specification. * <p/> * <P>If there are no attachments, just an XML stream is written out. For those messages that * have attachments, <CODE>writeTo</CODE> writes a MIME-encoded byte stream.</P> * * @param out the <CODE>OutputStream</CODE> object to which this <CODE>SOAPMessage</CODE> object * will be written * @throws SOAPException if there was a problem in externalizing this SOAP message * @throws IOException if an I/O error occurs */ public void writeTo(OutputStream out) throws SOAPException, IOException { try { saveChanges(); OMOutputFormat format = new OMOutputFormat(); String enc = (String)getProperty(CHARACTER_SET_ENCODING); format.setCharSetEncoding(enc != null ? enc : OMOutputFormat.DEFAULT_CHAR_SET_ENCODING); String writeXmlDecl = (String)getProperty(WRITE_XML_DECLARATION); if (writeXmlDecl == null || writeXmlDecl.equals("false")) { //SAAJ default case doesn't send XML decl format.setIgnoreXMLDeclaration(true); } SOAPEnvelope envelope = ((SOAPEnvelopeImpl) soapPart.getEnvelope()).getOMTarget(); if (attachmentParts.isEmpty()) { envelope.serialize(out, format); } else { ContentType.Builder contentType = new ContentType(getSingleHeaderValue(HTTPConstants.HEADER_CONTENT_TYPE)).toBuilder(); String boundary = contentType.getParameter("boundary"); if(isEmptyString(boundary)) { boundary = UIDGenerator.generateMimeBoundary(); contentType.setParameter("boundary", boundary); } format.setMimeBoundary(boundary); String rootContentId = soapPart.getContentId(); if(isEmptyString(rootContentId)) { rootContentId = "<" + UIDGenerator.generateContentId() + ">"; soapPart.setContentId(rootContentId); } contentType.setParameter("start", rootContentId); if ((rootContentId.indexOf("<") > -1) & (rootContentId.indexOf(">") > -1)) { rootContentId = rootContentId.substring(1, (rootContentId.length() - 1)); } format.setRootContentId(rootContentId); format.setSOAP11(((SOAPFactory)((SOAPEnvelopeImpl) soapPart.getEnvelope()).omTarget.getOMFactory()).getSOAPVersion() == SOAPVersion.SOAP11); //Double save the content-type in case anything is updated mimeHeaders.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, contentType.build().toString()); OMMultipartWriter mpw = new OMMultipartWriter(out, format); OutputStream rootPartOutputStream = mpw.writeRootPart(); envelope.serialize(rootPartOutputStream); rootPartOutputStream.close(); for (AttachmentPart ap : attachmentParts) { mpw.writePart(ap.getDataHandler(), ap.getContentId()); } mpw.complete(); } saveRequired = true; } catch (Exception e) { throw new SOAPException(e); } } /** * Associates the specified value with the specified property. If there was already a value * associated with this property, the old value is replaced. * <p/> * The valid property names include <code>WRITE_XML_DECLARATION</code> and * <code>CHARACTER_SET_ENCODING</code>. All of these standard SAAJ properties are prefixed by * "javax.xml.soap". Vendors may also add implementation specific properties. These properties * must be prefixed with package names that are unique to the vendor. * <p/> * Setting the property <code>WRITE_XML_DECLARATION</code> to <code>"true"</code> will cause an * XML Declaration to be written out at the start of the SOAP message. The default value of * "false" suppresses this declaration. * <p/> * The property <code>CHARACTER_SET_ENCODING</code> defaults to the value <code>"utf-8"</code> * which causes the SOAP message to be encoded using UTF-8. Setting * <code>CHARACTER_SET_ENCODING</code> to <code>"utf-16"</code> causes the SOAP message to be * encoded using UTF-16. * <p/> * Some implementations may allow encodings in addition to UTF-8 and UTF-16. Refer to your * vendor's documentation for details. * * @param property the property with which the specified value is to be associated * @param value the value to be associated with the specified property */ public void setProperty(String property, Object value) { props.put(property, value); } /** * Retrieves value of the specified property. * * @param property the name of the property to retrieve * @return the value of the property or <code>null</code> if no such property exists * @throws SOAPException if the property name is not recognized */ public Object getProperty(String property) throws SOAPException { return props.get(property); } /** * Returns an AttachmentPart object that is associated with an attachment that is referenced by * this SOAPElement or null if no such attachment exists. References can be made via an href * attribute as described in SOAP Messages with Attachments (http://www.w3.org/TR/SOAPattachments#SOAPReferenceToAttachements) * , or via a single Text child node containing a URI as described in the WS-I Attachments * Profile 1.0 for elements of schema type ref:swaRef(ref:swaRef (http://www.wsi.org/Profiles/AttachmentsProfile-1.0-2004-08-24.html") * ). These two mechanisms must be supported. The support for references via href attribute also * implies that this method should also be supported on an element that is an xop:Include * element (XOP (http://www.w3.org/2000/xp/Group/3/06/Attachments/XOP.html) ). other reference * mechanisms may be supported by individual implementations of this standard. Contact your * vendor for details. * * @param element - The SOAPElement containing the reference to an Attachment * @return the referenced AttachmentPart or null if no such AttachmentPart exists or no * reference can be found in this SOAPElement. * @throws SOAPException - if there is an error in the attempt to access the attachment */ public AttachmentPart getAttachment(SOAPElement soapelement) throws SOAPException { //TODO read strings from constants Iterator iterator = getAttachments(); { AttachmentPartImpl attachmentPart; while (iterator.hasNext()) { attachmentPart = (AttachmentPartImpl)iterator.next(); String[] contentIds = attachmentPart.getMimeHeader("Content-Id"); //References can be made via an href attribute as described in SOAP Messages //with Attachments or via a single Text child node containing a URI String reference = soapelement.getAttribute("href"); if (reference == null || reference.trim().length() == 0) { reference = soapelement.getValue(); if (reference == null || reference.trim().length() == 0) { return null; } } for (int a = 0; a < contentIds.length; a++) { //eg: cid:gifImage scenario String idPart = reference.substring(reference.indexOf(":") + 1); idPart = "<" + idPart + ">"; if (idPart.equals(contentIds[a])) { return attachmentPart; } } String[] contentLocations = attachmentPart.getMimeHeader("Content-Location"); if (!(contentLocations == null)) { //uri scenario for (int b = 0; b < contentLocations.length; b++) { if (reference.equals(contentLocations[b])) { return attachmentPart; } } } } } return null; } /** * Removes all the AttachmentPart objects that have header entries that match the specified * headers. Note that the removed attachment could have headers in addition to those specified. * * @param headers - a MimeHeaders object containing the MIME headers for which to search * @since SAAJ 1.3 */ public void removeAttachments(MimeHeaders headers) { Collection<AttachmentPart> newAttachmentParts = new ArrayList<AttachmentPart>(); for (AttachmentPart attachmentPart : attachmentParts) { //Get all the headers for (Iterator iterator = headers.getAllHeaders(); iterator.hasNext();) { MimeHeader mimeHeader = (MimeHeader)iterator.next(); String[] headerValues = attachmentPart.getMimeHeader(mimeHeader.getName()); //if values for this header name, do not remove it if (headerValues.length != 0) { if (!(headerValues[0].equals(mimeHeader.getValue()))) { newAttachmentParts.add(attachmentPart); } } } } attachmentParts.clear(); this.attachmentParts = newAttachmentParts; saveRequired = true; } /** * Gets the SOAP Header contained in this <code>SOAPMessage</code> object. * * @return the <code>SOAPHeader</code> object contained by this <code>SOAPMessage</code> object * @throws javax.xml.soap.SOAPException if the SOAP Header does not exist or cannot be * retrieved */ public SOAPHeader getSOAPHeader() throws SOAPException { return this.soapPart.getEnvelope().getHeader(); } /** * Gets the SOAP Body contained in this <code>SOAPMessage</code> object. * * @return the <code>SOAPBody</code> object contained by this <code>SOAPMessage</code> object * @throws javax.xml.soap.SOAPException if the SOAP Body does not exist or cannot be retrieved */ public SOAPBody getSOAPBody() throws SOAPException { return this.soapPart.getEnvelope().getBody(); } /** * Set the character encoding based on the <code>contentType</code> parameter * * @param contentType */ private void initCharsetEncodingFromContentType(final String contentType) { if (contentType != null) { int delimiterIndex = contentType.lastIndexOf("charset"); if (delimiterIndex > 0) { String charsetPart = contentType.substring(delimiterIndex); int charsetIndex = charsetPart.indexOf('='); String charset = charsetPart.substring(charsetIndex + 1).trim(); if ((charset.startsWith("\"") || charset.startsWith("\'"))) { charset = charset.substring(1, charset.length()); } if ((charset.endsWith("\"") || charset.endsWith("\'"))) { charset = charset.substring(0, charset.length() - 1); } int index = charset.indexOf(';'); if (index != -1) { charset = charset.substring(0, index); } setProperty(SOAPMessage.CHARACTER_SET_ENCODING, charset); } } } private boolean isEmptyString(String value) { return value == null || value.length() == 0; } private String getSingleHeaderValue(String[] values) { return values != null && values.length > 0 ? values[0] : null; } private String getSingleHeaderValue(String name) { String[] values = mimeHeaders.getHeader(name); if (values == null || values.length == 0) { return null; } else { return values[0]; } } private MediaType getMediaType() throws SOAPException { return ((SOAPFactory)((SOAPEnvelopeImpl) soapPart.getEnvelope()).omTarget.getOMFactory()).getSOAPVersion().getMediaType(); } /** * If the charset is configured by CHARACTER_SET_ENCODING, set it in the contentPart always. * If it has already been configured in the contentType, leave it there. * UTF-8 is used as the default value. * @param contentType * @throws SOAPException */ private void setCharsetParameter(ContentType.Builder contentType) throws SOAPException{ String charset = (String)getProperty(CHARACTER_SET_ENCODING); if (!isEmptyString(charset)) { contentType.setParameter("charset", charset); } else { charset = contentType.getParameter("charset"); if(isEmptyString(charset)) { contentType.setParameter("charset", "UTF-8"); } } } }