blob: 0db9233e6be9f09f40bac9be2dd3d55a436d7189 [file] [log] [blame]
/*
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.xml.internal.ws.encoding;
import com.sun.istack.internal.NotNull;
import com.sun.xml.internal.bind.DatatypeConverterImpl;
import com.sun.xml.internal.ws.api.SOAPVersion;
import com.sun.xml.internal.ws.api.WSFeatureList;
import com.sun.xml.internal.ws.api.message.Attachment;
import com.sun.xml.internal.ws.api.message.AttachmentSet;
import com.sun.xml.internal.ws.api.message.Packet;
import com.sun.xml.internal.ws.api.pipe.ContentType;
import com.sun.xml.internal.ws.api.pipe.StreamSOAPCodec;
import com.sun.xml.internal.ws.api.streaming.XMLStreamReaderFactory;
import com.sun.xml.internal.ws.api.streaming.XMLStreamWriterFactory;
import com.sun.xml.internal.ws.developer.SerializationFeature;
import com.sun.xml.internal.ws.developer.StreamingDataHandler;
import com.sun.xml.internal.ws.message.MimeAttachmentSet;
import com.sun.xml.internal.ws.streaming.XMLStreamWriterUtil;
import com.sun.xml.internal.ws.util.ByteArrayDataSource;
import com.sun.xml.internal.ws.util.xml.NamespaceContextExAdaper;
import com.sun.xml.internal.ws.util.xml.XMLStreamReaderFilter;
import com.sun.xml.internal.ws.util.xml.XMLStreamWriterFilter;
import com.sun.xml.internal.ws.streaming.MtomStreamWriter;
import com.sun.xml.internal.ws.streaming.XMLStreamReaderUtil;
import com.sun.xml.internal.ws.server.UnsupportedMediaException;
import com.sun.xml.internal.org.jvnet.staxex.Base64Data;
import com.sun.xml.internal.org.jvnet.staxex.NamespaceContextEx;
import com.sun.xml.internal.org.jvnet.staxex.XMLStreamReaderEx;
import com.sun.xml.internal.org.jvnet.staxex.XMLStreamWriterEx;
import javax.activation.DataHandler;
import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.soap.MTOMFeature;
import javax.xml.bind.attachment.AttachmentMarshaller;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Mtom message Codec. It can be used even for non-soap message's mtom encoding.
*
* @author Vivek Pandey
* @author Jitendra Kotamraju
*/
public class MtomCodec extends MimeCodec {
public static final String XOP_XML_MIME_TYPE = "application/xop+xml";
public static final String XOP_LOCALNAME = "Include";
public static final String XOP_NAMESPACEURI = "http://www.w3.org/2004/08/xop/include";
private final StreamSOAPCodec codec;
private final MTOMFeature mtomFeature;
private final SerializationFeature sf;
private final static String DECODED_MESSAGE_CHARSET = "decodedMessageCharset";
MtomCodec(SOAPVersion version, StreamSOAPCodec codec, WSFeatureList features){
super(version, features);
this.codec = codec;
sf = features.get(SerializationFeature.class);
MTOMFeature mtom = features.get(MTOMFeature.class);
if(mtom == null)
this.mtomFeature = new MTOMFeature();
else
this.mtomFeature = mtom;
}
/**
* Return the soap 1.1 and soap 1.2 specific XOP packaged ContentType
*
* @return A non-null content type for soap11 or soap 1.2 content type
*/
@Override
public ContentType getStaticContentType(Packet packet) {
return getStaticContentTypeStatic(packet, version);
}
public static ContentType getStaticContentTypeStatic(Packet packet, SOAPVersion version) {
ContentType ct = (ContentType) packet.getInternalContentType();
if ( ct != null ) return ct;
String uuid = UUID.randomUUID().toString();
String boundary = "uuid:" + uuid;
String rootId = "<rootpart*"+uuid+"@example.jaxws.sun.com>";
String soapActionParameter = SOAPVersion.SOAP_11.equals(version) ? null : createActionParameter(packet);
String boundaryParameter = "boundary=\"" + boundary +"\"";
String messageContentType = MULTIPART_RELATED_MIME_TYPE +
";start=\""+rootId +"\"" +
";type=\"" + XOP_XML_MIME_TYPE + "\";" +
boundaryParameter +
";start-info=\"" + version.contentType +
(soapActionParameter == null? "" : soapActionParameter) +
"\"";
ContentTypeImpl ctImpl = SOAPVersion.SOAP_11.equals(version) ?
new ContentTypeImpl(messageContentType, (packet.soapAction == null)?"":packet.soapAction, null) :
new ContentTypeImpl(messageContentType, null, null);
ctImpl.setBoundary(boundary);
ctImpl.setRootId(rootId);
packet.setContentType(ctImpl);
return ctImpl;
}
private static String createActionParameter(Packet packet) {
return packet.soapAction != null? ";action=\\\""+packet.soapAction+"\\\"" : "";
}
@Override
public ContentType encode(Packet packet, OutputStream out) throws IOException {
ContentTypeImpl ctImpl = (ContentTypeImpl) this.getStaticContentType(packet);
String boundary = ctImpl.getBoundary();
String rootId = ctImpl.getRootId();
if(packet.getMessage() != null){
try {
String encoding = getPacketEncoding(packet);
packet.invocationProperties.remove(DECODED_MESSAGE_CHARSET);
String actionParameter = getActionParameter(packet, version);
String soapXopContentType = getSOAPXopContentType(encoding, version, actionParameter);
writeln("--"+boundary, out);
writeMimeHeaders(soapXopContentType, rootId, out);
//mtom attachments that need to be written after the root part
List<ByteArrayBuffer> mtomAttachments = new ArrayList<ByteArrayBuffer>();
MtomStreamWriterImpl writer = new MtomStreamWriterImpl(
XMLStreamWriterFactory.create(out, encoding), mtomAttachments, boundary, mtomFeature);
packet.getMessage().writeTo(writer);
XMLStreamWriterFactory.recycle(writer);
writeln(out);
for(ByteArrayBuffer bos : mtomAttachments){
bos.write(out);
}
// now write out the attachments in the message that weren't
// previously written
writeNonMtomAttachments(packet.getMessage().getAttachments(),
out, boundary);
//write out the end boundary
writeAsAscii("--"+boundary, out);
writeAsAscii("--", out);
} catch (XMLStreamException e) {
throw new WebServiceException(e);
}
}
//now create the boundary for next encode() call
// createConteTypeHeader();
return ctImpl;
}
public static String getSOAPXopContentType(String encoding, SOAPVersion version,
String actionParameter) {
return XOP_XML_MIME_TYPE +";charset="+encoding+";type=\""+version.contentType+ actionParameter + "\"";
}
public static String getActionParameter(Packet packet, SOAPVersion version) {
return (version == SOAPVersion.SOAP_11) ? "" : createActionParameter(packet);
}
public static class ByteArrayBuffer{
final String contentId;
private final DataHandler dh;
private final String boundary;
ByteArrayBuffer(@NotNull DataHandler dh, String b) {
this.dh = dh;
String cid = null;
if (dh instanceof StreamingDataHandler) {
StreamingDataHandler sdh = (StreamingDataHandler) dh;
if (sdh.getHrefCid() != null)
cid = sdh.getHrefCid();
}
this.contentId = cid != null ? cid : encodeCid();
boundary = b;
}
public void write(OutputStream os) throws IOException {
//build attachment frame
writeln("--"+boundary, os);
writeMimeHeaders(dh.getContentType(), contentId, os);
dh.writeTo(os);
writeln(os);
}
}
public static void writeMimeHeaders(String contentType, String contentId, OutputStream out) throws IOException {
String cid = contentId;
if(cid != null && cid.length() >0 && cid.charAt(0) != '<')
cid = '<' + cid + '>';
writeln("Content-Id: " + cid, out);
writeln("Content-Type: " + contentType, out);
writeln("Content-Transfer-Encoding: binary", out);
writeln(out);
}
// Compiler warning for not calling close, but cannot call close,
// will consume attachment bytes.
@SuppressWarnings("resource")
private void writeNonMtomAttachments(AttachmentSet attachments,
OutputStream out, String boundary) throws IOException {
for (Attachment att : attachments) {
DataHandler dh = att.asDataHandler();
if (dh instanceof StreamingDataHandler) {
StreamingDataHandler sdh = (StreamingDataHandler) dh;
// If DataHandler has href Content-ID, it is MTOM, so skip.
if (sdh.getHrefCid() != null)
continue;
}
// build attachment frame
writeln("--" + boundary, out);
writeMimeHeaders(att.getContentType(), att.getContentId(), out);
att.writeTo(out);
writeln(out); // write \r\n
}
}
@Override
public ContentType encode(Packet packet, WritableByteChannel buffer) {
throw new UnsupportedOperationException();
}
@Override
public MtomCodec copy() {
return new MtomCodec(version, (StreamSOAPCodec)codec.copy(), features);
}
private static String encodeCid(){
String cid="example.jaxws.sun.com";
String name = UUID.randomUUID()+"@";
return name + cid;
}
@Override
protected void decode(MimeMultipartParser mpp, Packet packet) throws IOException {
//TODO shouldn't we check for SOAP1.1/SOAP1.2 and throw
//TODO UnsupportedMediaException like StreamSOAPCodec
String charset = null;
String ct = mpp.getRootPart().getContentType();
if (ct != null) {
charset = new ContentTypeImpl(ct).getCharSet();
}
if (charset != null && !Charset.isSupported(charset)) {
throw new UnsupportedMediaException(charset);
}
if (charset != null) {
packet.invocationProperties.put(DECODED_MESSAGE_CHARSET, charset);
} else {
packet.invocationProperties.remove(DECODED_MESSAGE_CHARSET);
}
// we'd like to reuse those reader objects but unfortunately decoder may be reused
// before the decoded message is completely used.
XMLStreamReader mtomReader = new MtomXMLStreamReaderEx( mpp,
XMLStreamReaderFactory.create(null, mpp.getRootPart().asInputStream(), charset, true)
);
packet.setMessage(codec.decode(mtomReader, new MimeAttachmentSet(mpp)));
packet.setMtomFeature(mtomFeature);
packet.setContentType(mpp.getContentType());
}
private String getPacketEncoding(Packet packet) {
// If SerializationFeature is set, just use that encoding
if (sf != null && sf.getEncoding() != null) {
return sf.getEncoding().equals("") ? SOAPBindingCodec.DEFAULT_ENCODING : sf.getEncoding();
}
return determinePacketEncoding(packet);
}
public static String determinePacketEncoding(Packet packet) {
if (packet != null && packet.endpoint != null) {
// Use request message's encoding for Server-side response messages
String charset = (String)packet.invocationProperties.get(DECODED_MESSAGE_CHARSET);
return charset == null
? SOAPBindingCodec.DEFAULT_ENCODING : charset;
}
// Use default encoding for client-side request messages
return SOAPBindingCodec.DEFAULT_ENCODING;
}
public static class MtomStreamWriterImpl extends XMLStreamWriterFilter implements XMLStreamWriterEx,
MtomStreamWriter, HasEncoding {
private final List<ByteArrayBuffer> mtomAttachments;
private final String boundary;
private final MTOMFeature myMtomFeature;
public MtomStreamWriterImpl(XMLStreamWriter w, List<ByteArrayBuffer> mtomAttachments, String b, MTOMFeature myMtomFeature) {
super(w);
this.mtomAttachments = mtomAttachments;
this.boundary = b;
this.myMtomFeature = myMtomFeature;
}
@Override
public void writeBinary(byte[] data, int start, int len, String contentType) throws XMLStreamException {
//check threshold and if less write as base64encoded value
if(myMtomFeature.getThreshold() > len){
writeCharacters(DatatypeConverterImpl._printBase64Binary(data, start, len));
return;
}
ByteArrayBuffer bab = new ByteArrayBuffer(new DataHandler(new ByteArrayDataSource(data, start, len, contentType)), boundary);
writeBinary(bab);
}
@Override
public void writeBinary(DataHandler dataHandler) throws XMLStreamException {
// TODO how do we check threshold and if less inline the data
writeBinary(new ByteArrayBuffer(dataHandler, boundary));
}
@Override
public OutputStream writeBinary(String contentType) throws XMLStreamException {
throw new UnsupportedOperationException();
}
@Override
public void writePCDATA(CharSequence data) throws XMLStreamException {
if(data == null)
return;
if(data instanceof Base64Data){
Base64Data binaryData = (Base64Data)data;
writeBinary(binaryData.getDataHandler());
return;
}
writeCharacters(data.toString());
}
private void writeBinary(ByteArrayBuffer bab) {
try {
mtomAttachments.add(bab);
String prefix = writer.getPrefix(XOP_NAMESPACEURI);
if (prefix == null || !prefix.equals("xop")) {
writer.setPrefix("xop", XOP_NAMESPACEURI);
writer.writeNamespace("xop", XOP_NAMESPACEURI);
}
writer.writeStartElement(XOP_NAMESPACEURI, XOP_LOCALNAME);
writer.writeAttribute("href", "cid:"+bab.contentId);
writer.writeEndElement();
writer.flush();
} catch (XMLStreamException e) {
throw new WebServiceException(e);
}
}
@Override
public Object getProperty(String name) throws IllegalArgumentException {
// Hack for JDK6's SJSXP
if (name.equals("sjsxp-outputstream") && writer instanceof Map) {
Object obj = ((Map) writer).get("sjsxp-outputstream");
if (obj != null) {
return obj;
}
}
return super.getProperty(name);
}
/**
* JAXBMessage writes envelope directly to the OutputStream(for SJSXP, woodstox).
* While writing, it calls the AttachmentMarshaller methods for adding attachments.
* JAXB writes xop:Include in this case.
*/
@Override
public AttachmentMarshaller getAttachmentMarshaller() {
return new AttachmentMarshaller() {
@Override
public String addMtomAttachment(DataHandler data, String elementNamespace, String elementLocalName) {
// Should we do the threshold processing on DataHandler ? But that would be
// expensive as DataHolder need to read the data again from its source
ByteArrayBuffer bab = new ByteArrayBuffer(data, boundary);
mtomAttachments.add(bab);
return "cid:"+bab.contentId;
}
@Override
public String addMtomAttachment(byte[] data, int offset, int length, String mimeType, String elementNamespace, String elementLocalName) {
// inline the data based on the threshold
if (myMtomFeature.getThreshold() > length) {
return null; // JAXB inlines the attachment data
}
ByteArrayBuffer bab = new ByteArrayBuffer(new DataHandler(new ByteArrayDataSource(data, offset, length, mimeType)), boundary);
mtomAttachments.add(bab);
return "cid:"+bab.contentId;
}
@Override
public String addSwaRefAttachment(DataHandler data) {
ByteArrayBuffer bab = new ByteArrayBuffer(data, boundary);
mtomAttachments.add(bab);
return "cid:"+bab.contentId;
}
@Override
public boolean isXOPPackage() {
return true;
}
};
}
public List<ByteArrayBuffer> getMtomAttachments() {
return this.mtomAttachments;
}
@Override
public String getEncoding() {
return XMLStreamWriterUtil.getEncoding(writer);
}
private static class MtomNamespaceContextEx implements NamespaceContextEx {
private final NamespaceContext nsContext;
public MtomNamespaceContextEx(NamespaceContext nsContext) {
this.nsContext = nsContext;
}
@Override
public Iterator<Binding> iterator() {
throw new UnsupportedOperationException();
}
@Override
public String getNamespaceURI(String prefix) {
return nsContext.getNamespaceURI(prefix);
}
@Override
public String getPrefix(String namespaceURI) {
return nsContext.getPrefix(namespaceURI);
}
@Override
public Iterator getPrefixes(String namespaceURI) {
return nsContext.getPrefixes(namespaceURI);
}
}
@Override
public NamespaceContextEx getNamespaceContext() {
NamespaceContext nsContext = writer.getNamespaceContext();
return new MtomNamespaceContextEx(nsContext);
}
}
public static class MtomXMLStreamReaderEx extends XMLStreamReaderFilter implements XMLStreamReaderEx {
/**
* The parser for the outer MIME 'shell'.
*/
private final MimeMultipartParser mimeMP;
private boolean xopReferencePresent = false;
private Base64Data base64AttData;
//To be used with #getTextCharacters
private char[] base64EncodedText;
private String xopHref;
public MtomXMLStreamReaderEx(MimeMultipartParser mimeMP, XMLStreamReader reader) {
super(reader);
this.mimeMP = mimeMP;
}
@Override
public CharSequence getPCDATA() throws XMLStreamException {
if(xopReferencePresent){
return base64AttData;
}
return reader.getText();
}
@Override
public NamespaceContextEx getNamespaceContext() {
return new NamespaceContextExAdaper(reader.getNamespaceContext());
}
@Override
public String getElementTextTrim() throws XMLStreamException {
throw new UnsupportedOperationException();
}
@Override
public int getTextLength() {
if (xopReferencePresent) {
return base64AttData.length();
}
return reader.getTextLength();
}
@Override
public int getTextStart() {
if (xopReferencePresent) {
return 0;
}
return reader.getTextStart();
}
@Override
public int getEventType() {
if(xopReferencePresent)
return XMLStreamConstants.CHARACTERS;
return super.getEventType();
}
@Override
public int next() throws XMLStreamException {
int event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equals(XOP_LOCALNAME) && reader.getNamespaceURI().equals(XOP_NAMESPACEURI)) {
//its xop reference, take the URI reference
String href = reader.getAttributeValue(null, "href");
try {
xopHref = href;
Attachment att = getAttachment(href);
if(att != null){
DataHandler dh = att.asDataHandler();
if (dh instanceof StreamingDataHandler) {
((StreamingDataHandler)dh).setHrefCid(att.getContentId());
}
base64AttData = new Base64Data();
base64AttData.set(dh);
}
xopReferencePresent = true;
} catch (IOException e) {
throw new WebServiceException(e);
}
//move to the </xop:Include>
XMLStreamReaderUtil.nextElementContent(reader);
return XMLStreamConstants.CHARACTERS;
}
if(xopReferencePresent){
xopReferencePresent = false;
base64EncodedText = null;
xopHref = null;
}
return event;
}
private String decodeCid(String cid) {
try {
cid = URLDecoder.decode(cid, "utf-8");
} catch (UnsupportedEncodingException e) {
//on recceiving side lets not fail now, try to look for it
}
return cid;
}
private Attachment getAttachment(String cid) throws IOException {
if (cid.startsWith("cid:"))
cid = cid.substring(4, cid.length());
if (cid.indexOf('%') != -1) {
cid = decodeCid(cid);
return mimeMP.getAttachmentPart(cid);
}
return mimeMP.getAttachmentPart(cid);
}
@Override
public char[] getTextCharacters() {
if (xopReferencePresent) {
char[] chars = new char[base64AttData.length()];
base64AttData.writeTo(chars, 0);
return chars;
}
return reader.getTextCharacters();
}
@Override
public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) throws XMLStreamException {
if(xopReferencePresent){
if(target == null){
throw new NullPointerException("target char array can't be null") ;
}
if(targetStart < 0 || length < 0 || sourceStart < 0 || targetStart >= target.length ||
(targetStart + length ) > target.length) {
throw new IndexOutOfBoundsException();
}
int textLength = base64AttData.length();
if(sourceStart > textLength)
throw new IndexOutOfBoundsException();
if(base64EncodedText == null){
base64EncodedText = new char[base64AttData.length()];
base64AttData.writeTo(base64EncodedText, 0);
}
int copiedLength = Math.min(textLength - sourceStart, length);
System.arraycopy(base64EncodedText, sourceStart , target, targetStart, copiedLength);
return copiedLength;
}
return reader.getTextCharacters(sourceStart, target, targetStart, length);
}
@Override
public String getText() {
if (xopReferencePresent) {
return base64AttData.toString();
}
return reader.getText();
}
protected boolean isXopReference() throws XMLStreamException {
return xopReferencePresent;
}
protected String getXopHref() {
return xopHref;
}
public MimeMultipartParser getMimeMultipartParser() {
return mimeMP;
}
}
}