/*
* soapUI, copyright (C) 2004-2011 eviware.com
*
* soapUI is free software; you can redistribute it and/or modify it under the
* terms of version 2.1 of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* soapUI 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 Lesser General Public License for more details at gnu.org.
*/
package com.eviware.soapui.impl.wsdl.panels.teststeps.amf;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.methods.ExtendedPostMethod;
import com.eviware.soapui.impl.wsdl.support.http.HttpClientSupport;
import com.eviware.soapui.impl.wsdl.support.http.ProxyUtils;
import com.eviware.soapui.model.propertyexpansion.PropertyExpansionContext;
import flex.messaging.io.ClassAliasRegistry;
import flex.messaging.io.MessageDeserializer;
import flex.messaging.io.MessageIOConstants;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.io.amf.ActionMessage;
import flex.messaging.io.amf.AmfMessageDeserializer;
import flex.messaging.io.amf.AmfMessageSerializer;
import flex.messaging.io.amf.MessageBody;
import flex.messaging.io.amf.MessageHeader;
import flex.messaging.io.amf.client.AMFHeaderProcessor;
import flex.messaging.io.amf.client.exceptions.ClientStatusException;
import flex.messaging.io.amf.client.exceptions.ServerStatusException;
import flex.messaging.io.amf.client.exceptions.ServerStatusException.HttpResponseInfo;
/**
* AMFConnection derivate using HttpClient instead of UrlConnection
*
* @author Ole
*/
public class SoapUIAMFConnection
{
private static int DEFAULT_OBJECT_ENCODING = MessageIOConstants.AMF3;
/**
* Creates a default AMF connection instance.
*/
public SoapUIAMFConnection()
{
}
private ActionContext actionContext;
private boolean connected;
private int objectEncoding;
private boolean objectEncodingSet = false;
private SerializationContext serializationContext;
private String url;
private List<MessageHeader> amfHeaders;
private AMFHeaderProcessor amfHeaderProcessor;
private Map<String, String> httpRequestHeaders;
private int responseCounter;
private ExtendedPostMethod postMethod;
private HttpState httpState = new HttpState();
private PropertyExpansionContext context;
public int getObjectEncoding()
{
if( !objectEncodingSet )
return DEFAULT_OBJECT_ENCODING;
return objectEncoding;
}
public void setObjectEncoding( int objectEncoding )
{
this.objectEncoding = objectEncoding;
objectEncodingSet = true;
}
public String getUrl()
{
return url;
}
/**
* Adds an AMF packet-level header which is sent with every request for the
* life of this AMF connection.
*
* @param name
* The name of the header.
* @param mustUnderstand
* Whether the header must be processed or not.
* @param data
* The value of the header.
*/
public void addAmfHeader( String name, boolean mustUnderstand, Object data )
{
if( amfHeaders == null )
amfHeaders = new ArrayList<MessageHeader>();
MessageHeader header = new MessageHeader( name, mustUnderstand, data );
amfHeaders.add( header );
}
/**
* Add an AMF packet-level header with mustUnderstand=false, which is sent
* with every request for the life of this AMF connection.
*
* @param name
* The name of the header.
* @param data
* The value of the header.
*/
public void addAmfHeader( String name, Object data )
{
addAmfHeader( name, false, data );
}
/**
* Removes any AMF headers found with the name given.
*
* @param name
* The name of the header(s) to remove.
*
* @return true if a header existed with the given name.
*/
public boolean removeAmfHeader( String name )
{
boolean exists = false;
if( amfHeaders != null )
{
for( Iterator<MessageHeader> iterator = amfHeaders.iterator(); iterator.hasNext(); )
{
MessageHeader header = iterator.next();
if( name.equals( header.getName() ) )
{
iterator.remove();
exists = true;
}
}
}
return exists;
}
/**
* Removes all AMF headers.
*/
public void removeAllAmfHeaders()
{
if( amfHeaders != null )
amfHeaders = null;
}
/**
* Adds a Http request header to the underlying connection.
*
* @param name
* The name of the Http header.
* @param value
* The value of the Http header.
*/
public void addHttpRequestHeader( String name, String value )
{
if( httpRequestHeaders == null )
httpRequestHeaders = new HashMap<String, String>();
httpRequestHeaders.put( name, value );
}
/**
* Removes the Http header found with the name given.
*
* @param name
* The name of the Http header.
*
* @return true if a header existed with the given name.
*/
public boolean removeHttpRequestHeader( String name )
{
boolean exists = false;
if( httpRequestHeaders != null )
{
Object previousValue = httpRequestHeaders.remove( name );
exists = ( previousValue != null );
}
return exists;
}
/**
* Removes all Http request headers.
*/
public void removeAllHttpRequestHeaders()
{
if( httpRequestHeaders != null )
httpRequestHeaders = null;
}
/**
* Makes an AMF request to the server. A connection must have been made prior
* to making a call.
*
* @param command
* The method to call on the server.
* @param arguments
* Arguments for the method.
*
* @return The result of the call.
*
* @throws ClientStatusException
* If there is a client side exception.
* @throws ServerStatusException
* If there is a server side exception.
*/
public Object call( PropertyExpansionContext context, String command, Object... arguments )
throws ClientStatusException, ServerStatusException
{
this.context = context;
if( !connected )
{
String message = "AMF connection is not connected";
ClientStatusException cse = new ClientStatusException( message, ClientStatusException.AMF_CALL_FAILED_CODE );
throw cse;
}
String responseURI = getResponseURI();
ActionMessage requestMessage = new ActionMessage( getObjectEncoding() );
if( amfHeaders != null )
{
for( MessageHeader header : amfHeaders )
requestMessage.addHeader( header );
}
MessageBody amfMessage = new MessageBody( command, responseURI, arguments );
requestMessage.addBody( amfMessage );
// Setup for AMF message serializer
actionContext.setRequestMessage( requestMessage );
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
AmfMessageSerializer amfMessageSerializer = new AmfMessageSerializer();
amfMessageSerializer.initialize( serializationContext, outBuffer, null/* debugTrace */);
try
{
amfMessageSerializer.writeMessage( requestMessage );
Object result = send( outBuffer );
return result;
}
catch( Exception e )
{
if( e instanceof ClientStatusException )
throw ( ClientStatusException )e;
else if( e instanceof ServerStatusException )
throw ( ServerStatusException )e;
// Otherwise, wrap into a ClientStatusException.
ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CALL_FAILED_CODE );
throw exception;
}
finally
{
try
{
outBuffer.close();
}
catch( IOException ignore )
{
}
}
}
/**
* Closes the underlying URL connection, sets the url to null, and clears the
* cookies.
*/
public void close()
{
// Clear the URL connection and URL.
if( postMethod != null )
{
postMethod.releaseConnection();
postMethod = null;
}
url = null;
serializationContext = null;
connected = false;
}
/**
* Connects to the URL provided. Any previous connections are closed.
*
* @param url
* The url to connect to.
*
* @throws ClientStatusException
* If there is a client side exception.
*/
public void connect( String url ) throws ClientStatusException
{
if( connected )
close();
this.url = url;
try
{
serializationContext = new SerializationContext();
serializationContext.createASObjectForMissingType = true;
internalConnect();
}
catch( IOException e )
{
ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CONNECT_FAILED_CODE );
throw exception;
}
}
// --------------------------------------------------------------------------
//
// Protected Methods
//
// --------------------------------------------------------------------------
/**
* Generates the HTTP response info for the server status exception.
*
* @return The HTTP response info for the server status exception.
*/
protected HttpResponseInfo generateHttpResponseInfo()
{
HttpResponseInfo httpResponseInfo = null;
try
{
int responseCode = postMethod.getStatusCode();
String responseMessage = postMethod.getResponseBodyAsString();
httpResponseInfo = new HttpResponseInfo( responseCode, responseMessage );
}
catch( IOException ignore )
{
}
return httpResponseInfo;
}
/**
* Generates and returns the response URI.
*
* @return The response URI.
*/
protected String getResponseURI()
{
String responseURI = "/" + responseCounter;
responseCounter++ ;
return responseURI;
}
/**
* An internal method that sets up the underlying URL connection.
*
* @throws IOException
* If an exception is encountered during URL connection setup.
*/
protected void internalConnect() throws IOException
{
serializationContext.instantiateTypes = false;
postMethod = new ExtendedPostMethod( url );
setHttpRequestHeaders();
actionContext = new ActionContext();
connected = true;
}
/**
* Processes the HTTP response headers and body.
*/
protected Object processHttpResponse( InputStream inputStream ) throws ClassNotFoundException, IOException,
ClientStatusException, ServerStatusException
{
return processHttpResponseBody( inputStream );
}
/**
* Processes the HTTP response body.
*/
protected Object processHttpResponseBody( InputStream inputStream ) throws ClassNotFoundException, IOException,
ClientStatusException, ServerStatusException
{
DataInputStream din = new DataInputStream( inputStream );
ActionMessage message = new ActionMessage();
actionContext.setRequestMessage( message );
MessageDeserializer deserializer = new AmfMessageDeserializer();
deserializer.initialize( serializationContext, din, null/* trace */);
deserializer.readMessage( message, actionContext );
din.close();
context.setProperty( AMFResponse.AMF_RESPONSE_ACTION_MESSAGE, message );
return processAmfPacket( message );
}
/**
* Processes the AMF packet.
*/
@SuppressWarnings( "unchecked" )
protected Object processAmfPacket( ActionMessage packet ) throws ClientStatusException, ServerStatusException
{
processAmfHeaders( packet.getHeaders() );
return processAmfBody( packet.getBodies() );
}
/**
* Processes the AMF headers by dispatching them to an AMF header processor,
* if one exists.
*/
protected void processAmfHeaders( ArrayList<MessageHeader> headers ) throws ClientStatusException
{
// No need to process headers if there's no AMF header processor.
if( amfHeaderProcessor == null )
return;
for( MessageHeader header : headers )
amfHeaderProcessor.processHeader( header );
}
/**
* Processes the AMF body. Note that this method won't work if batching of
* AMF messages is supported at some point but for now we are guaranteed to
* have a single message.
*/
protected Object processAmfBody( ArrayList<MessageBody> messages ) throws ServerStatusException
{
for( MessageBody message : messages )
{
String targetURI = message.getTargetURI();
if( targetURI.endsWith( MessageIOConstants.RESULT_METHOD ) )
{
return message.getData();
}
else if( targetURI.endsWith( MessageIOConstants.STATUS_METHOD ) )
{
// String exMessage = "Server error";
// HttpResponseInfo responseInfo = generateHttpResponseInfo();
// ServerStatusException exception = new ServerStatusException(
// exMessage, message.getData(), responseInfo );
return message.getData();
// throw exception;
}
}
return null; // Should not happen.
}
/**
* Writes the output buffer and processes the HTTP response.
*/
protected Object send( ByteArrayOutputStream outBuffer ) throws ClassNotFoundException, IOException,
ClientStatusException, ServerStatusException
{
// internalConnect.
internalConnect();
postMethod.setRequestEntity( new ByteArrayRequestEntity( outBuffer.toByteArray() ) );
HostConfiguration hostConfiguration = new HostConfiguration();
ProxyUtils.initProxySettings( context.getModelItem() == null ? SoapUI.getSettings() : context.getModelItem()
.getSettings(), httpState, hostConfiguration, url, context );
HttpClientSupport.getHttpClient().executeMethod( hostConfiguration, postMethod, httpState );
context.setProperty( AMFResponse.AMF_POST_METHOD, postMethod );
return processHttpResponse( responseBodyInputStream() );
}
private ByteArrayInputStream responseBodyInputStream() throws IOException
{
byte[] responseBody = postMethod.getResponseBody();
ByteArrayInputStream bais = new ByteArrayInputStream( responseBody );
context.setProperty( AMFResponse.AMF_RAW_RESPONSE_BODY, responseBody );
return bais;
}
/**
* Sets the Http request headers, including the cookie headers.
*/
protected void setHttpRequestHeaders()
{
if( httpRequestHeaders != null )
{
for( Map.Entry<String, String> element : httpRequestHeaders.entrySet() )
{
String key = element.getKey();
String value = element.getValue();
postMethod.setRequestHeader( key, value );
}
}
}
/**
* Registers a custom alias for a class name bidirectionally.
*
* @param alias
* The alias for the class name.
* @param className
* The concrete class name.
*/
public static void registerAlias( String alias, String className )
{
ClassAliasRegistry registry = ClassAliasRegistry.getRegistry();
registry.registerAlias( alias, className );
registry.registerAlias( className, alias );
}
}