53

Eclipse's RAP Push Session Revisited

 5 years ago
source link: https://www.tuicool.com/articles/hit/FNnYner
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A few years ago, I wrotean article on Eclipse's RAP Push Session mechanism . To review, the Remote Application Platform team faced the problem that a full-fledged Rich Client application comes with its conundrums when this needs to be implemented with HTTP and JavaScript. A regular rich client can respond to any event that it receives, while HTTP always starts with an event that is spawned in the client itself. Usually, this problem is tackled by polling the event source, and generating a UI event when the event source changes. The Push Session mechanism provided such a mechanism in Eclipse RAP but needed a bit of boilerplate code to make it work. Myprevious post addressed this issue.

I've been using the proposed mechanism for a few years, without too much ado, but there were a few developments which made me reconsider the solution I wrote about. The main ones are:

  1. The solution seemed less efficient for extremely fast events, which caused the Rich Client to freeze every now and then.
  2. I realized that the push session is always activated by event listeners, which allowed for a more efficient design.
  3. The proposed solution sometimes makes handling of Java to JavaScript calls problematic.

The latter issue may need some further explanation! One of the libraries I have developed is an OpenLayers plugin, which is used to create and handle maps. The calls are made in Java, which are transformed to JavaScript functions that are consequently handled by OpenLayers. A callback function notifies listeners about the success of these calls, and are sometimes used to generate new calls to the OpenLayers map.

The nature of this approach in RAP is such that a call has to be completely handled before the next call can be made. If not, a "JavaScript function is already pending"   exception is thrown, and the next call will not be processed. I had worked around this problem by first collecting all the JavaScript calls that are required as a response to an event, prior to calling a synchronize()   function that processes all the calls as a batch.

With the push session mechanism, this approach becomes problematic! An event may notify a number of listeners about a change, and all these may affect the OpenLayers map. As the handling of the event in the UI has to be performed in the UI thread, the result was that all the listeners were processed simultaneously, and the calls to OpenLayers were interfering with each other. As a result, the improved Push Session mechanism I present here will give a notification when all the listeners have been processed, after which the synchronise()   call can be made.

An Improved Push Session Handler

As mentioned earlier, the basic principles of theprevious post are still correct, so I will not dwell too much on the implementation details. The basic premise of the session handler is that a listener can provide a data object (usually a   java.util.EventObject   ), after which a  Display.asynexec()   call will handle this at some point. This is a fairly standard approach for UI handling. The Session handler adds the Push Session mechanism and sees to it that a new push session is started when the handling of the UI has completed.

The improved session handler can collect multiple events, prior to the processing of these events by Display.asynexec() , and sees to it that this call only has to be made once, which greatly improves the UI performance. The resulting code is depicted below

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.rap.rwt.service.ServerPushSession;
import org.eclipse.swt.widgets.Display;

public class RefreshSession {

    private Display display;
    private ServerPushSession session;
    private Collection data;
    private boolean started;
    private boolean refresh;
    private Collection> listeners;
    private Lock lock; 

    public RefreshSession() {
        listeners = new ArrayList>();
        this.started = false;
        this.refresh = false;
        data = new ArrayList<>();
        lock = new ReentrantLock();
    }

    public void addSessionListener( ISessionListener listener ){
        this.listeners.add( listener );
    }

    public void removeSessionListener( ISessionListener listener ){
        this.listeners.remove( listener );
    }

    public void init( Display display ){
        this.display = display;
    }

    /**
     * Called to refresh the UI
     */
    public synchronized void addData( T data ){
        lock.lock();
        try {
            this.data.add( data );
        }
        finally {
            lock.unlock();
        } 
        refresh();
    }

    public void start(){
        session = new ServerPushSession();
        session.start();
        this.started = true;
    }

    public void stop(){
        this.started = false;
    }

    protected void notifyListeners( SessionEvent event ) {
        for( ISessionListener listener: listeners){
            try{
                if( listener != null )
                    listener.notifySessionChanged( event );
            } catch( Exception ex ){
               ex.printStackTrace();
            }
        }
    }

    public void dispose(){
        this.listeners.clear();
        this.display = null;
        this.stop();
    }

    protected void refresh() {
        if( !started || this.refresh || ( display == null ) || ( display.isDisposed()))
            return;
        this.refresh = true;
        display.asyncExec(
            new Runnable() {

                @Override public void run() {
                    lock.lock();
                    try {
                        SessionEvent event = null;
                        for( T dt: data ) {
                            event = new SessionEvent( this, dt );
                            notifyListeners(event);
                        }
                        data.clear();
                        refresh = false;
                        session.stop();
                        notifyListeners( new SessionEvent( this, ISessionListener.EventTypes.COMPLETED, null ));
                    }
                    finally {
                        lock.unlock();
                    }
                    start();
                }
            });
        }
    }
}

This bit of code is fairly similar to my previous post, but is better optimised to handle incoming events. All that remains to be done is to:

  • Add the RefreshSession to a widget, for instance a org.eclipse.swt.Composite class.
  • Call the addData() method in the event handlers.
  • Add a session listener to handle the code.
  • Dispose the RefreshSession when the widget is disposed.

Obviously, this bit of boilerplate code can also be abstracted, resulting in the AbstractSessionHandler depicted below:

import org.eclipse.swt.widgets.Display;
/**
 * Handle the session. This is usually implmenented as an inner class in
 * a composite or other widget
 * @author keesp
 *
 * @param
 */
 public abstract class AbstractSessionHandler {
    private RefreshSession session;
    private ISessionListener listener = new ISessionListener(){
        @Override public void notifySessionChanged(SessionEvent event) {
            onHandleSession( event );
        }
    };

    public void addData( D data ) {
        session.addData(data);
    }

    protected AbstractSessionHandler( Display display ) {
        this.session = new RefreshSession<>();
        this.session.init( display );
        this.session.addSessionListener( listener);
        this.session.start();
    }

    protected abstract void onHandleSession( SessionEvent sevent );
        public void dispose() {
            this.session.removeSessionListener(listener);
            this.session.stop();
        }
    }
}

You can now easily add this handler as an inner class in your widget, as follows:

import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import java.util.EventObject;
import org.eclipse.swt.SWT;

....

public class MyComposite extends Composite {
    private static final long serialVersionUID = 1L;

   ....

    private SessionHandler handler;

    public MyComposite(Composite parent, int style) {
        super(parent, style);
        ....
        handler = new SessionHandler( super.getDisplay());
    }

    @Override
    public void dispose() {
        this.handler.dispose();
        ....
        super.dispose();
    }

    private class SessionHandler extends AbstractSessionHandler<MyEvent> implements IMyListener{
        protected SessionHandler(Display display) {
            super(display);
        }

        @Override protected void onHandleSession(SessionEvent<MyEvent> sevent) {
            try{
                /** HANDLE YOUR EVENT HERE **/
            }
            catch( Exception ex ){
                ex.printStackTrace();
            }
        }

        /**
         * This bit of code implements the event listener and is always the same!
        */
        @Override
        public void notifyChanged(MyEvent event) {
            if( getDisplay().isDisposed() )
                return;
            super.addData(event);
        }
    }
}

Conclusion

With the above code, much of the boilerplate code required to implement a push session in Eclipse RAP has been abstracted away and results in efficient handling of external events by the graphical user interface.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK