/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.hazelcast.adapters; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.granite.clustering.TransientReference; import org.granite.context.GraniteContext; import org.granite.gravity.Channel; import org.granite.gravity.MessageReceivingException; import org.granite.gravity.Subscription; import org.granite.gravity.adapters.ServiceAdapter; import org.granite.gravity.selector.TopicMatcher; import org.granite.logging.Logger; import org.granite.messaging.service.ServiceException; import org.granite.messaging.webapp.ServletGraniteContext; import org.granite.util.XMap; import com.hazelcast.config.Config; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.ITopic; import com.hazelcast.core.Message; import flex.messaging.messages.AcknowledgeMessage; import flex.messaging.messages.AsyncMessage; import flex.messaging.messages.CommandMessage; import flex.messaging.messages.ErrorMessage; /** * @author William DRAI */ public class HazelcastServiceAdapter extends ServiceAdapter { private static final Logger log = Logger.getLogger(HazelcastServiceAdapter.class); private static final String HAZELCASTCLIENT_KEY_PREFIX = "org.granite.gravity.hazelcastClient."; protected ConcurrentMap<String, HazelcastClient> hazelcastClients = new ConcurrentHashMap<String, HazelcastClient>(); private HazelcastInstance hazelcastInstance; protected boolean noLocal = false; protected boolean sessionSelector = false; @Override public void configure(XMap adapterProperties, XMap destinationProperties) throws ServiceException { if (Boolean.TRUE.toString().equals(destinationProperties.get("no-local"))) noLocal = true; if (Boolean.TRUE.toString().equals(destinationProperties.get("session-selector"))) sessionSelector = true; String instanceName = destinationProperties.get("hazelcast-instance-name"); if (instanceName == null) instanceName = "__GDS_HAZELCAST_INSTANCE__"; Config config = new Config(); config.setInstanceName(instanceName); hazelcastInstance = Hazelcast.getOrCreateHazelcastInstance(config); } @Override public void start() throws ServiceException { super.start(); } @Override public void stop() throws ServiceException { super.stop(); for (HazelcastClient hazelcastClient : hazelcastClients.values()) hazelcastClient.close(); hazelcastClients.clear(); } private synchronized HazelcastClient initClient(Channel client, ITopic<flex.messaging.messages.Message> destination) throws Exception { HazelcastClient hazelcastClient = hazelcastClients.get(client.getId()); if (hazelcastClient == null) { hazelcastClient = new HazelcastClient(client, destination); hazelcastClients.put(client.getId(), hazelcastClient); if (sessionSelector && GraniteContext.getCurrentInstance() instanceof ServletGraniteContext) ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getSessionMap().put(HAZELCASTCLIENT_KEY_PREFIX + destination.getName(), hazelcastClient); log.debug("Hazelcast client started for channel " + client.getId()); } return hazelcastClient; } private synchronized void closeClientIfNecessary(Channel client, ITopic<flex.messaging.messages.Message> destination) throws Exception { HazelcastClient hazelcastClient = hazelcastClients.get(client.getId()); if (hazelcastClient != null && !hazelcastClient.hasActiveSubscription()) { hazelcastClient.close(); hazelcastClients.remove(client.getId()); if (sessionSelector && GraniteContext.getCurrentInstance() instanceof ServletGraniteContext) ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getSessionMap().remove(HAZELCASTCLIENT_KEY_PREFIX + destination.getName()); log.debug("Hazelcast client closed for channel " + client.getId()); } } @Override public Object invoke(Channel fromClient, AsyncMessage message) { String topicId = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER); if (getSecurityPolicy().canPublish(fromClient, topicId, message)) { try { ITopic<flex.messaging.messages.Message> topic = hazelcastInstance.getTopic(message.getDestination()); topic.publish(message); AsyncMessage reply = new AcknowledgeMessage(message); reply.setMessageId(message.getMessageId()); return reply; } catch (Exception e) { log.error(e, "Error sending message"); ErrorMessage error = new ErrorMessage(message, null); error.setFaultString("JMS Adapter error " + e.getMessage()); return error; } } log.debug("Channel %s tried to publish a message to topic %s", fromClient, topicId); ErrorMessage error = new ErrorMessage(message, null); error.setFaultString("Server.Publish.Denied"); return error; } @Override public Object manage(Channel fromClient, CommandMessage message) { String topicId = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER); if (message.getOperation() == CommandMessage.SUBSCRIBE_OPERATION) { if (getSecurityPolicy().canSubscribe(fromClient, topicId, message)) { try { ITopic<flex.messaging.messages.Message> destination = hazelcastInstance.getTopic(message.getDestination()); HazelcastClient hazelcastClient = initClient(fromClient, destination); hazelcastClient.subscribe(message); AsyncMessage reply = new AcknowledgeMessage(message); return reply; } catch (Exception e) { throw new RuntimeException("JMSAdapter subscribe error on topic: " + message, e); } } log.debug("Channel %s tried to subscribe to topic %s", fromClient, topicId); ErrorMessage error = new ErrorMessage(message, null); error.setFaultString("Server.Subscribe.Denied"); return error; } else if (message.getOperation() == CommandMessage.UNSUBSCRIBE_OPERATION) { try { ITopic<flex.messaging.messages.Message> destination = hazelcastInstance.getTopic(message.getDestination()); HazelcastClient hazelcastClient = initClient(fromClient, destination); hazelcastClient.unsubscribe(message); closeClientIfNecessary(fromClient, destination); AsyncMessage reply = new AcknowledgeMessage(message); return reply; } catch (Exception e) { throw new RuntimeException("JMSAdapter unsubscribe error on topic: " + message, e); } } return null; } @TransientReference private class HazelcastClient { private final Channel client; private final ITopic<flex.messaging.messages.Message> destination; private String topic; private final ConcurrentMap<String, HazelcastSubscription> subscriptions = new ConcurrentHashMap<String, HazelcastSubscription>(); public HazelcastClient(Channel client, ITopic<flex.messaging.messages.Message> destination) { this.client = client; this.destination = destination; } public boolean hasActiveSubscription() { return subscriptions != null && !subscriptions.isEmpty(); } public void close() { for (HazelcastSubscription subscription : subscriptions.values()) subscription.close(); subscriptions.clear(); } public void subscribe(CommandMessage message) throws Exception { String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER); String selector = (String)message.getHeader(CommandMessage.SELECTOR_HEADER); this.topic = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER); internalSubscribe(subscriptionId, selector, this.topic); } private void internalSubscribe(String subscriptionId, String selector, String topic) throws Exception { synchronized (subscriptions) { HazelcastSubscription subscription = subscriptions.get(subscriptionId); Subscription sub = client.addSubscription(destination.getName(), topic, subscriptionId, noLocal); if (subscription == null) { subscription = new HazelcastSubscription(client, sub, destination, topic); subscriptions.put(subscriptionId, subscription); } else sub.setSelector(selector); } } public void unsubscribe(CommandMessage message) throws Exception { String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER); synchronized (subscriptions) { HazelcastSubscription subscription = subscriptions.get(subscriptionId); try { if (subscription != null) subscription.close(); } finally { subscriptions.remove(subscriptionId); client.removeSubscription(subscriptionId); } } } } @TransientReference private class HazelcastSubscription implements com.hazelcast.core.MessageListener<flex.messaging.messages.Message> { private final String id; private final Channel client; private final Subscription subscription; private final ITopic<flex.messaging.messages.Message> destination; private final String topic; public HazelcastSubscription(Channel client, Subscription subscription, ITopic<flex.messaging.messages.Message> destination, String topic) { this.client = client; this.subscription = subscription; this.destination = destination; this.topic = topic; this.id = destination.addMessageListener(this); } public void close() { destination.removeMessageListener(id); } @Override public void onMessage(Message<flex.messaging.messages.Message> message) { log.debug("Delivering message to channel %s subscription %s", client.getId(), subscription.getSubscriptionId()); if (message.getMessageObject() instanceof AsyncMessage) { String topic = (String)message.getMessageObject().getHeader(AsyncMessage.SUBTOPIC_HEADER); if (!TopicMatcher.matchesTopic(this.topic, topic)) return; if (!subscription.accept(client, (AsyncMessage)message.getMessageObject())) return; message.getMessageObject().setDestination(getDestination().getId()); message.getMessageObject().setHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER, subscription.getSubscriptionId()); try { client.receive((AsyncMessage)message.getMessageObject()); } catch (MessageReceivingException e) { throw new RuntimeException("Channel delivery Error", e); } } else log.warn("Received message of unknown type %s", message.getMessageObject()); } } }