/*
* Copyright 2000-2013 Enonic AS
* http://www.enonic.com/license
*/
package com.enonic.cms.api.client.binrpc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.enonic.cms.api.client.ClientException;
/**
* This class implements the invocation handler.
*/
public final class BinRpcInvocationHandler
implements InvocationHandler
{
/**
* Session id cookie.
*/
private final static String SESSION_COOKIE_NAME = "JSESSIONID";
/**
* Content type.
*/
private final static String CONTENT_TYPE_SERIALIZED_OBJECT = "application/x-java-serialized-object";
/**
* Http method post.
*/
private final static String HTTP_METHOD_POST = "POST";
/**
* Http cookie header.
*/
private final static String HTTP_HEADER_COOKIE = "Cookie";
/**
* Http set-cookie header.
*/
private final static String HTTP_HEADER_SET_COOKIE = "Set-Cookie";
/**
* Http content type.
*/
private final static String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
/**
* Http content length.
*/
private final static String HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
/**
* Connection url.
*/
private final String serviceUrl;
/**
* Use global session.
*/
private final boolean useGlobalSession;
/**
* Session id per thread.
*/
private final ThreadLocal<String> localSessionIds;
private final ThreadLocal<Map<String, String>> localCookies;
/**
* Current session id.
*/
private String globalSessionId;
/**
* Construct the invocation handler.
*/
public BinRpcInvocationHandler( String serviceUrl, boolean useGlobalSession )
{
this.serviceUrl = serviceUrl;
this.useGlobalSession = useGlobalSession;
this.localSessionIds = new ThreadLocal<String>();
this.localCookies = new ThreadLocal<Map<String, String>>();
}
/**
* Invoke the method.
*/
public Object invoke( Object object, Method method, Object[] args )
throws Throwable
{
try
{
String methodName = method.getName();
Class<?>[] params = method.getParameterTypes();
BinRpcInvocation invocation = new BinRpcInvocation( methodName, params, args );
BinRpcInvocationResult result = executeRequest( invocation );
return result.recreate();
}
catch ( ClientException e )
{
throw e;
}
catch ( UndeclaredThrowableException e )
{
throw new ClientException( e.getUndeclaredThrowable() );
}
catch ( Throwable e )
{
throw new ClientException( e );
}
}
/**
* Open connection.
*/
private HttpURLConnection openConnection()
throws IOException
{
URLConnection conn = new URL( this.serviceUrl ).openConnection();
if ( !( conn instanceof HttpURLConnection ) )
{
throw new ClientException( "Service URL [" + this.serviceUrl + "] is not an HTTP URL" );
}
return (HttpURLConnection) conn;
}
/**
* Execute the request.
*/
private BinRpcInvocationResult executeRequest( BinRpcInvocation invocation )
throws IOException, ClassNotFoundException
{
ByteArrayOutputStream baos = getByteArrayOutputStream( invocation );
HttpURLConnection conn = openConnection();
prepareConnection( conn, baos.size() );
writeRequestBody( conn, baos );
validateResponse( conn );
checkResponseHeaders( conn );
return readResponseBody( conn );
}
/**
* Prepare connection.
*/
private void prepareConnection( HttpURLConnection conn, int contentLength )
throws IOException
{
conn.setDoOutput( true );
conn.setRequestMethod( HTTP_METHOD_POST );
conn.setRequestProperty( HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_SERIALIZED_OBJECT );
conn.setRequestProperty( HTTP_HEADER_CONTENT_LENGTH, Integer.toString( contentLength ) );
if ( getSessionId() != null )
{
conn.setRequestProperty( HTTP_HEADER_COOKIE, createCookieString() );
}
}
private String createCookieString()
{
StringBuffer buf = new StringBuffer();
if ( getSessionId() != null )
{
buf.append( createSessionCookie() );
}
final Map<String, String> localCookies = this.localCookies.get();
if ( localCookies != null && localCookies.size() > 0 )
{
buf.append( createLocalCookies( localCookies ) );
}
return buf.toString();
}
private String createLocalCookies( Map<String, String> localCookies )
{
StringBuffer buf = new StringBuffer();
for ( String cookieKey : localCookies.keySet() )
{
buf.append( cookieKey ).append( "=" ).append( localCookies.get( cookieKey ) ).append( ";" );
}
return buf.toString();
}
private String createSessionCookie()
{
if ( getSessionId() == null )
{
return null;
}
return SESSION_COOKIE_NAME + "=" + getSessionId() + ";";
}
/**
* Write the request body.
*/
private void writeRequestBody( HttpURLConnection conn, ByteArrayOutputStream baos )
throws IOException
{
baos.writeTo( conn.getOutputStream() );
}
/**
* Validate the response.
*/
private void validateResponse( HttpURLConnection conn )
throws IOException
{
if ( conn.getResponseCode() >= 300 )
{
throw new ClientException(
"Did not receive successful HTTP response: status code = " + conn.getResponseCode() + ", status message = [" +
conn.getResponseMessage() + "]" );
}
}
/**
* Check the session information.
*/
private void checkResponseHeaders( HttpURLConnection conn )
{
List<String> responseCookieHeaders = conn.getHeaderFields().get( HTTP_HEADER_SET_COOKIE );
if ( responseCookieHeaders == null )
{
return;
}
for ( String cookieHeader : responseCookieHeaders )
{
if ( cookieHeader == null )
{
continue;
}
String[] bits = cookieHeader.split( "[=;]" );
if ( SESSION_COOKIE_NAME.equals( bits[0] ) )
{
setSessionId( bits[1] );
}
else if ( bits[0] != null )
{
getLocalCookies().put( bits[0], bits[1] );
}
}
}
private Map<String, String> getLocalCookies()
{
if ( this.localCookies.get() == null )
{
this.localCookies.set( new HashMap<String, String>() );
}
return this.localCookies.get();
}
/**
* Read response body.
*/
private BinRpcInvocationResult readResponseBody( HttpURLConnection conn )
throws IOException, ClassNotFoundException
{
InputStream in = conn.getInputStream();
ObjectInputStream ois = new ObjectInputStream( in );
try
{
Object obj = ois.readObject();
if ( !( obj instanceof BinRpcInvocationResult ) )
{
throw new ClientException(
"Deserialized object needs to be assignable to type [" + BinRpcInvocationResult.class.getName() + "]: " + obj );
}
else
{
return (BinRpcInvocationResult) obj;
}
}
finally
{
ois.close();
}
}
/**
* Return the stream for invocation.
*/
private ByteArrayOutputStream getByteArrayOutputStream( BinRpcInvocation invocation )
throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream( baos );
try
{
oos.writeObject( invocation );
oos.flush();
}
finally
{
oos.close();
}
return baos;
}
/**
* Return the session id.
*/
private String getSessionId()
{
if ( this.useGlobalSession )
{
return this.globalSessionId;
}
else
{
return this.localSessionIds.get();
}
}
/**
* Set the session id.
*/
private void setSessionId( String sessionId )
{
if ( this.useGlobalSession )
{
this.globalSessionId = sessionId;
}
else
{
this.localSessionIds.set( sessionId );
}
}
}