/**
* Copyright (c) 2002-2012 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.cluster.statemachine;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.neo4j.cluster.BindingListener;
import org.neo4j.cluster.ConnectedStateMachines;
import org.neo4j.cluster.com.message.Message;
import org.neo4j.cluster.com.message.MessageProcessor;
import org.neo4j.cluster.com.message.MessageType;
/**
* Used to generate dynamic proxies whose methods are backed by a {@link StateMachine}. Method
* calls will be translated to the corresponding message, and the parameters are set as payload.
* <p/>
* Methods in the interface to be proxied can either return void or Future<T>. If a method returns
* a future, then the value of it will be set when a message named nameResponse or nameFailure is created,
* where "name" corresponds to the name of the method.
*/
public class StateMachineProxyFactory
implements MessageProcessor, BindingListener
{
private ConnectedStateMachines stateMachines;
private StateMachineConversations conversations;
private volatile URI serverId;
private Map<String, ResponseFuture> responseFutureMap = new ConcurrentHashMap<String, ResponseFuture>();
public StateMachineProxyFactory( ConnectedStateMachines stateMachines, StateMachineConversations conversations )
{
this.stateMachines = stateMachines;
this.conversations = conversations;
}
public <CLIENT> CLIENT newProxy( Class<CLIENT> proxyInterface )
throws IllegalArgumentException
{
// Get the state machine whose messages correspond to the methods of the proxy interface
StateMachine stateMachine = getStateMachine( proxyInterface );
// Create a new dynamic proxy and handler that converts calls to state machine invocations
return proxyInterface.cast( Proxy.newProxyInstance( proxyInterface.getClassLoader(),
new Class<?>[]{proxyInterface}, new StateMachineProxyHandler( this, stateMachine ) ) );
}
Object invoke( StateMachine stateMachine, Method method, Object arg )
throws Throwable
{
if ( method.getName().equals( "toString" ) )
{
return serverId == null ? "" : serverId.toString();
}
if ( method.getName().equals( "equals" ) )
{
return ((StateMachineProxyHandler) Proxy.getInvocationHandler( arg )).getStateMachineProxyFactory()
.serverId.equals( serverId );
}
String conversationId = conversations.getNextConversationId();
try
{
MessageType typeAsEnum = (MessageType) Enum.valueOf( (Class<? extends Enum>) stateMachine.getMessageType
(), method.getName() );
Message<?> message = Message.internal( typeAsEnum, arg );
if ( serverId != null )
{
message.setHeader( Message.CONVERSATION_ID, conversationId ).setHeader( Message.CREATED_BY,
serverId.toString() );
}
if ( method.getReturnType().equals( Void.TYPE ) )
{
stateMachines.process( message );
return null;
}
else
{
ResponseFuture future = new ResponseFuture( typeAsEnum );
responseFutureMap.put( conversationId, future );
stateMachines.process( message );
return future;
}
}
catch ( IllegalArgumentException e )
{
throw new IllegalStateException( "No state machine can handle the method " + method.getName() );
}
}
@Override
public void listeningAt( URI me )
{
serverId = me;
}
@Override
public void process( Message message )
{
if ( !responseFutureMap.isEmpty() )
{
if ( !message.hasHeader( Message.TO ) )
{
String conversationId = message.getHeader( Message.CONVERSATION_ID );
ResponseFuture future = responseFutureMap.get( conversationId );
if ( future != null )
{
if ( future.setPotentialResponse( message ) )
{
responseFutureMap.remove( conversationId );
}
}
}
}
}
private StateMachine getStateMachine( Class<?> proxyInterface )
throws IllegalArgumentException
{
IllegalArgumentException exception = new IllegalArgumentException( "No state machine can handle the " +
"interface:" + proxyInterface.getName() );
statemachine:
for ( StateMachine stateMachine : stateMachines.getStateMachines() )
{
boolean foundMatch = false;
for ( Method method : proxyInterface.getMethods() )
{
if ( !(method.getReturnType().equals( Void.TYPE ) || method.getReturnType().equals( Future.class )) )
{
throw new IllegalArgumentException( "Methods must return either void or Future" );
}
try
{
Enum.valueOf( (Class<? extends Enum>) stateMachine.getMessageType(), method.getName() );
// Ok!
foundMatch = true;
}
catch ( Exception e )
{
if ( foundMatch )
// State machine could only partially handle this interface
{
exception = new IllegalArgumentException( "State machine for " + stateMachine.getMessageType
().getName() + " cannot handle method:" + method.getName() );
}
// Continue searching
continue statemachine;
}
}
// All methods are implemented by this state machine - return it!
return stateMachine;
}
// Could not find any state machine that can handle this interface
throw exception;
}
class ResponseFuture
implements Future<Object>
{
private MessageType initiatedByMessageType;
private Message response;
ResponseFuture( MessageType initiatedByMessageType )
{
this.initiatedByMessageType = initiatedByMessageType;
}
public synchronized boolean setPotentialResponse( Message response )
{
if ( isResponse( response ) )
{
this.response = response;
this.notifyAll();
return true;
}
else
{
return false;
}
}
private boolean isResponse( Message response )
{
return (response.getMessageType().name().equals( initiatedByMessageType.name() + "Response" ) ||
response.getMessageType().name().equals( initiatedByMessageType.name() + "Failure" ));
}
@Override
public boolean cancel( boolean mayInterruptIfRunning )
{
return false;
}
@Override
public boolean isCancelled()
{
return false;
}
@Override
public boolean isDone()
{
return response != null;
}
@Override
public synchronized Object get()
throws InterruptedException, ExecutionException
{
if ( response != null )
{
return getResult();
}
this.wait();
return getResult();
}
private synchronized Object getResult()
throws InterruptedException, ExecutionException
{
if ( response.getMessageType().name().equals( initiatedByMessageType.name() + "Failure" ) )
{
// Call failed
if ( response.getPayload() != null )
{
if ( response.getPayload() instanceof Throwable )
{
throw new ExecutionException( (Throwable) response.getPayload() );
}
else
{
throw new InterruptedException( response.getPayload().toString() );
}
}
else
{
// No message specified
throw new InterruptedException();
}
}
else
{
// Return result
return response.getPayload();
}
}
@Override
public synchronized Object get( long timeout, TimeUnit unit )
throws InterruptedException, ExecutionException, TimeoutException
{
if ( response != null )
{
getResult();
}
this.wait( unit.toMillis( timeout ) );
if ( response == null )
{
throw new TimeoutException();
}
return getResult();
}
}
}