/*
* JBoss, Home of Professional Open Source
* Copyright 2013, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This 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.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.application.push.impl;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import javax.faces.context.FacesContext;
import org.richfaces.application.configuration.ConfigurationServiceHelper;
import org.richfaces.application.push.DestroyableSession;
import org.richfaces.application.push.MessageData;
import org.richfaces.application.push.Request;
import org.richfaces.application.push.Session;
import org.richfaces.application.push.SessionManager;
import org.richfaces.application.push.SessionSubscriptionEvent;
import org.richfaces.application.push.SessionUnsubscriptionEvent;
import org.richfaces.application.push.SubscriptionFailureException;
import org.richfaces.application.push.Topic;
import org.richfaces.application.push.TopicKey;
import org.richfaces.application.push.TopicsContext;
import org.richfaces.application.CoreConfiguration;
import org.richfaces.log.Logger;
import org.richfaces.log.RichfacesLogger;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Session represents user’s subscription to a set of topics
*
* @author Nick Belaevski
*
* @see Session
*/
public class SessionImpl implements Session, DestroyableSession {
private static final Logger LOGGER = RichfacesLogger.APPLICATION.getLogger();
private final int maxInactiveInterval;
private final String id;
private final SessionManager sessionManager;
private volatile long lastAccessedTime;
private volatile Request request;
private volatile boolean active = true;
private final Queue<MessageData> messagesQueue = new ConcurrentLinkedQueue<MessageData>();
private final Set<TopicKey> successfulSubscriptions = Sets.newHashSet();
private final Map<TopicKey, String> failedSubscriptions = Maps.newHashMap();
private TopicsContext topicsContext;
private AtomicLong sequenceCounter = new AtomicLong();
public SessionImpl(String id, SessionManager sessionManager, TopicsContext topicsContext) {
super();
this.id = id;
this.sessionManager = sessionManager;
this.topicsContext = topicsContext;
FacesContext facesContext = FacesContext.getCurrentInstance();
this.maxInactiveInterval = ConfigurationServiceHelper.getIntConfigurationValue(facesContext,
CoreConfiguration.Items.pushSessionMaxInactiveInterval);
resetLastAccessedTimeToCurrent();
}
private void resetLastAccessedTimeToCurrent() {
lastAccessedTime = System.currentTimeMillis();
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#connect(org.richfaces.application.push.Request)
*/
@Override
public synchronized void connect(Request request) throws Exception {
releaseRequest();
if (active) {
processConnect(request);
} else {
request.resume();
}
}
/**
* Returns {@link Request} associated with this session
*/
protected Request getRequest() {
return request;
}
/**
* Process connecting of given request to this session and try to post messages if there are any
*/
protected void processConnect(Request request) throws Exception {
this.request = request;
sessionManager.requeue(this);
request.postMessages();
}
private void releaseRequest() {
Request localRequestCopy = this.request;
if (localRequestCopy != null) {
resetLastAccessedTimeToCurrent();
this.request = null;
localRequestCopy.resume();
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#disconnect()
*/
@Override
public synchronized void disconnect() throws Exception {
releaseRequest();
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getLastAccessedTime()
*/
@Override
public long getLastAccessedTime() {
if (!active) {
return -1;
}
if (this.request != null) {
// being accessed right now
return System.currentTimeMillis();
} else {
return lastAccessedTime;
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getMaxInactiveInterval()
*/
@Override
public int getMaxInactiveInterval() {
return maxInactiveInterval;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getId()
*/
@Override
public String getId() {
return id;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#invalidate()
*/
@Override
public void invalidate() {
active = false;
sessionManager.requeue(this);
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.DestroyableSession#destroy()
*/
@Override
public synchronized void destroy() {
active = false;
for (TopicKey key : successfulSubscriptions) {
Topic topic = topicsContext.getTopic(key);
topic.publishEvent(new SessionUnsubscriptionEvent(topic, key, this));
}
try {
disconnect();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getFailedSubscriptions()
*/
@Override
public Map<TopicKey, String> getFailedSubscriptions() {
return failedSubscriptions;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getSuccessfulSubscriptions()
*/
@Override
public Collection<TopicKey> getSuccessfulSubscriptions() {
return successfulSubscriptions;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#subscribe(java.lang.String[])
*/
@Override
public void subscribe(String[] topics) {
Iterable<TopicKey> topicKeys = Iterables.transform(Lists.newLinkedList(Arrays.asList(topics)), TopicKey.factory());
createSubscriptions(topicKeys);
}
private void createSubscriptions(Iterable<TopicKey> topicKeys) {
for (TopicKey topicKey : topicKeys) {
Topic pushTopic = topicsContext.getOrCreateTopic(topicKey);
String errorMessage = null;
if (pushTopic == null) {
errorMessage = MessageFormat.format("Topic ''{0}'' is not configured", topicKey.getTopicAddress());
} else {
try {
// TODO - publish another events
pushTopic.checkSubscription(topicKey, this);
} catch (SubscriptionFailureException e) {
if (e.getMessage() != null) {
errorMessage = e.getMessage();
} else {
errorMessage = MessageFormat.format("Unknown error connecting to ''{0}'' topic",
topicKey.getTopicAddress());
}
}
}
if (errorMessage != null) {
failedSubscriptions.put(topicKey, errorMessage);
} else {
pushTopic.publishEvent(new SessionSubscriptionEvent(pushTopic, topicKey, this));
successfulSubscriptions.add(topicKey);
}
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#getMessages()
*/
@Override
public Collection<MessageData> getMessages() {
return messagesQueue;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#clearBroadcastedMessages(long)
*/
@Override
public void clearBroadcastedMessages(long sequenceNumber) {
Queue<MessageData> queue = messagesQueue;
while (true) {
MessageData message = queue.peek();
if (message == null || sequenceNumber < message.getSequenceNumber()) {
break;
}
queue.remove();
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.application.push.Session#push(org.richfaces.application.push.TopicKey, java.lang.String)
*/
@Override
public void push(TopicKey topicKey, String serializedData) {
MessageData serializedMessage = new MessageData(topicKey, serializedData, sequenceCounter.getAndIncrement());
messagesQueue.add(serializedMessage);
synchronized (this) {
if (request != null) {
request.postMessages();
}
}
}
}