View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2009 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.impl.wsdl.submit.transports.http.support.attachments;
14  
15  import java.io.File;
16  import java.io.FileInputStream;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.net.URI;
20  import java.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  
27  import javax.activation.DataHandler;
28  import javax.mail.MessagingException;
29  import javax.mail.Session;
30  import javax.mail.internet.MimeBodyPart;
31  import javax.mail.internet.MimeMultipart;
32  import javax.mail.internet.PreencodedMimeBodyPart;
33  import javax.wsdl.Input;
34  import javax.wsdl.Output;
35  import javax.xml.namespace.QName;
36  
37  import org.apache.commons.codec.binary.Base64;
38  import org.apache.commons.codec.binary.Hex;
39  import org.apache.log4j.Logger;
40  import org.apache.xmlbeans.SchemaType;
41  import org.apache.xmlbeans.XmlBase64Binary;
42  import org.apache.xmlbeans.XmlCursor;
43  import org.apache.xmlbeans.XmlHexBinary;
44  import org.apache.xmlbeans.XmlObject;
45  
46  import com.eviware.soapui.SoapUI;
47  import com.eviware.soapui.config.PartsConfig;
48  import com.eviware.soapui.config.PartsConfig.Part;
49  import com.eviware.soapui.impl.wsdl.AttachmentContainer;
50  import com.eviware.soapui.impl.wsdl.HttpAttachmentPart;
51  import com.eviware.soapui.impl.wsdl.WsdlAttachmentContainer;
52  import com.eviware.soapui.impl.wsdl.WsdlOperation;
53  import com.eviware.soapui.impl.wsdl.support.MessageXmlPart;
54  import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
55  import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlContext;
56  import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlValidator;
57  import com.eviware.soapui.impl.wsdl.support.xsd.SchemaUtils;
58  import com.eviware.soapui.model.iface.Attachment;
59  import com.eviware.soapui.model.iface.Attachment.AttachmentEncoding;
60  import com.eviware.soapui.support.StringUtils;
61  import com.eviware.soapui.support.Tools;
62  import com.eviware.soapui.support.editor.inspectors.attachments.ContentTypeHandler;
63  import com.eviware.soapui.support.types.StringToStringMap;
64  import com.eviware.soapui.support.xml.XmlUtils;
65  
66  /***
67   * Attachment-related utility classes
68   * 
69   * @author ole.matzura
70   */
71  
72  public class AttachmentUtils
73  {
74  	private final static Logger log = Logger.getLogger( AttachmentUtils.class );
75  	private static final QName XMLMIME_CONTENTTYPE_200505 = new QName( "http://www.w3.org/2005/05/xmlmime",
76  			"contentType" );
77  	private static final QName XMLMIME_CONTENTTYPE_200411 = new QName( "http://www.w3.org/2004/11/xmlmime",
78  			"contentType" );
79  	private static final QName SWAREF_QNAME = new QName( "http://ws-i.org/profiles/basic/1.1/xsd", "swaRef" );
80  	public static final QName XOP_HREF_QNAME = new QName( "href" );
81  	private static final QName XOP_INCLUDE_QNAME = new QName( "http://www.w3.org/2004/08/xop/include", "Include" );
82  	public static final String ROOTPART_SOAPUI_ORG = "<rootpart@soapui.org>";
83  
84  	public static boolean prepareMessagePart( WsdlAttachmentContainer container, MimeMultipart mp,
85  			MessageXmlPart messagePart, StringToStringMap contentIds ) throws Exception, MessagingException
86  	{
87  		boolean isXop = false;
88  
89  		XmlCursor cursor = messagePart.newCursor();
90  
91  		try
92  		{
93  			while( !cursor.isEnddoc() )
94  			{
95  				if( cursor.isContainer() )
96  				{
97  					// could be an attachment part (as of "old" SwA specs which
98  					// specify a content
99  					// element referring to the attachment)
100 					if( messagePart.isAttachmentPart() )
101 					{
102 						String href = cursor.getAttributeText( XOP_HREF_QNAME );
103 						if( href != null && href.length() > 0 )
104 						{
105 							contentIds.put( messagePart.getPart().getName(), href );
106 						}
107 
108 						break;
109 					}
110 
111 					SchemaType schemaType = cursor.getObject().schemaType();
112 
113 					if( AttachmentUtils.isSwaRefType( schemaType ) )
114 					{
115 						String textContent = XmlUtils.getNodeValue( cursor.getDomNode() );
116 						if( StringUtils.hasContent( textContent ) && textContent.startsWith( "cid:" ) )
117 						{
118 							textContent = textContent.substring( 4 );
119 
120 							try
121 							{
122 								// is the textcontent already a URI?
123 								new URI( textContent );
124 								contentIds.put( textContent, textContent );
125 							}
126 							catch( RuntimeException e )
127 							{
128 								// not a URI.. try to create one..
129 								String contentId = textContent + "@soapui.org";
130 								cursor.setTextValue( "cid:" + contentId );
131 								contentIds.put( textContent, contentId );
132 							}
133 						}
134 					}
135 					else if( AttachmentUtils.isXopInclude( schemaType ) )
136 					{
137 						String contentId = cursor.getAttributeText( new QName( "href" ) );
138 						if( contentId != null && contentId.length() > 0 )
139 						{
140 							contentIds.put( contentId, contentId );
141 							isXop = true;
142 
143 							Attachment[] attachments = container.getAttachmentsForPart( contentId );
144 							if( attachments.length == 1 )
145 							{
146 								XmlCursor cur = cursor.newCursor();
147 								if( cur.toParent() )
148 								{
149 									String contentType = getXmlMimeContentType( cur );
150 									if( contentType != null && contentType.length() > 0 )
151 										attachments[0].setContentType( contentType );
152 								}
153 
154 								cur.dispose();
155 							}
156 						}
157 					}
158 					else
159 					{
160 						// extract contentId
161 						String textContent = XmlUtils.getNodeValue( cursor.getDomNode() );
162 						if( StringUtils.hasContent( textContent ) )
163 						{
164 							Attachment attachment = null;
165 							boolean isXopAttachment = false;
166 
167 							// is content a reference to a file?
168 							if( container.isInlineFilesEnabled() && textContent.startsWith( "file:" ) )
169 							{
170 								String filename = textContent.substring( 5 );
171 								if( container.isMtomEnabled() )
172 								{
173 									MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );
174 									String xmimeContentType = getXmlMimeContentType( cursor );
175 
176 									if( StringUtils.isNullOrEmpty( xmimeContentType ) )
177 										xmimeContentType = ContentTypeHandler.getContentTypeFromFilename( filename );
178 
179 									part.setDataHandler( new DataHandler( new XOPPartDataSource( new File( filename ),
180 											xmimeContentType, schemaType ) ) );
181 									part.setContentID( "<" + filename + ">" );
182 									mp.addBodyPart( part );
183 
184 									isXopAttachment = true;
185 								}
186 								else
187 								{
188 									inlineData( cursor, schemaType, new FileInputStream( filename ) );
189 								}
190 							}
191 							// is content a reference to an attachment?
192 							else if( textContent.startsWith( "cid:" ) )
193 							{
194 								textContent = textContent.substring( 4 );
195 
196 								Attachment[] attachments = container.getAttachmentsForPart( textContent );
197 								if( attachments.length == 1 )
198 								{
199 									attachment = attachments[0];
200 								}
201 								else if( attachments.length > 1 )
202 								{
203 									attachment = buildMulitpartAttachment( attachments );
204 								}
205 
206 								isXopAttachment = container.isMtomEnabled();
207 								contentIds.put( textContent, textContent );
208 							}
209 							// content should be binary data; is this an XOP element
210 							// which should be serialized with MTOM?
211 							else if( container.isMtomEnabled() && SchemaUtils.isBinaryType( schemaType )
212 									|| SchemaUtils.isAnyType( schemaType ) )
213 							{
214 								MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );
215 								String xmimeContentType = getXmlMimeContentType( cursor );
216 
217 								part.setDataHandler( new DataHandler( new XOPPartDataSource( textContent, xmimeContentType,
218 										schemaType ) ) );
219 
220 								textContent = "http://www.soapui.org/" + System.nanoTime();
221 
222 								part.setContentID( "<" + textContent + ">" );
223 								mp.addBodyPart( part );
224 
225 								isXopAttachment = true;
226 							}
227 
228 							// add XOP include?
229 							if( isXopAttachment && container.isMtomEnabled() )
230 							{
231 								buildXopInclude( cursor, textContent );
232 								isXop = true;
233 							}
234 							// inline?
235 							else if( attachment != null )
236 							{
237 								inlineAttachment( cursor, schemaType, attachment );
238 							}
239 						}
240 					}
241 				}
242 
243 				cursor.toNextToken();
244 			}
245 		}
246 		finally
247 		{
248 			cursor.dispose();
249 		}
250 
251 		return isXop;
252 	}
253 
254 	private static void inlineAttachment( XmlCursor cursor, SchemaType schemaType, Attachment attachment )
255 			throws Exception
256 	{
257 		inlineData( cursor, schemaType, attachment.getInputStream() );
258 	}
259 
260 	private static void inlineData( XmlCursor cursor, SchemaType schemaType, InputStream in ) throws IOException
261 	{
262 		String content = null;
263 		byte[] data = Tools.readAll( in, -1 ).toByteArray();
264 
265 		if( SchemaUtils.isInstanceOf( schemaType, XmlHexBinary.type ) )
266 		{
267 			content = new String( Hex.encodeHex( data ) );
268 		}
269 		else if( SchemaUtils.isInstanceOf( schemaType, XmlBase64Binary.type ) )
270 		{
271 			content = new String( Base64.encodeBase64( data ) );
272 		}
273 
274 		XmlCursor c = cursor.newCursor();
275 		c.setTextValue( content );
276 		c.dispose();
277 	}
278 
279 	private static void buildXopInclude( XmlCursor cursor, String contentId )
280 	{
281 		// build xop:Include
282 		XmlCursor c = cursor.newCursor();
283 		c.removeXmlContents();
284 		c.toFirstContentToken();
285 		c.beginElement( XOP_INCLUDE_QNAME );
286 		c.insertAttributeWithValue( XOP_HREF_QNAME, "cid:" + contentId );
287 		c.toNextSibling();
288 		c.removeXml();
289 		c.dispose();
290 	}
291 
292 	private static Attachment buildMulitpartAttachment( Attachment[] attachments )
293 	{
294 		System.out.println( "buildMulitpartAttachment(Attachment[] attachments) not implemented!" );
295 		return null;
296 	}
297 
298 	public static String buildRootPartContentType( String action, SoapVersion soapVersion )
299 	{
300 		String contentType = "application/xop+xml; charset=UTF-8; type=\"" + soapVersion.getContentType();
301 		if( soapVersion == SoapVersion.Soap12 )
302 			contentType += "; action=//\"" + action + "//\"";
303 
304 		return contentType + "\"";
305 	}
306 
307 	public static String buildMTOMContentType( String header, String action, SoapVersion soapVersion )
308 	{
309 		int ix = header.indexOf( "boundary" );
310 		String contentType = "multipart/related; type=\"application/xop+xml\"; start=\"" + ROOTPART_SOAPUI_ORG + "\"; "
311 				+ "start-info=\"" + soapVersion.getContentType();
312 
313 		if( soapVersion == SoapVersion.Soap12 && action != null )
314 			contentType += "; action=//\"" + action + "//\"";
315 
316 		return contentType + "\"; " + header.substring( ix );
317 	}
318 
319 	public static boolean isSwaRefType( SchemaType schemaType )
320 	{
321 		return schemaType.getName() != null && schemaType.getName().equals( SWAREF_QNAME );
322 	}
323 
324 	public static String getXmlMimeContentType( XmlCursor cursor )
325 	{
326 		String attributeText = cursor.getAttributeText( XMLMIME_CONTENTTYPE_200411 );
327 		if( attributeText == null )
328 			attributeText = cursor.getAttributeText( XMLMIME_CONTENTTYPE_200505 );
329 		return attributeText;
330 	}
331 	
332 	public static AttachmentEncoding getAttachmentEncoding( WsdlOperation operation,
333 			HttpAttachmentPart httpAttachmentPart, boolean isResponse )
334 	{
335 		if( httpAttachmentPart.getSchemaType() != null )
336 		{
337 			if( SchemaUtils.isInstanceOf( httpAttachmentPart.getSchemaType(), XmlBase64Binary.type ) )
338 				return AttachmentEncoding.BASE64;
339 			else if( SchemaUtils.isInstanceOf( httpAttachmentPart.getSchemaType(), XmlHexBinary.type ) )
340 				return AttachmentEncoding.HEX;
341 		}
342 
343 		return getAttachmentEncoding( operation, httpAttachmentPart.getName(), isResponse );
344 	}
345 
346 	public static AttachmentEncoding getAttachmentEncoding( WsdlOperation operation, String partName, boolean isResponse )
347 	{
348 		// make sure we have access
349 		if( operation == null || operation.getBindingOperation() == null
350 				|| operation.getBindingOperation().getOperation() == null )
351 		{
352 			return AttachmentEncoding.NONE;
353 		}
354 
355 		javax.wsdl.Part part = null;
356 
357 		if( isResponse )
358 		{
359 			Output output = operation.getBindingOperation().getOperation().getOutput();
360 			if( output == null || output.getMessage() == null )
361 			{
362 				return AttachmentEncoding.NONE;
363 			}
364 			else
365 			{
366 				part = output.getMessage().getPart( partName );
367 			}
368 		}
369 		else
370 		{
371 			Input input = operation.getBindingOperation().getOperation().getInput();
372 			if( input == null || input.getMessage() == null )
373 			{
374 				return AttachmentEncoding.NONE;
375 			}
376 			else
377 			{
378 				part = input.getMessage().getPart( partName );
379 			}
380 		}
381 
382 		if( part != null )
383 		{
384 			QName typeName = part.getTypeName();
385 			if( typeName.getNamespaceURI().equals( "http://www.w3.org/2001/XMLSchema" ) )
386 			{
387 				if( typeName.getLocalPart().equals( "base64Binary" ) )
388 				{
389 					return AttachmentEncoding.BASE64;
390 				}
391 				else if( typeName.getLocalPart().equals( "hexBinary" ) )
392 				{
393 					return AttachmentEncoding.HEX;
394 				}
395 			}
396 		}
397 
398 		return AttachmentEncoding.NONE;
399 	}
400 
401 	public static boolean isXopInclude( SchemaType schemaType )
402 	{
403 		return XOP_INCLUDE_QNAME.equals( schemaType.getName() );
404 	}
405 
406 	public static List<HttpAttachmentPart> extractAttachmentParts( WsdlOperation operation, String messageContent,
407 			boolean addAnonymous, boolean isResponse, boolean forceMtom )
408 	{
409 		List<HttpAttachmentPart> result = new ArrayList<HttpAttachmentPart>();
410 
411 		PartsConfig messageParts = isResponse ? operation.getConfig().getResponseParts() : operation.getConfig()
412 				.getRequestParts();
413 		if( messageParts != null )
414 		{
415 			for( Part part : messageParts.getPartList() )
416 			{
417 				HttpAttachmentPart attachmentPart = new HttpAttachmentPart( part.getName(), part.getContentTypeList() );
418 				attachmentPart.setType( Attachment.AttachmentType.MIME );
419 				result.add( attachmentPart );
420 			}
421 		}
422 
423 		if( messageContent.length() > 0 )
424 		{
425 			WsdlContext wsdlContext = operation.getInterface().getWsdlContext();
426 			WsdlValidator validator = new WsdlValidator( wsdlContext );
427 			try
428 			{
429 				XmlObject[] requestDocuments = validator.getMessageParts( messageContent, operation.getName(), isResponse );
430 
431 				for( XmlObject partDoc : requestDocuments )
432 				{
433 					XmlCursor cursor = partDoc.newCursor();
434 					while( !cursor.isEnddoc() )
435 					{
436 						if( cursor.isContainer() )
437 						{
438 							SchemaType schemaType = cursor.getObject().schemaType();
439 							if( schemaType != null )
440 							{
441 								String attributeText = AttachmentUtils.getXmlMimeContentType( cursor );
442 
443 								// xop?
444 								if( SchemaUtils.isBinaryType( schemaType ) || SchemaUtils.isAnyType( schemaType ) )
445 								{
446 									String contentId = cursor.getTextValue();
447 									if( contentId.startsWith( "cid:" ) )
448 									{
449 										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId.substring( 4 ),
450 												attributeText );
451 										attachmentPart
452 												.setType( attributeText == null && !forceMtom ? Attachment.AttachmentType.CONTENT
453 														: Attachment.AttachmentType.XOP );
454 										result.add( attachmentPart );
455 									}
456 								}
457 								else if( AttachmentUtils.isXopInclude( schemaType ) )
458 								{
459 									String contentId = cursor.getAttributeText( new QName( "href" ) );
460 									if( contentId != null && contentId.length() > 0 )
461 									{
462 										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId, attributeText );
463 										attachmentPart.setType( Attachment.AttachmentType.XOP );
464 										result.add( attachmentPart );
465 									}
466 								}
467 								// swaref?
468 								else if( AttachmentUtils.isSwaRefType( schemaType ) )
469 								{
470 									String contentId = cursor.getTextValue();
471 									if( contentId.startsWith( "cid:" ) )
472 									{
473 										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId.substring( 4 ),
474 												attributeText );
475 										attachmentPart.setType( Attachment.AttachmentType.SWAREF );
476 										result.add( attachmentPart );
477 									}
478 								}
479 							}
480 						}
481 
482 						cursor.toNextToken();
483 					}
484 				}
485 			}
486 			catch( Exception e )
487 			{
488 				if( e instanceof NullPointerException )
489 					SoapUI.logError( e );
490 				log.warn( e.getMessage() );
491 			}
492 		}
493 
494 		if( addAnonymous )
495 			result.add( new HttpAttachmentPart() );
496 
497 		return result;
498 	}
499 
500 	/***
501 	 * Adds defined attachments as mimeparts
502 	 */
503 
504 	public static void addMimeParts( AttachmentContainer container, List<Attachment> attachments, MimeMultipart mp,
505 			StringToStringMap contentIds ) throws MessagingException
506 	{
507 		// no multipart handling?
508 		if( !container.isMultipartEnabled() )
509 		{
510 			for( int c = 0; c < attachments.size(); c++ )
511 			{
512 				Attachment att = attachments.get( c );
513 				if( att.getAttachmentType() != Attachment.AttachmentType.CONTENT )
514 				{
515 					addSingleAttachment( mp, contentIds, att );
516 				}
517 			}
518 		}
519 		else
520 		{
521 			// first identify if any part has more than one attachments
522 			Map<String, List<Attachment>> attachmentsMap = new HashMap<String, List<Attachment>>();
523 			for( int c = 0; c < attachments.size(); c++ )
524 			{
525 				Attachment att = attachments.get( c );
526 				if( att.getAttachmentType() == Attachment.AttachmentType.CONTENT )
527 					continue;
528 
529 				String partName = att.getPart();
530 
531 				if( !attachmentsMap.containsKey( partName ) )
532 				{
533 					attachmentsMap.put( partName, new ArrayList<Attachment>() );
534 				}
535 
536 				attachmentsMap.get( partName ).add( att );
537 			}
538 
539 			// add attachments
540 			for( Iterator<String> i = attachmentsMap.keySet().iterator(); i.hasNext(); )
541 			{
542 				attachments = attachmentsMap.get( i.next() );
543 				if( attachments.size() == 1 )
544 				{
545 					Attachment att = attachments.get( 0 );
546 					addSingleAttachment( mp, contentIds, att );
547 				}
548 				// more than one attachment with the same part -> create multipart
549 				// attachment
550 				else if( attachments.size() > 1 )
551 				{
552 					addMultipartAttachment( mp, contentIds, attachments );
553 				}
554 			}
555 		}
556 	}
557 
558 	/***
559 	 * Adds a mulitpart MimeBodyPart from an array of attachments
560 	 */
561 
562 	public static void addMultipartAttachment( MimeMultipart mp, StringToStringMap contentIds,
563 			List<Attachment> attachments ) throws MessagingException
564 	{
565 		MimeMultipart multipart = new MimeMultipart( "mixed" );
566 		for( int c = 0; c < attachments.size(); c++ )
567 		{
568 			Attachment att = attachments.get( c );
569 			String contentType = att.getContentType();
570 
571 			MimeBodyPart part = contentType.startsWith( "text/" ) ? new MimeBodyPart() : new PreencodedMimeBodyPart(
572 					"binary" );
573 
574 			part.setDataHandler( new DataHandler( new AttachmentDataSource( att ) ) );
575 			initPartContentId( contentIds, part, att, false );
576 			multipart.addBodyPart( part );
577 		}
578 
579 		MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );
580 		part.setDataHandler( new DataHandler( new MultipartAttachmentDataSource( multipart ) ) );
581 
582 		Attachment attachment = attachments.get( 0 );
583 		initPartContentId( contentIds, part, attachment, true );
584 
585 		mp.addBodyPart( part );
586 	}
587 
588 	public static void initPartContentId( StringToStringMap contentIds, MimeBodyPart part, Attachment attachment,
589 			boolean isMultipart ) throws MessagingException
590 	{
591 		String partName = attachment.getPart();
592 
593 		String contentID = attachment.getContentID();
594 		if( contentID != null )
595 		{
596 			contentID = contentID.trim();
597 			int ix = contentID.indexOf( ' ' );
598 			if( ix != -1 )
599 				part.setContentID( "<" + ( isMultipart ? contentID.substring( ix + 1 ) : contentID.substring( 0, ix ) )
600 						+ ">" );
601 			else
602 			{
603 				if( !contentID.startsWith( "<" ) )
604 					contentID = "<" + contentID;
605 
606 				if( !contentID.endsWith( ">" ) )
607 					contentID = contentID + ">";
608 
609 				part.setContentID( contentID );
610 			}
611 		}
612 		else if( partName != null && !partName.equals( HttpAttachmentPart.ANONYMOUS_NAME ) )
613 		{
614 			if( contentIds.containsKey( partName ) )
615 			{
616 				part.setContentID( "<" + contentIds.get( partName ) + ">" );
617 			}
618 			else
619 			{
620 				part.setContentID( "<" + partName + "=" + System.nanoTime() + "@soapui.org>" );
621 			}
622 		}
623 	}
624 
625 	/***
626 	 * Adds a simple MimeBodyPart from an attachment
627 	 */
628 
629 	public static void addSingleAttachment( MimeMultipart mp, StringToStringMap contentIds, Attachment att )
630 			throws MessagingException
631 	{
632 		String contentType = att.getContentType();
633 		MimeBodyPart part = contentType.startsWith( "text/" ) ? new MimeBodyPart()
634 				: new PreencodedMimeBodyPart( "binary" );
635 
636 		part.setDataHandler( new DataHandler( new AttachmentDataSource( att ) ) );
637 		initPartContentId( contentIds, part, att, false );
638 
639 		mp.addBodyPart( part );
640 	}
641 
642 	public static final Session JAVAMAIL_SESSION = Session.getDefaultInstance( new Properties() );
643 }