/*
* Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.errai.enterprise.client.cdi.api;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.errai.bus.client.ErraiBus;
import org.jboss.errai.bus.client.api.ClientMessageBus;
import org.jboss.errai.bus.client.api.Subscription;
import org.jboss.errai.bus.client.api.base.CommandMessage;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.client.framework.BusState;
import org.jboss.errai.bus.client.framework.ClientMessageBusImpl;
import org.jboss.errai.bus.client.util.BusToolsCli;
import org.jboss.errai.common.client.api.WrappedPortable;
import org.jboss.errai.common.client.api.extension.InitVotes;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.jboss.errai.enterprise.client.cdi.AbstractCDIEventCallback;
import org.jboss.errai.enterprise.client.cdi.CDICommands;
import org.jboss.errai.enterprise.client.cdi.CDIEventTypeLookup;
import org.jboss.errai.enterprise.client.cdi.CDIProtocol;
import org.jboss.errai.enterprise.client.cdi.EventQualifierSerializer;
import org.jboss.errai.enterprise.client.cdi.JsTypeEventObserver;
import org.jboss.errai.enterprise.client.cdi.WindowEventObservers;
import org.jboss.errai.marshalling.client.api.MarshallerFramework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* CDI client interface.
*
* @author Heiko Braun <hbraun@redhat.com>
* @author Christian Sadilek <csadilek@redhat.com>
* @author Mike Brock <cbrock@redhat.com>
*/
public class CDI {
public static final String CDI_SUBJECT_PREFIX = "cdi.event:";
public static final String CDI_SERVICE_SUBJECT_PREFIX = "cdi.event:";
public static final String SERVER_DISPATCHER_SUBJECT = CDI_SERVICE_SUBJECT_PREFIX + "Dispatcher";
public static final String CLIENT_DISPATCHER_SUBJECT = CDI_SERVICE_SUBJECT_PREFIX + "ClientDispatcher";
private static final String CLIENT_ALREADY_FIRED_RESOURCE = CDI_SERVICE_SUBJECT_PREFIX + "AlreadyFired";
private static final Set<String> remoteEvents = new HashSet<>();
private static boolean active = false;
private static Map<String, List<AbstractCDIEventCallback<?>>> eventObservers = new HashMap<>();
private static Set<String> localOnlyObserverTypes = new HashSet<>();
private static Map<String, Collection<String>> lookupTable = Collections.emptyMap();
private static Map<String, List<MessageFireDeferral>> fireOnSubscribe = new LinkedHashMap<>();
private static Logger logger = LoggerFactory.getLogger(CDI.class);
public static final MessageCallback ROUTING_CALLBACK = new MessageCallback() {
@Override
public void callback(final Message message) {
consumeEventFromMessage(message);
}
};
public static String getSubjectNameByType(final String typeName) {
return CDI_SUBJECT_PREFIX + typeName;
}
/**
* Should only be called by bootstrapper for testing purposes.
*/
public void __resetSubsystem() {
for (final String eventType : new HashSet<>(((ClientMessageBus) ErraiBus.get()).getAllRegisteredSubjects())) {
if (eventType.startsWith(CDI_SUBJECT_PREFIX)) {
ErraiBus.get().unsubscribeAll(eventType);
}
}
remoteEvents.clear();
active = false;
fireOnSubscribe.clear();
eventObservers.clear();
localOnlyObserverTypes.clear();
lookupTable = Collections.emptyMap();
}
public void initLookupTable(final CDIEventTypeLookup lookup) {
lookupTable = lookup.getTypeLookupMap();
}
/**
* Return a list of string representations for the qualifiers.
*
* @param qualifiers -
*
* @return
*/
public static Set<String> getQualifiersPart(final Annotation[] qualifiers) {
Set<String> qualifiersPart = null;
if (qualifiers != null) {
for (final Annotation qualifier : qualifiers) {
if (qualifiersPart == null)
qualifiersPart = new HashSet<>(qualifiers.length);
qualifiersPart.add(asString(qualifier));
}
}
return qualifiersPart == null ? Collections.<String>emptySet() : qualifiersPart;
}
private static String asString(final Annotation qualifier) {
return EventQualifierSerializer.get().serialize(qualifier);
}
public static void fireEvent(final Object payload, final Annotation... qualifiers) {
fireEvent(false, payload, qualifiers);
}
public static void fireEvent(final boolean local,
final Object payload,
final Annotation... qualifiers) {
if (payload == null) return;
final Object beanRef;
if (payload instanceof WrappedPortable) {
beanRef = ((WrappedPortable) payload).unwrap();
if (beanRef == null) return;
}
else {
beanRef = payload;
}
final Map<String, Object> messageMap = new HashMap<>();
messageMap.put(MessageParts.CommandType.name(), CDICommands.CDIEvent.name());
messageMap.put(CDIProtocol.BeanType.name(), beanRef.getClass().getName());
messageMap.put(CDIProtocol.BeanReference.name(), beanRef);
messageMap.put(CDIProtocol.FromClient.name(), "1");
if (qualifiers != null && qualifiers.length > 0) {
messageMap.put(CDIProtocol.Qualifiers.name(), getQualifiersPart(qualifiers));
}
consumeEventFromMessage(CommandMessage.createWithParts(messageMap));
if (isRemoteCommunicationEnabled()) {
final CommandMessage withParts = CommandMessage.createWithParts(messageMap);
messageMap.put(MessageParts.ToSubject.name(), SERVER_DISPATCHER_SUBJECT);
fireOnSubscribe(beanRef.getClass().getName(), withParts);
}
}
public static Subscription subscribeLocal(final String eventType, final AbstractCDIEventCallback<?> callback) {
return subscribeLocal(eventType, callback, true);
}
public static Subscription subscribeJsType(final String eventType, final JsTypeEventObserver<?> callback) {
WindowEventObservers.createOrGet().add(eventType, callback);
return new Subscription() {
@Override
public void remove() {
// TODO can't unsubscribe per module atm.
}
};
}
private static Subscription subscribeLocal(final String eventType, final AbstractCDIEventCallback<?> callback,
final boolean isLocalOnly) {
if (!eventObservers.containsKey(eventType)) {
eventObservers.put(eventType, new ArrayList<AbstractCDIEventCallback<?>>());
}
eventObservers.get(eventType).add(callback);
if (isLocalOnly) {
localOnlyObserverTypes.add(eventType);
}
return new Subscription() {
@Override
public void remove() {
unsubscribe(eventType, callback);
}
};
}
public static Subscription subscribe(final String eventType, final AbstractCDIEventCallback<?> callback) {
if (isRemoteCommunicationEnabled() && ErraiBus.get() instanceof ClientMessageBusImpl
&& ((ClientMessageBusImpl) ErraiBus.get()).getState().equals(BusState.CONNECTED)) {
MessageBuilder.createMessage()
.toSubject(CDI.SERVER_DISPATCHER_SUBJECT)
.command(CDICommands.RemoteSubscribe)
.with(CDIProtocol.BeanType, eventType)
.with(CDIProtocol.Qualifiers, callback.getQualifiers())
.noErrorHandling().sendNowWith(ErraiBus.get());
}
return subscribeLocal(eventType, callback, false);
}
private static void unsubscribe(final String eventType, final AbstractCDIEventCallback<?> callback) {
if (eventObservers.containsKey(eventType)) {
eventObservers.get(eventType).remove(callback);
if (!localOnlyObserverTypes.contains(eventType)) {
boolean shouldUnsubscribe = true;
for (final AbstractCDIEventCallback<?> cb : eventObservers.get(eventType)) {
if (cb.getQualifiers().equals(callback.getQualifiers())) {
// found another matching observer -> do not unsubscribe
shouldUnsubscribe = false;
break;
}
}
if (isRemoteCommunicationEnabled() && shouldUnsubscribe) {
MessageBuilder.createMessage()
.toSubject(CDI.SERVER_DISPATCHER_SUBJECT)
.command(CDICommands.RemoteUnsubscribe)
.with(CDIProtocol.BeanType, eventType)
.with(CDIProtocol.Qualifiers, callback.getQualifiers())
.noErrorHandling().sendNowWith(ErraiBus.get());
}
if (eventObservers.get(eventType).isEmpty()) {
eventObservers.remove(eventType);
}
}
}
}
/**
* Informs the server of all active CDI observers currently registered on the
* client. This is not strictly necessary when the client bus first connects,
* because observers register themselves with the server as they are created.
* However, if the QueueSession expires and the bus reconnects, it is
* essential to inform the server of all existing CDI observers so the
* server-side event routing can be established for the new session.
* <p>
* Application code should never have to call this method directly. The Errai
* framework calls this method when required.
*/
public static void resendSubscriptionRequestForAllEventTypes() {
if (isRemoteCommunicationEnabled()) {
int remoteEventCount = 0;
for (final Map.Entry<String, List<AbstractCDIEventCallback<?>>> mapEntry : eventObservers.entrySet()) {
final String eventType = mapEntry.getKey();
if (!localOnlyObserverTypes.contains(eventType)) {
for (final AbstractCDIEventCallback<?> callback : mapEntry.getValue()) {
remoteEventCount++;
MessageBuilder.createMessage()
.toSubject(CDI.SERVER_DISPATCHER_SUBJECT)
.command(CDICommands.RemoteSubscribe)
.with(CDIProtocol.BeanType, eventType)
.with(CDIProtocol.Qualifiers, callback.getQualifiers())
.noErrorHandling().sendNowWith(ErraiBus.get());
}
}
}
logger.info("requested server to forward CDI events for " + remoteEventCount + " existing observers");
}
}
public static void consumeEventFromMessage(final Message message) {
final String beanType = message.get(String.class, CDIProtocol.BeanType);
final Object beanRef = message.get(Object.class, CDIProtocol.BeanReference);
final Set<String> firedBeanTypes = new HashSet<>();
final Deque<String> beanTypeQueue = new LinkedList<>();
beanTypeQueue.addLast(beanType);
firedBeanTypes.add(beanType);
while (!beanTypeQueue.isEmpty()) {
final String curType = beanTypeQueue.poll();
WindowEventObservers.createOrGet().fireEvent(curType, beanRef);
_fireEvent(curType, message);
if (lookupTable.containsKey(curType)) {
for (final String superType : lookupTable.get(curType)) {
if (!firedBeanTypes.contains(superType)) {
beanTypeQueue.addLast(superType);
firedBeanTypes.add(superType);
}
}
}
}
}
private static void _fireEvent(final String beanType, final Message message) {
if (eventObservers.containsKey(beanType)) {
for (final MessageCallback callback : new ArrayList<MessageCallback>(eventObservers.get(beanType))) {
fireIfNotFired(callback, message);
}
}
}
@SuppressWarnings("unchecked")
private static void fireIfNotFired(final MessageCallback callback, final Message message) {
if (!message.hasResource(CLIENT_ALREADY_FIRED_RESOURCE)) {
message.setResource(CLIENT_ALREADY_FIRED_RESOURCE, new IdentityHashMap<>());
}
if (!message.getResource(Map.class, CLIENT_ALREADY_FIRED_RESOURCE).containsKey(callback)) {
callback.callback(message);
message.getResource(Map.class, CLIENT_ALREADY_FIRED_RESOURCE).put(callback, "");
}
}
public static void addRemoteEventType(final String remoteEvent) {
remoteEvents.add(remoteEvent);
if (active) {
fireIfWaiting(remoteEvent);
}
}
private static void fireIfWaiting(final String remoteEvent) {
if (fireOnSubscribe.containsKey(remoteEvent)) {
for (final MessageFireDeferral runnable : fireOnSubscribe.get(remoteEvent)) {
runnable.send();
}
fireOnSubscribe.remove(remoteEvent);
}
}
private static void fireAllIfWaiting() {
for (final String svc : new HashSet<>(fireOnSubscribe.keySet())) {
fireIfWaiting(svc);
}
}
public static void addRemoteEventTypes(final String[] remoteEvent) {
for (final String s : remoteEvent) {
addRemoteEventType(s);
}
}
public static void addPostInitTask(final Runnable runnable) {
InitVotes.registerOneTimeDependencyCallback(CDI.class, runnable);
}
private static void fireOnSubscribe(final String type, final Message message) {
if (MarshallerFramework.canMarshall(type)) {
final MessageFireDeferral deferral = new MessageFireDeferral(System.currentTimeMillis(), message);
if (remoteEvents.contains(type)) {
ErraiBus.get().send(message);
return;
}
List<MessageFireDeferral> runnables = fireOnSubscribe.get(type);
if (runnables == null) {
fireOnSubscribe.put(type, runnables = new ArrayList<>());
}
runnables.add(deferral);
}
}
public static void activate(final String... remoteTypes) {
if (!active) {
addRemoteEventTypes(remoteTypes);
active = true;
fireAllIfWaiting();
logger.info("activated CDI eventing subsystem.");
InitVotes.voteFor(CDI.class);
}
}
static class MessageFireDeferral {
final Message message;
final long time;
MessageFireDeferral(final long time, final Message message) {
this.time = time;
this.message = message;
}
public Message getMessage() {
return message;
}
public long getTime() {
return time;
}
public void send() {
ErraiBus.get().send(message);
}
}
private static boolean isRemoteCommunicationEnabled() {
return BusToolsCli.isRemoteCommunicationEnabled();
}
}