View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2010 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.panels.teststeps.amf;
14  
15  import java.io.ByteArrayInputStream;
16  import java.io.ByteArrayOutputStream;
17  import java.io.DataInputStream;
18  import java.io.IOException;
19  import java.io.InputStream;
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  
26  import org.apache.commons.httpclient.HostConfiguration;
27  import org.apache.commons.httpclient.HttpState;
28  import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
29  
30  import com.eviware.soapui.SoapUI;
31  import com.eviware.soapui.impl.wsdl.submit.transports.http.support.methods.ExtendedPostMethod;
32  import com.eviware.soapui.impl.wsdl.support.http.HttpClientSupport;
33  import com.eviware.soapui.impl.wsdl.support.http.ProxyUtils;
34  import com.eviware.soapui.model.propertyexpansion.PropertyExpansionContext;
35  
36  import flex.messaging.io.ClassAliasRegistry;
37  import flex.messaging.io.MessageDeserializer;
38  import flex.messaging.io.MessageIOConstants;
39  import flex.messaging.io.SerializationContext;
40  import flex.messaging.io.amf.ActionContext;
41  import flex.messaging.io.amf.ActionMessage;
42  import flex.messaging.io.amf.AmfMessageDeserializer;
43  import flex.messaging.io.amf.AmfMessageSerializer;
44  import flex.messaging.io.amf.MessageBody;
45  import flex.messaging.io.amf.MessageHeader;
46  import flex.messaging.io.amf.client.AMFHeaderProcessor;
47  import flex.messaging.io.amf.client.exceptions.ClientStatusException;
48  import flex.messaging.io.amf.client.exceptions.ServerStatusException;
49  import flex.messaging.io.amf.client.exceptions.ServerStatusException.HttpResponseInfo;
50  
51  /***
52   * AMFConnection derivate using HttpClient instead of UrlConnection
53   * 
54   * @author Ole
55   */
56  
57  public class SoapUIAMFConnection
58  {
59  	private static int DEFAULT_OBJECT_ENCODING = MessageIOConstants.AMF3;
60  
61  	/***
62  	 * Creates a default AMF connection instance.
63  	 */
64  	public SoapUIAMFConnection()
65  	{
66  	}
67  
68  	private ActionContext actionContext;
69  	private boolean connected;
70  	private int objectEncoding;
71  	private boolean objectEncodingSet = false;
72  	private SerializationContext serializationContext;
73  	private String url;
74  
75  	private List<MessageHeader> amfHeaders;
76  	private AMFHeaderProcessor amfHeaderProcessor;
77  	private Map<String, String> httpRequestHeaders;
78  	private int responseCounter;
79  
80  	private ExtendedPostMethod postMethod;
81  	private HttpState httpState = new HttpState();
82  	private PropertyExpansionContext context;
83  
84  	public int getObjectEncoding()
85  	{
86  		if( !objectEncodingSet )
87  			return DEFAULT_OBJECT_ENCODING;
88  
89  		return objectEncoding;
90  	}
91  
92  	public void setObjectEncoding( int objectEncoding )
93  	{
94  		this.objectEncoding = objectEncoding;
95  		objectEncodingSet = true;
96  	}
97  
98  	public String getUrl()
99  	{
100 		return url;
101 	}
102 
103 	/***
104 	 * Adds an AMF packet-level header which is sent with every request for the
105 	 * life of this AMF connection.
106 	 * 
107 	 * @param name
108 	 *           The name of the header.
109 	 * @param mustUnderstand
110 	 *           Whether the header must be processed or not.
111 	 * @param data
112 	 *           The value of the header.
113 	 */
114 	public void addAmfHeader( String name, boolean mustUnderstand, Object data )
115 	{
116 		if( amfHeaders == null )
117 			amfHeaders = new ArrayList<MessageHeader>();
118 
119 		MessageHeader header = new MessageHeader( name, mustUnderstand, data );
120 		amfHeaders.add( header );
121 	}
122 
123 	/***
124 	 * Add an AMF packet-level header with mustUnderstand=false, which is sent
125 	 * with every request for the life of this AMF connection.
126 	 * 
127 	 * @param name
128 	 *           The name of the header.
129 	 * @param data
130 	 *           The value of the header.
131 	 */
132 	public void addAmfHeader( String name, Object data )
133 	{
134 		addAmfHeader( name, false, data );
135 	}
136 
137 	/***
138 	 * Removes any AMF headers found with the name given.
139 	 * 
140 	 * @param name
141 	 *           The name of the header(s) to remove.
142 	 * 
143 	 * @return true if a header existed with the given name.
144 	 */
145 	public boolean removeAmfHeader( String name )
146 	{
147 		boolean exists = false;
148 		if( amfHeaders != null )
149 		{
150 			for( Iterator<MessageHeader> iterator = amfHeaders.iterator(); iterator.hasNext(); )
151 			{
152 				MessageHeader header = iterator.next();
153 				if( name.equals( header.getName() ) )
154 				{
155 					iterator.remove();
156 					exists = true;
157 				}
158 			}
159 		}
160 		return exists;
161 	}
162 
163 	/***
164 	 * Removes all AMF headers.
165 	 */
166 	public void removeAllAmfHeaders()
167 	{
168 		if( amfHeaders != null )
169 			amfHeaders = null;
170 	}
171 
172 	/***
173 	 * Adds a Http request header to the underlying connection.
174 	 * 
175 	 * @param name
176 	 *           The name of the Http header.
177 	 * @param value
178 	 *           The value of the Http header.
179 	 */
180 	public void addHttpRequestHeader( String name, String value )
181 	{
182 		if( httpRequestHeaders == null )
183 			httpRequestHeaders = new HashMap<String, String>();
184 
185 		httpRequestHeaders.put( name, value );
186 	}
187 
188 	/***
189 	 * Removes the Http header found with the name given.
190 	 * 
191 	 * @param name
192 	 *           The name of the Http header.
193 	 * 
194 	 * @return true if a header existed with the given name.
195 	 */
196 	public boolean removeHttpRequestHeader( String name )
197 	{
198 		boolean exists = false;
199 		if( httpRequestHeaders != null )
200 		{
201 			Object previousValue = httpRequestHeaders.remove( name );
202 			exists = ( previousValue != null );
203 		}
204 		return exists;
205 	}
206 
207 	/***
208 	 * Removes all Http request headers.
209 	 */
210 	public void removeAllHttpRequestHeaders()
211 	{
212 		if( httpRequestHeaders != null )
213 			httpRequestHeaders = null;
214 	}
215 
216 	/***
217 	 * Makes an AMF request to the server. A connection must have been made prior
218 	 * to making a call.
219 	 * 
220 	 * @param command
221 	 *           The method to call on the server.
222 	 * @param arguments
223 	 *           Arguments for the method.
224 	 * 
225 	 * @return The result of the call.
226 	 * 
227 	 * @throws ClientStatusException
228 	 *            If there is a client side exception.
229 	 * @throws ServerStatusException
230 	 *            If there is a server side exception.
231 	 */
232 
233 	public Object call( PropertyExpansionContext context, String command, Object... arguments )
234 			throws ClientStatusException, ServerStatusException
235 	{
236 		this.context = context;
237 
238 		if( !connected )
239 		{
240 			String message = "AMF connection is not connected";
241 			ClientStatusException cse = new ClientStatusException( message, ClientStatusException.AMF_CALL_FAILED_CODE );
242 			throw cse;
243 		}
244 
245 		String responseURI = getResponseURI();
246 
247 		ActionMessage requestMessage = new ActionMessage( getObjectEncoding() );
248 
249 		if( amfHeaders != null )
250 		{
251 			for( MessageHeader header : amfHeaders )
252 				requestMessage.addHeader( header );
253 		}
254 
255 		MessageBody amfMessage = new MessageBody( command, responseURI, arguments );
256 		requestMessage.addBody( amfMessage );
257 
258 		// Setup for AMF message serializer
259 		actionContext.setRequestMessage( requestMessage );
260 		ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
261 		AmfMessageSerializer amfMessageSerializer = new AmfMessageSerializer();
262 		amfMessageSerializer.initialize( serializationContext, outBuffer, null/* debugTrace */);
263 
264 		try
265 		{
266 			amfMessageSerializer.writeMessage( requestMessage );
267 			Object result = send( outBuffer );
268 			return result;
269 		}
270 		catch( Exception e )
271 		{
272 			if( e instanceof ClientStatusException )
273 				throw ( ClientStatusException )e;
274 			else if( e instanceof ServerStatusException )
275 				throw ( ServerStatusException )e;
276 			// Otherwise, wrap into a ClientStatusException.
277 			ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CALL_FAILED_CODE );
278 			throw exception;
279 		}
280 		finally
281 		{
282 			try
283 			{
284 				outBuffer.close();
285 			}
286 			catch( IOException ignore )
287 			{
288 			}
289 		}
290 	}
291 
292 	/***
293 	 * Closes the underlying URL connection, sets the url to null, and clears the
294 	 * cookies.
295 	 */
296 	public void close()
297 	{
298 		// Clear the URL connection and URL.
299 		if( postMethod != null )
300 		{
301 			postMethod.releaseConnection();
302 			postMethod = null;
303 		}
304 		url = null;
305 
306 		serializationContext = null;
307 		connected = false;
308 	}
309 
310 	/***
311 	 * Connects to the URL provided. Any previous connections are closed.
312 	 * 
313 	 * @param url
314 	 *           The url to connect to.
315 	 * 
316 	 * @throws ClientStatusException
317 	 *            If there is a client side exception.
318 	 */
319 	public void connect( String url ) throws ClientStatusException
320 	{
321 		if( connected )
322 			close();
323 
324 		this.url = url;
325 		try
326 		{
327 			serializationContext = new SerializationContext();
328 			serializationContext.createASObjectForMissingType = true;
329 			internalConnect();
330 		}
331 		catch( IOException e )
332 		{
333 			ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CONNECT_FAILED_CODE );
334 			throw exception;
335 		}
336 	}
337 
338 	// --------------------------------------------------------------------------
339 	//
340 	// Protected Methods
341 	//
342 	// --------------------------------------------------------------------------
343 
344 	/***
345 	 * Generates the HTTP response info for the server status exception.
346 	 * 
347 	 * @return The HTTP response info for the server status exception.
348 	 */
349 	protected HttpResponseInfo generateHttpResponseInfo()
350 	{
351 		HttpResponseInfo httpResponseInfo = null;
352 		try
353 		{
354 			int responseCode = postMethod.getStatusCode();
355 			String responseMessage = postMethod.getResponseBodyAsString();
356 			httpResponseInfo = new HttpResponseInfo( responseCode, responseMessage );
357 		}
358 		catch( IOException ignore )
359 		{
360 		}
361 		return httpResponseInfo;
362 	}
363 
364 	/***
365 	 * Generates and returns the response URI.
366 	 * 
367 	 * @return The response URI.
368 	 */
369 	protected String getResponseURI()
370 	{
371 		String responseURI = "/" + responseCounter;
372 		responseCounter++ ;
373 		return responseURI;
374 	}
375 
376 	/***
377 	 * An internal method that sets up the underlying URL connection.
378 	 * 
379 	 * @throws IOException
380 	 *            If an exception is encountered during URL connection setup.
381 	 */
382 	protected void internalConnect() throws IOException
383 	{
384 		serializationContext.instantiateTypes = false;
385 		postMethod = new ExtendedPostMethod( url );
386 		setHttpRequestHeaders();
387 		actionContext = new ActionContext();
388 		connected = true;
389 	}
390 
391 	/***
392 	 * Processes the HTTP response headers and body.
393 	 */
394 	protected Object processHttpResponse( InputStream inputStream ) throws ClassNotFoundException, IOException,
395 			ClientStatusException, ServerStatusException
396 	{
397 		return processHttpResponseBody( inputStream );
398 	}
399 
400 	/***
401 	 * Processes the HTTP response body.
402 	 */
403 	protected Object processHttpResponseBody( InputStream inputStream ) throws ClassNotFoundException, IOException,
404 			ClientStatusException, ServerStatusException
405 	{
406 		DataInputStream din = new DataInputStream( inputStream );
407 		ActionMessage message = new ActionMessage();
408 		actionContext.setRequestMessage( message );
409 		MessageDeserializer deserializer = new AmfMessageDeserializer();
410 		deserializer.initialize( serializationContext, din, null/* trace */);
411 		deserializer.readMessage( message, actionContext );
412 		din.close();
413 		context.setProperty( AMFResponse.AMF_RESPONSE_ACTION_MESSAGE, message );
414 		return processAmfPacket( message );
415 	}
416 
417 	/***
418 	 * Processes the AMF packet.
419 	 */
420 	@SuppressWarnings( "unchecked" )
421 	protected Object processAmfPacket( ActionMessage packet ) throws ClientStatusException, ServerStatusException
422 	{
423 		processAmfHeaders( packet.getHeaders() );
424 		return processAmfBody( packet.getBodies() );
425 	}
426 
427 	/***
428 	 * Processes the AMF headers by dispatching them to an AMF header processor,
429 	 * if one exists.
430 	 */
431 	protected void processAmfHeaders( ArrayList<MessageHeader> headers ) throws ClientStatusException
432 	{
433 		// No need to process headers if there's no AMF header processor.
434 		if( amfHeaderProcessor == null )
435 			return;
436 
437 		for( MessageHeader header : headers )
438 			amfHeaderProcessor.processHeader( header );
439 	}
440 
441 	/***
442 	 * Processes the AMF body. Note that this method won't work if batching of
443 	 * AMF messages is supported at some point but for now we are guaranteed to
444 	 * have a single message.
445 	 */
446 	protected Object processAmfBody( ArrayList<MessageBody> messages ) throws ServerStatusException
447 	{
448 		for( MessageBody message : messages )
449 		{
450 			String targetURI = message.getTargetURI();
451 
452 			if( targetURI.endsWith( MessageIOConstants.RESULT_METHOD ) )
453 			{
454 				return message.getData();
455 			}
456 			else if( targetURI.endsWith( MessageIOConstants.STATUS_METHOD ) )
457 			{
458 				// String exMessage = "Server error";
459 				// HttpResponseInfo responseInfo = generateHttpResponseInfo();
460 				// ServerStatusException exception = new ServerStatusException(
461 				// exMessage, message.getData(), responseInfo );
462 
463 				return message.getData();
464 				// throw exception;
465 			}
466 		}
467 		return null; // Should not happen.
468 	}
469 
470 	/***
471 	 * Writes the output buffer and processes the HTTP response.
472 	 */
473 	protected Object send( ByteArrayOutputStream outBuffer ) throws ClassNotFoundException, IOException,
474 			ClientStatusException, ServerStatusException
475 	{
476 		// internalConnect.
477 		internalConnect();
478 
479 		postMethod.setRequestEntity( new ByteArrayRequestEntity( outBuffer.toByteArray() ) );
480 		HostConfiguration hostConfiguration = new HostConfiguration();
481 
482 		ProxyUtils.initProxySettings( context.getModelItem() == null ? SoapUI.getSettings() : context.getModelItem()
483 				.getSettings(), httpState, hostConfiguration, url, context );
484 
485 		HttpClientSupport.getHttpClient().executeMethod( hostConfiguration, postMethod, httpState );
486 
487 		context.setProperty( AMFResponse.AMF_POST_METHOD, postMethod );
488 
489 		return processHttpResponse( responseBodyInputStream() );
490 	}
491 
492 	private ByteArrayInputStream responseBodyInputStream() throws IOException
493 	{
494 		byte[] responseBody = postMethod.getResponseBody();
495 		ByteArrayInputStream bais = new ByteArrayInputStream( responseBody );
496 		context.setProperty( AMFResponse.AMF_RAW_RESPONSE_BODY, responseBody );
497 		return bais;
498 	}
499 
500 	/***
501 	 * Sets the Http request headers, including the cookie headers.
502 	 */
503 	protected void setHttpRequestHeaders()
504 	{
505 		if( httpRequestHeaders != null )
506 		{
507 			for( Map.Entry<String, String> element : httpRequestHeaders.entrySet() )
508 			{
509 				String key = element.getKey();
510 				String value = element.getValue();
511 				postMethod.setRequestHeader( key, value );
512 			}
513 		}
514 	}
515 
516 	/***
517 	 * Registers a custom alias for a class name bidirectionally.
518 	 * 
519 	 * @param alias
520 	 *           The alias for the class name.
521 	 * @param className
522 	 *           The concrete class name.
523 	 */
524 	public static void registerAlias( String alias, String className )
525 	{
526 		ClassAliasRegistry registry = ClassAliasRegistry.getRegistry();
527 		registry.registerAlias( alias, className );
528 		registry.registerAlias( className, alias );
529 	}
530 }