Sharing objects over XMPP

From Eclipsepedia

Jump to: navigation, search

Contents

Intro

Ok, so you have been trying to share objects but whatever you do, neither your objects nor your remote method invocations work. This page hopefully will help you troubleshooting these issues.

Checklist

This is something you'll want to double check when debugging your code: keep in mind that much of both XMPP library (based on SMACK) and ECF have asynchronous and multithreaded processing. You'll have hard times trying to follow the events flow, so watch out!

  • The XMPP container will throw an (uncatched) IOException if:
  1. remote ID is null (maybe you intended null == all clients but the XMPP implementation thinks different)
  2. remote ID is not an XMPPChatRoomID or XMPPID: so use getConnectedID() and not getID() on your clients!
		IContainer clientPrimary = getClient( SLAVE_CLIENT );
		ID clientPrimaryID = clientPrimary.getConnectedID();

Currently the exception gets silently discarded: this behavior means that you won't see any packet leaving your sender client and (quite obviously) no packet being received by remote clients and no message at all appearing on the console. If you want to know what is happening, just place your breakpoint at XMPPContainer.processAsynch(AsynchEvent) method in the org.eclipse.ecf.provider.xmpp plug-in.

  • Object replication will fail if:
  1. the package containing the class the we want to replicate is not exported by the bundle (watch out during tests with ad hoc mocks!) you will get ClassNotFoundException (once again silently ignored). See the SOManager.loadShared(ObjectSharedObjectDescription) method in org.eclipse.ecf.provider if you want to look at the dreaded Class.forName() call.
  2. the class has no default constructor (and once again, reflection will fail and the exception get silenced).

Discovering how the Shared Objects work

This section will briefly outline the working of your shared objects. Do not take the following code as the Right One(TM): it works but it needs to be cleaned up or rethought.

You start defining your shared object by inheriting from BaseSharedObject: this forces to adapt your class hierarchy on ECF desires. I plan to find other ways to work this out. We have to specify the ID of the remote containers we want to share this object with (i.e., the chatroom ID or another client ID):

public class ClassicSharedSalute extends BaseSharedObject implements ISharedSalute {
        // Your properties ...
	private String sentence = "Hello, world!";
 
        // The remote clients IDs ...  
	private ID remoteClient;
 
	/*
	 * The default constructor is needed by the replication API.
	 */
	public ClassicSharedSalute() {
		this.remoteClient = null;
	}
 
        // This c-tor instead is used when creating the primary object
	public ClassicSharedSalute( ID remoteClient ) {
		super();
		this.remoteClient = remoteClient;
	}
 
        // Have to define your modifier methods in a way that the SharedObject notifies changes to remote clients too.
}

Ok, we are mixing domain (the entity we want to share) and infrastructure (ECF code for allowing objects to be shared): until better solutions are found, we just have to adapt. Now it comes the interesting stuff since we have to listen for ECF events in order to share our object:

@Override
protected void initialize() throws SharedObjectInitException {
	super.initialize();
 
        // Initialization of the primary object means that you have to inform remote clients (containers) about the presence of this 
        // shared object and you must do this at the right time too: that is, when ECF asynchronous sharing thread will give you a
        // ISharedObjectActivatedEvent event. Here we add a custom IEventProcessor but we may implement the interface in the same class.
	if (isPrimary()) {
		// If primary, then add an event processor that handles activated
		// event by replicating to all current remote containers
		addEventProcessor(new IEventProcessor() {
			public boolean processEvent(Event event) {
				if (event instanceof ISharedObjectActivatedEvent) {
					// If we've been activated, are primary and are connected
					// then replicate to all remotes
					// This calls the getReplicaDescription method below
					if (isPrimary() && isConnected()) {
						ClassicSharedSalute.this.replicateToRemoteContainers( new ID[] { remoteClient } );
					}
				} 
				return false;
			}
		});
		System.out.println("Primary(" + getContext().getLocalContainerID() + ") says: " + getSentence());
	} else {
                // If this is an object just instantiated, that ECF will give us the values for initializing the first state of the object
                // in form of (key,value) pairs. We just read and set them.
		// This is a replica, so initialize the name from property
		sentence = (String) getConfig().getProperties().get( SENTENCE_PROPERTY );
		System.out.println("Replica(" + getContext().getLocalContainerID() + ") says: " + getSentence());
	}
}

Once the object is initialized you'll want to start notifying remote clients about changes in you object state (for example, when a setProperty() method is called). We have to arrange something of a remote procedure call (RPC) invokation, which in ECF is called SharedObjectMessage:

 
public void setSentence( String sentence ) {
	this.sentence = sentence;
 
        // XXX Ok, we really need a way to avoid the Eternal Ping-Pong of Modification. Suggestions accepted.		
	if (remoteClient != null) {
		final SharedObjectMsg message = SharedObjectMsg.createMsg( "setSentence", new Object[] { sentence } );
 
		try {
			sendSharedObjectMsgTo( remoteClient, message );
		} catch (IOException e) {
			throw new ObjectSharingException( e );
		}
	}
}

Now every time a client will call setSentence() the change will (almost) immediately propagate to remote clients. How are they going to know about such changes? Unfortunately, you have to implement the answer on your own by reimplementing the handleSharedObjectMsg(SharedObjectMsg) method and either use your reflection black magic or the SharedObjectMessage.invoke() helper method:

@Override
protected boolean handleSharedObjectMsg( SharedObjectMsg msg ) {
	try {
		msg.invoke( this );
	} catch (Exception e) {
                // Do whatever you want if you encounter an error during the invocation of the method: here I decided to rethrow an unchecked
                // exception of my own 
		throw new ObjectSharingException( e );
	}
        // we return true since we don't want other clients to process this message (you needs may be different) 
	return true;
}

Testing

If you want to test this, you are going to write something like:

public void testTheClassicWay() throws Exception {
	ClassicSharedSalute primary = new ClassicSharedSalute( getClient( SLAVE_CLIENT ).getConnectedID() );
 
	final ISharedObjectManager managerMaster = getSOManager( MASTER_CLIENT );
	final ID objectID = createSharedObjectID();
 
        // Create the object and wait for remote clients to be notified about it
	final ID id = managerMaster.addSharedObject( objectID, primary, null );
	sleep( 5000 );
 
	// Now change the object state and wait again for message to arrive (your mileage may vary)
	primary.setSentence( NEW_SENTENCE );
	sleep( 5000 );
 
        // Now get the replica from the other client and see if something changed.
	final ISharedObjectManager managerSlave = getSOManager( SLAVE_CLIENT );
	ISharedSalute replica = (ISharedSalute) managerSlave.getSharedObject( objectID );
 
	assertNotNull( replica );
	assertEquals( NEW_SENTENCE, replica.getSentence() );
}

This conclude our troubleshooting guide. An interesting improvement would be the construction of a Dynamic Proxy on top of the domain object to handle the sharing concern or just use an aspect to transparently decorate objects that are properly annotated. More of this soon!

Eclipse Communication Framework
API
API DocumentationJavadocProvidersECF/Bot Framework
Components
ServersShared EditingShared Code Plug-in
Development
Development GuidelinesIntegrators Guide