The Vaadin Wiki is temporarily in read only mode due to large recent spam attacks.

Vaadin CDI

IV - Events and contexts

(Redirected from Events and contexts)
WARNING: This wiki page was last edited over a year ago and might be outdated.

In this tutorial we'll create a functional chat feature and improve on the navigation of the application. We'll utilize CDI events and dig a little deeper into scoping.

Messaging #

Let's start off by expanding the ChatView that we created earlier.

	private Layout messageLayout;
	
	private Layout buildUserLayout(User targetUser) {
        VerticalLayout layout = new VerticalLayout();
        layout.setSizeFull();
        layout.setMargin(true);
        layout.setSpacing(true);
        layout.addComponent(new Label("Talking to " + targetUser.getName()));
        layout.addComponent(generateBackButton());
        layout.addComponent(buildChatLayout(targetUser));
        return layout;
    }

    private Component buildChatLayout(final User targetUser) {
        VerticalLayout chatLayout = new VerticalLayout();
        chatLayout.setSizeFull();
        chatLayout.setSpacing(true);
        messageLayout = new VerticalLayout();
        messageLayout.setWidth("100%");
        final TextField messageField = new TextField();
        messageField.setWidth("100%");
        final Button sendButton = new Button("Send");
        sendButton.addClickListener(new ClickListener() {

            @Override
            public void buttonClick(ClickEvent event) {
                // nothing for now
            }
        });
        sendButton.setClickShortcut(KeyCode.ENTER);
        Panel messagePanel = new Panel(messageLayout);
        messagePanel.setHeight("400px");
        messagePanel.setWidth("100%");
        chatLayout.addComponent(messagePanel);
        HorizontalLayout entryLayout = new HorizontalLayout(sendButton,
                messageField);
        entryLayout.setWidth("100%");
        entryLayout.setExpandRatio(messageField, 1);
        entryLayout.setSpacing(true);
        chatLayout.addComponent(entryLayout);
        return chatLayout;
    }

We'll need some sort of a class to store individual messages. Let's go ahead and create one.

package com.vaadin.cdi.tutorial;

import java.util.Date;

public class Message {

    private final User sender;
    private final User recipient;
    private final String message;
    private final Date sendTime;

    public Message(User sender, User recipient, String message) {
        this.sender = sender;
        this.recipient = recipient;
        this.message = message;
        this.sendTime = new Date();
    }

    public Message(User sender, User recipient, String message, Date sendTime) {
        this.sender = sender;
        this.recipient = recipient;
        this.message = message;
        this.sendTime = sendTime;
    }

    public User getSender() {
        return sender;
    }

    public User getRecipient() {
        return recipient;
    }

    public String getMessage() {
        return message;
    }

    public Date getSendTime() {
        return sendTime;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        sb.append(sendTime.toLocaleString());
        sb.append("] ");
        sb.append(sender.getName());
        sb.append(": ");
        sb.append(message);
        return sb.toString();
    }
	
	public boolean involves(User... users) {
		for(User user : users) {
			if(!user.equals(recipient) && !user.equals(sender)) {
				return false;
			}
			return true;
		}
	}
}

We'll be passing instances of this class around as a CDI events as well as storing them in our backend. We'll instantiate a message in the our ClickListener, but to fire it we'll need to inject a JEE Event object into our ChatView first.

	@Inject
    private javax.enterprise.event.Event<Message> messageEvent;

Within Vaadin components you'll unfortunately need to use the fully qualified name as just plain Event would resolve to com.vaadin.ui.Component.Event.

	sendButton.addClickListener(new ClickListener() {
		@Override
		public void buttonClick(ClickEvent event) {
			String message = messageField.getValue();
			if (!message.isEmpty()) {
				messageField.setValue("");
				messageEvent.fire(new Message(userInfo.getUser(),
						targetUser, message));
			}
		}
	});

Now let's add an observer for the event that'll update the sender's UI.

	private static final int MAX_MESSAGES = 16;

	private void observeMessage(@Observes Message message) {
        User currentUser = userInfo.getUser();
        if (message.getRecipient().equals(currentUser)
                || message.getSender().equals(currentUser)) {
            if (messageLayout != null) {
                if (messageLayout.getComponentCount() >= MAX_MESSAGES) {
                    messageLayout.removeComponent(messageLayout
                            .getComponentIterator().next());
                }
                messageLayout.addComponent(new Label(message.toString()));
            }
        }
    }

We'll need to implement some way of propagating this message to the recipient though. CDI events don't propagate to beans in inactive contexts. When we're firing it, the recipient will be in a different UI and View, so we'll have to find a way to route the event. To this end we'll start building ourselves a MessageService.
The service will need at least a way to keep references to the UI's of the active participants. For good measure we'll also include a way to retrieve older messages that were sent to us when we didn't have the UI open.

package com.vaadin.cdi.tutorial;

import java.util.List;

import com.vaadin.navigator.View;
import com.vaadin.ui.UI;

public interface MessageService {

    public List<Message> getLatestMessages(User user1, User user2, int n);

    public void registerParticipant(User user, UI ui);

    public void unregisterParticipant(User user);

}

Let's start building an implementation for the interface. I've called mine MessageServiceImpl (I'm creative). Eclipse autogenerated the following class for me

package com.vaadin.cdi.tutorial;

import java.util.List;

import com.vaadin.ui.UI;

public class MessageServiceImpl implements MessageService {

    @Override
    public List<Message> getLatestMessages(User user1, User user2, int n) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void registerParticipant(User user, UI ui) {
        // TODO Auto-generated method stub

    }

    @Override
    public void unregisterParticipant(User user) {
        // TODO Auto-generated method stub

    }

}

First off, we need to give it the @ApplicationScoped annotation, effectively making it a singleton (at least for our purposes, see the CDI specification for details).

Then we'll add a map between active Users and their UI's. We'll also alter getLatestMessages return an empty list (for now). The resulting class should look something like this:

package com.vaadin.cdi.tutorial;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.enterprise.context.ApplicationScoped;

import com.vaadin.ui.UI;

@ApplicationScoped
public class MessageServiceImpl implements MessageService {

    private Map<User, UI> activeUIMap = new HashMap<User, UI>();

    @Override
    public List<Message> getLatestMessages(User user1, User user2, int n) {
        // TODO Dummy implementation
        return new LinkedList<Message>();
    }

    @Override
    public void registerParticipant(User user, UI ui) {
        activeUIMap.put(user, ui);
    }

    @Override
    public void unregisterParticipant(User user) {
        activeUIMap.remove(user);
    }

}

Then we'll add an observer for the Message event and include an event object, just like in ChatView.

    @Inject
    private Event<Message> messageEvent;

	private void observeMessage(@Observes final Message message) {
        UI recipientUI = activeUIMap.get(message.getRecipient());
        if (recipientUI != null && recipientUI.isAttached()
                && !recipientUI.isClosing()) {
            recipientUI.access(new Runnable() {

                @Override
                public void run() {
                    messageEvent.fire(message);
                }
            });
        }
    }

There is one problem with this approach, namely that the MessageService will catch it's own Message events and get stuck in an infinite loop. We could write a programmatic check in observeMessage, but there's also a CDI mechanism to prevent this sort of thing. Event qualifiers. We'll define a qualifier like this:

package com.vaadin.cdi.tutorial;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Retention(RUNTIME)
@Target({ PARAMETER, FIELD })
public @interface OriginalAuthor {

}

It's basically just an annotation with the CDI annotation @Qualifier. We'll add the qualifier to the Event we injected into ChatView, add the @Any qualifier to the observed parameter in ChatView#observeMessage and finally add the @OriginalAuthor qualifier to the MessageServiceImpl#observeMessage parameter.

ChatView:

	@Inject
    @OriginalAuthor
    private javax.enterprise.event.Event<Message> messageEvent;
	private void observeMessage(@Observes @Any Message message) {

MessageServiceImpl:

	private void observeMessage(@Observes @OriginalAuthor final Message message) {

This way, we fire the event once from ChatView with the @OriginalAuthor qualifier. That event is caught both by the ChatView itself and by the MessageServiceImpl. MessageServiceImpl with then locate the the recipient's UI, access it in a threadsafe way and fire the event again without the qualifier. This time, only the ChatView will catch the event.

To make this work as we expect we'll also need to add the @Push annotation to the UI and register the UI to the MessageService

MyVaadinUI:

@Theme("valo")
@CDIUI("")
@Push
@SuppressWarnings("serial")
public class MyVaadinUI extends UI {

ChatView:

	@Inject
    private MessageService messageService;

	@Override
    public void enter(ViewChangeEvent event) {
        ...
        messageService.registerParticipant(userInfo.getUser(), getUI());
    }
	
	@Override
    public void detach() {
        messageService.unregisterParticipant(userInfo.getUser());
        super.detach();
    }

To see the messages sent to us when we weren't following the right conversation we'll poll the MessageService for past messages and populate the layout with them.

	private Component buildChatLayout(final User targetUser) {
        ...
        for (Message message : messageService.getLatestMessages(
                userInfo.getUser(), targetUser, MAX_MESSAGES)) {
            observeMessage(message);
        }
        ...
    }

Building a simple backend to store the messages isn't really related to CDI so we'll omit that step. The MessageServiceImpl I ended up with is as follows:

package com.vaadin.cdi.tutorial;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.inject.Inject;

import com.vaadin.ui.UI;

@ApplicationScoped
public class MessageServiceImpl implements MessageService {

    public static class Conversation {
        private User user1;
        private User user2;

        public Conversation(User o1, User o2) {
            if (o1.getId() < o2.getId()) {
                user1 = o1;
                user2 = o2;
            } else {
                user1 = o2;
                user2 = o1;
            }
        }

        @Override
        public int hashCode() {
            return (int) (user1.getId() ^ user2.getId());
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Conversation)) {
                return false;
            }
            Conversation c = (Conversation) o;
            return user1.getId() == c.user1.getId()
                    && user2.getId() == c.user2.getId();
        }
    }

    private Map<Conversation, LinkedList<Message>> messageLog = new HashMap<Conversation, LinkedList<Message>>();

    private Map<User, UI> activeUIMap = new HashMap<User, UI>();

    @Inject
    private Event<Message> messageEvent;

    private LinkedList<Message> getMessagesBetween(User user1, User user2) {
        return getMessagesIn(new Conversation(user1, user2));
    }

    private LinkedList<Message> getMessagesIn(Conversation conversation) {
        LinkedList<Message> messages = messageLog.get(conversation);
        if (messages == null) {
            messages = new LinkedList<Message>();
            messageLog.put(conversation, messages);
        }
        return messages;
    }

    @Override
    public List<Message> getLatestMessages(User user1, User user2, int n) {
        LinkedList<Message> messagesBetween = getMessagesBetween(user1, user2);
        Iterator<Message> iterator = messagesBetween.descendingIterator();
        LinkedList<Message> messages = new LinkedList<Message>();
        while (messages.size() < n && iterator.hasNext()) {
            messages.addFirst(iterator.next());
        }
        return messages;
    }

    @Override
    public void registerParticipant(User user, UI ui) {
        activeUIMap.put(user, ui);
    }

    @Override
    public void unregisterParticipant(User user) {
        activeUIMap.remove(user);
    }

    private void observeMessage(@Observes @OriginalAuthor final Message message) {
        User sender = message.getSender();
        User recipient = message.getRecipient();
        getMessagesBetween(sender, recipient).add(message);

        UI recipientUI = activeUIMap.get(message.getRecipient());
        if (recipientUI != null && recipientUI.isAttached()
                && !recipientUI.isClosing()) {
            recipientUI.access(new Runnable() {

                @Override
                public void run() {
                    messageEvent.fire(message);
                }
            });
        }
    }
}

After this the chat feature is largely feature-complete (thank goodness persistence wasn't in the spec).

Navigation #

Right now we navigate through the application by retrieving the UI and it's navigator. This can sometimes cause problems as we may not have easy access to the UI deep within the application code. It's possible to get around this with CDI events. We'll create a NavigationService which will perform two functions: ensure that your UI's have a correctly set up Navigator and listen to custom navigation events.

Here's a simple navigation event:

package com.vaadin.cdi.tutorial;

public class NavigationEvent {
    private final String navigateTo;

    public NavigationEvent(String navigateTo) {
        this.navigateTo = navigateTo;
    }

    public String getNavigateTo() {
        return navigateTo;
    }

}

Let's create a NavigationService and an implementation for it.

package com.vaadin.cdi.tutorial;

import java.io.Serializable;

public interface NavigationService extends Serializable {

    public void onNavigationEvent(@Observes NavigationEvent event);
}

We'll set the implementation as @NormalUIScoped, meaning it's lifecycle is bound to the UI and since it's in a normal scope it can be proxied.

package com.vaadin.cdi.tutorial;

import javax.annotation.PostConstruct;
import javax.enterprise.event.Observes;
import javax.inject.Inject;

import com.vaadin.cdi.CDIViewProvider;
import com.vaadin.cdi.NormalUIScoped;
import com.vaadin.navigator.Navigator;
import com.vaadin.ui.UI;

@NormalUIScoped
public class NavigationServiceImpl implements NavigationService {

    @Inject
    private CDIViewProvider viewProvider;

    @Inject
    private UI ui;

    @PostConstruct
    public void initialize() {
        if (ui.getNavigator() == null) {
            Navigator navigator = new Navigator(ui, ui);
            navigator.addProvider(viewProvider);
        }
    }

    public void onNavigationEvent(@Observes NavigationEvent event) {
        try {
            ui.getNavigator().navigateTo(event.getNavigateTo());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

Now we can simplify the UI initialization a great deal, we'll just have to fire a single NavigationEvent to initialize the navigator and the CDIViewProvider.

package com.vaadin.cdi.tutorial;

import javax.inject.Inject;

import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.cdi.CDIUI;
import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.UI;

@Theme("valo")
@CDIUI("")
@Push
@SuppressWarnings("serial")
public class MyVaadinUI extends UI {

    @Inject
    private javax.enterprise.event.Event<NavigationEvent> navigationEvent;

    @Override
    protected void init(VaadinRequest request) {
        navigationEvent.fire(new NavigationEvent("login"));
    }

}

Similarly, navigation in the views becomes easier.

LoginView:

	@Inject
    private javax.enterprise.event.Event<NavigationEvent> navigationEvent;

    @Override
    public void enter(ViewChangeEvent event) {
		// Remove the reference to the Navigator
		...
	}
	
	@Override
    public void buttonClick(ClickEvent event) {
		...
        navigationEvent.fire(new NavigationEvent("chat"));
    }

ChatView:

    @Inject
    private javax.enterprise.event.Event<NavigationEvent> navigationEvent;
	
    private Button generateBackButton() {
        Button button = new Button("Back");
        button.addClickListener(new ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                navigationEvent.fire(new NavigationEvent(Conventions
                        .deriveMappingForView(ChatView.class)));
            }
        });
        return button;
    }

    private Button generateUserSelectionButton(final User user) {
        Button button = new Button(user.getName());
        button.addClickListener(new ClickListener() {

            @Override
            public void buttonClick(ClickEvent event) {
                navigationEvent.fire(new NavigationEvent(Conventions
                        .deriveMappingForView(ChatView.class)
                        + "/"
                        + user.getUsername()));
            }
        });
        return button;
    }

Now let's suppose we want to add a feature to our NavigationService without touching the original source code. Say we want to log all navigation within the application.Since we used the @NormalUIScoped annotation (instead of @UIScoped) for our NavigationService, we can create a decorator for the class.

package com.vaadin.cdi.tutorial;

import java.util.logging.Logger;

import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.inject.Inject;

@Decorator
public class NavigationLogDecorator implements NavigationService {

    @Inject
    @Delegate
    @Any
    NavigationService delegate;

    @Inject
    private UserInfo userInfo;

    @Override
    public void onNavigationEvent(NavigationEvent event) {
        getLogger().info(
                userInfo.getName() + " navigated to " + event.getNavigateTo());
        delegate.onNavigationEvent(event);
    }

    private Logger getLogger() {
        return Logger.getLogger(this.getClass().getSimpleName());
    }

}

Then we'll have to declare the decorator in the beans.xml:

<beans>
    <alternatives>
        <class>com.vaadin.cdi.tutorial.UserGreetingImpl</class>
    </alternatives>
    <decorators>
        <class>com.vaadin.cdi.tutorial.NavigationLogDecorator</class>
    </decorators>
</beans>
0 Attachments
9655 Views
Average (0 Votes)
Comments
Add Comment
Nice tutorial! I think there is a small bug in the NavigationLogDecorator. You must not use methods with @Observe parameters in a Decorator (JSR-299 §10.4.2).
Posted on 11/18/14 7:23 AM.