Provides event support for Slide.

Introduction

Event handling in Slide is similar to the way it is done in awt or swing. So if you are not familiar with the concept of event listeners in general, you should have a look at the {@link appropriate sections} in The Java Tutorial

Event dispatching

The event dispatching is done by the {@link org.apache.slide.event.EventDispatcher}. The event dispatcher is implemented using the singleton pattern, so only one instance can exists per VM.

Adding event listeners

Event listeners can be added by configuring them in the Domain.xml file. Every event listener must implement the {@link java.util.EventListener} interface or one of its subinterfaces. So if you want to add a listener that wants to receive content events, you have to register a class that implements the {@link org.apache.slide.event.ContentListener} interface or extends the {@link org.apache.slide.event.ContentAdapter}, a class that provides an empty implementation of every method so that you just have to override the methods you are interested in.

Event listeners must either follow the singleton pattern or provide a default constructor. So if you want to implement a service that shall be accessed from within the VM you should use the singleton pattern. If this is not necessary just provide a default constructor.

Example

The following example will implement an event listener that will log a message everytime a resource is removed:
public class ExampleListener extends ContentAdapter {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    public void remove(ContentEvent event) {
        Domain.log("Recieved remove event");
    }
}

After implenenting the listener it must be registered. This is done by adding the following lines to the Domain.xml file.

<slide>

 ...

    <!-- Event configuration -->
    <events>

        <!-- Adding our event listener -->
        <listener classname="com.mycompany.listener.ExampleListener" />
    </events>
</slide>
        

Enabling events

The registered logger will not log anything until the events you are listening to are enabled. The events may consume some processing power so they can be enabled very fine grained to achieve maximum performance.

Enabling all events

All events can be switched on by default if the following lines are added to the slide.properties file:

# Events
# Default: false
org.apache.slide.events=true

Customizing events

Now every event is fired! This causes a lot of event traffic, so this might no be exactly what we want. We can start from this situation and disable events that we don't like. Here is an example:

<slide>

    ...

    <!-- Event configuration -->
    <events>
        <!-- Disable all events that are fired, when webdav methods are invoked -->
        <event classname="org.apache.slide.webdav.event.WebdavEvent" enable="false"/>

        <!-- Disable all lock event -->
        <event classname="org.apache.slide.event.LockEvent" enable="false"/>

        <!-- Disable all other events... -->

        <!-- Disable all content event... -->
        <event classname="org.apache.slide.event.ContentEvent" enable="false"/>

        <!-- ...but enable the "remove" method -->
        <event classname="org.apache.slide.event.ContentEvent" method="remove" enable="true"/>

        <!-- Adding our event listener -->
        <listener classname="com.mycompany.listener.ExampleListener" />
    </events>
</slide>
        

You can of course do it the other way round: Simply disable all events be default:

# Events
# Default: false
org.apache.slide.events=false

Now we have to switch on the event we are interested in:

<slide>

    ...

    <!-- Event configuration -->
    <events>
        <!-- Enable only the "remove" method -->
        <event classname="org.apache.slide.event.ContentEvent" method="remove" enable="true"/>

        <!-- Adding our event listener -->
        <listener classname="com.mycompany.listener.ExampleListener" />
    </events>
</slide>
        

This approach is preferrable in our scenario because the configuration is a lot shorter and we will never overlook any events that might be fired from some module.

Configuring event listeners

There might be the need for configuring the event listeners. The can be done by implementing the {@link org.apache.slide.util.conf.Configurable} interface. Let's do this with our listener to see how it works.

Example

The following example add the implementation of the {@link org.apache.slide.util.conf.Configurable} to our listener. The message that will be logged shall be configurable:

public class ExampleListener extends ContentAdapter implements Configurable {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    private String logMessage;

    public void remove(ContentEvent event) {
        Domain.log(logMessage);
    }

    public void configure(Configuration configuration) throws ConfigurationException {
        configuration.getConfiguration("log-message").getValue();
    }
}

Now we can configure the log message of our listener by adding the following line to Domain.xml:

<slide>

    ...

    <!-- Event configuration -->
    <events>
        <!-- Enable only the "remove" method -->
        <event classname="org.apache.slide.event.ContentEvent" method="remove" enable="true"/>

        <!-- Adding our event listener -->
        <listener classname="com.mycompany.listener.ExampleListener" >
            <configuration>
                <log-message>Recieved another remove event</log-message>
            </configuration>
        </listener>
    </events>
</slide>
        

The configuration of listeners can be very sophisticated, because they use the {@see org.apache.slide.util.conf configuration mechanism} that is used by slide.

Vetoable events

Almost every event listener method that is called within a transaction is vetaoable. Event listeners can throw a {@link org.apache.slide.event.VetoException}, if they want to enforce the transaction to be rolled back.

This feature allows to implement sophisticated listeners. We want to extend our ExampleListener, so that it denies the storage of all documents that are of the content-type image/gif:

public class ExampleListener extends ContentAdapter implements Configurable {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    private String logMessage;

    public void remove(ContentEvent event) {
        Domain.log(logMessage);
    }

    public void store(ContentEvent event) throws VetoException {
        if ( event.getRevisionDescriptor().getContentType().equals("image/gif") ) {
            throw new VetoException("Storage of gif-images denied!");
        }
    }

    public void configure(Configuration configuration) throws ConfigurationException {
        configuration.getConfiguration("log-message").getValue();
    }
}

Event collections

In some cases it might be valuable to listen to all events that are fired in a transaction as a collection. This can be the case when for example referencial integrity shall be checked. What if a resource is deleted and recreated afterwards? If the references would be checked after each event, the removal of a used resource would fail, even if it will be recreated in the same transaciton. So this is a case, where listening on event collections is a must.

How to listen on collections?

First we have to register the event collector that we prefer. If we want to implement a reference checker, we need a vetoable collection, because we want to deny the deletion of used resources. This can be done by registering the {@link org.apache.slide.event.VetoableEventCollector} as an event listener.

If transaction events are disabled or no event collector is registered, no event collections are fired.

This event collector fires an {@link org.apache.slide.event.EventCollection} just before the transaction is commited. We can still abort the transaction by throwing a {@link org.apache.slide.event.VetoException}.

Now lets extend our listener, so that it will listen to this vetoable collection event:

public class ExampleListener extends ContentAdapter implements EventCollectionListener, Configurable {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    private String logMessage;

    public void remove(ContentEvent event) {
        Domain.log(logMessage);
    }

    public void store(ContentEvent event) throws VetoException {
        if ( event.getRevisionDescriptor().getContentType().equals("image/gif") ) {
            throw new VetoException("Storage of gif-images denied!");
        }
    }

    public void configure(Configuration configuration) throws ConfigurationException {
        configuration.getConfiguration("log-message").getValue();
    }


    public void vetoableCollected(EventCollection collection) throws VetoException {
        ContentEvent[] removedContents = EventCollectionFilter.getRemovedContents(collection);
        for ( int i = 0; i < removedContents.length; i++ ) {
            if ( ReferenceManager.isUsed(removedContents[i].getRevisionDescriptor())) {
                throw new VetoException("Content with Uri='"+removedContents[i].getRevisionDescriptors().getUri()+"' is used!");
            }
        }
    }

    public void collected(EventCollection collection) {
        // do nothing
    }
}

As state before we have not only to register the event collector, but also have to enable the transaction events, because the event collectors rely on transaction event!

So the Domain.xml might look like this

<slide>

    ...

    <!-- Event configuration -->
    <events>
        <!-- Enable transaction events -->
        <event classname="org.apache.slide.event.TransactionEvent" enable="true"/>

        <!-- Enable only the "remove" method -->
        <event classname="org.apache.slide.event.ContentEvent" method="remove" enable="true"/>

        <!-- Enable only the "store" method -->
        <event classname="org.apache.slide.event.ContentEvent" method="store" enable="true"/>

        <!-- Adding the vetoable event collector -->
        <listener classname="org.apache.slide.event.VetoableEventCollector" />

        <!-- Adding our event listener -->
        <listener classname="com.mycompany.listener.ExampleListener" >
            <configuration>
                <log-message>Recieved another remove event</log-message>
            </configuration>
        </listener>
    </events>
</slide>
        

Collection filters

If you have read the source code of our listener carefully, you might have seen that it makes use of the {@link org.apache.slide.event.EventCollectionFilter}. This class provides some useful static methods to filter a collection of events.

We want to check all removed resources, so the used method filters out all events that are revoked by a corresponding content creation event. In other words: If a resource is remove and recreated within the same transaction, this remove event will not be part of the given event array.

Global event listeners

In some cases it might be needed to listen to every event that occurs. If you want to implement a logging on basis of events, this could be the case.

For scenarios like this, there is an interface called {@link org.apache.slide.event.GlobalListener}. This interface provides methods for listening on every event.

We will extend our listener so that it logs every event that is enabled:

public class ExampleListener extends ContentAdapter implements GlobalListener, EventCollectionListener, Configurable {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    private String logMessage;

    public void remove(ContentEvent event) {
        Domain.log(logMessage);
    }

    public void store(ContentEvent event) throws VetoException {
        if ( event.getRevisionDescriptor().getContentType().equals("image/gif") ) {
            throw new VetoException("Storage of gif-images denied!");
        }
    }

    public void configure(Configuration configuration) throws ConfigurationException {
        configuration.getConfiguration("log-message").getValue();
    }


    public void vetoableCollected(EventCollection collection) throws VetoException {
        ContentEvent[] removedContents = EventCollectionFilter.getRemovedContents(collection);
        for ( int i = 0; i < removedContents.length; i++ ) {
            if ( ReferenceManager.isUsed(removedContents[i].getRevisionDescriptor())) {
                throw new VetoException("Content with Uri='"+removedContents[i].getRevisionDescriptors().getUri()+"' is used!");
            }
        }
    }

    public void collected(EventCollection collection) {
        // do nothing
    }

    public void vetoableEventFired(VetoableEventMethod method, EventObject event) throws VetoException {
        Domain.log("Recieved vetoable event with name '"+method.getId()+"': "+event, LOG_CHANNEL, Logger.INFO);
    }

    public void eventFired(EventMethod method, EventObject event) {
        Domain.log("Recieved event with name '"+method.getId()+"': "+event, LOG_CHANNEL, Logger.INFO);
    }
}

The method {@link org.apache.slide.event.GlobalListener#vetoableEventFired(EventMethod,EventObject)} is called, when a vetoable event occurs. The method {@link org.apache.slide.event.GlobalListener#eventFired(EventMethod,EventObject)} is called when non vetoable events are fired.

Beside the event object itself, this method gets the {@link org.apache.slide.event.EventMethod} that occured.

We can use this method to identify which method on the appropriate listener would be called. Let's use this feature handle all events without extending the content adapter anymore:

public class ExampleListener implements GlobalListener, EventCollectionListener, Configurable {
    protected static final String LOG_CHANNEL = ExampleListener.class.getName();

    private String logMessage;

    public void configure(Configuration configuration) throws ConfigurationException {
        configuration.getConfiguration("log-message").getValue();
    }


    public void vetoableCollected(EventCollection collection) throws VetoException {
        ContentEvent[] removedContents = EventCollectionFilter.getRemovedContents(collection);
        for ( int i = 0; i < removedContents.length; i++ ) {
            if ( ReferenceManager.isUsed(removedContents[i].getRevisionDescriptor())) {
                throw new VetoException("Content with Uri='"+removedContents[i].getRevisionDescriptors().getUri()+"' is used!");
            }
        }
    }

    public void collected(EventCollection collection) {
        // do nothing
    }

    public void vetoableEventFired(VetoableEventMethod method, EventObject event) throws VetoException {
        if ( method == ContentEvent.STORE ) && event.getRevisionDescriptor().getContentType().equals("image/gif") ) {
            throw new VetoException("Storage of gif-images denied!");
        } else if ( method == ContentEvent.REMOVE ) {
            Domain.log(logMessage);
        }
        Domain.log("Recieved vetoable event with name '"+method.getId()+"': "+event, LOG_CHANNEL, Logger.INFO);
    }

    public void eventFired(EventMethod method, EventObject event) {
        Domain.log("Recieved event with name '"+method.getId()+"': "+event, LOG_CHANNEL, Logger.INFO);
    }
}

In some scenarios it makes sense to handle events in this way.

Custom events

It is possible to introduce new events and listener types without the need to modify any class in the slide kernel. This feature is for example used in the webdav part. The slide core doesn't know anything about the exisitence of the webdav events, but they can be used and configured in the same way as core events.

Let's implement a new event to see how this works. Imagine a web portal where users can log in to access a registered user area. We want to create a session event that will be fired, when a user tries to log in or a user has logged out.

We want to give listeners the ability to veto the login, for example to be able to build a pluggable user authentication.

The logout event will be fired after logout, so this one is not vetoable

The code for our event looks like this:

public class SessionEvent extends EventObject {
    public final static Login LOGIN = new Login();
    public final static LoggedOut LOGOUT = new Logout();

    public final static String GROUP = "session";
    public final static AbstractEventMethod[] methods = new AbstractEventMethod[] { LOGIN, LOGOUT };

    private Principal user;

    public TransactionEvent(Object source, Principal user) {
        super(source);
        this.user = user;
    }

    public Principal getPrincipal() {
        return user;
    }

    public static class Login extends VetoableEventMethod {
        public Login() {
            super(GROUP, "login");
        }

        public void fireVetaoableEvent(EventListener listener, EventObject event) throws VetoException {
            if ( listener instanceof SessionListener ) ((SessionListener)listener).login((SessionEvent)event);
        }
    }

    public static class Logout extends EventMethod {
        public Logout() {
            super(GROUP, "logout");
        }

        public void fireEvent(EventListener listener, EventObject event) {
            if ( listener instanceof SessionListener ) ((SessionListener)listener).logout((SessionEvent)event);
        }
    }
}

Use this code as a template if you want to implement custom events.

Notes:

After implenting the event we also need a corresponding event listener. The interface for our session event looks like this:

public interface SessionListener extends EventListener {
    public void login(SessionEvent event) throws VetoException;

    public void logout(SessionEvent event);
}

Now we have to fire the events at the appropriate location in our application. This might look like this:

// somewhere in the login method of our application
try {
    if ( SessionEvent.LOGIN.isEnabled() ) {
        EventDispatcher.getInstance().fireVetoableEvent(SessionEvent.LOGIN, new SessionEvent(this, principal));
    }
} catch ( VetoException exception ) {
    // Abort the login process
}

...

// somewhere after user logged out
if ( SessionEvent.LOGOUT.isEnabled() ) {
    EventDispatcher.getInstance().fireEvent(SessionEvent.LOGOUT, new SessionEvent(this, principal));
}

Now the events are fired and session listeners can be implemented and configured in the same way as described above.