![JAR search and dependency download from the Maven repository](/logo.png)
javax.net.msrp.Session Maven / Gradle / Ivy
Show all versions of msrp Show documentation
/*
* Copyright � Jo�o Antunes 2008 This file is part of MSRP Java Stack.
*
* MSRP Java Stack 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 3 of the License, or (at your
* option) any later version.
*
* MSRP Java Stack 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 MSRP Java Stack. If not, see .
*/
package javax.net.msrp;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import javax.net.msrp.events.*;
import javax.net.msrp.exceptions.*;
import javax.net.msrp.wrap.Wrap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An MSRP Session.
*
* This interface, combined with {@link SessionListener} is the primary
* interface for sending and receiving MSRP traffic.
*
* The class manages the list of MSRP Messages with which it's currently
* associated.
*
* @author Jo�o Antunes
*/
public class Session
{
/** The logger associated with this class */
private static final Logger logger = LoggerFactory.getLogger(Session.class);
private Stack stack = Stack.getInstance();
/**
* Associates an listener with the session, processing incoming messages
*/
private SessionListener myListener;
private ArrayList toUris = new ArrayList();
private TransactionManager txManager;
private InetAddress localAddress;
@SuppressWarnings("unused") // TODO: implement TLS
private boolean isSecure;
/**
* @uml.property name="_relays"
*/
private boolean isRelay;
/**
* RFC 3994 support: indication of message composition.
*/
private ImState isComposing = ImState.idle;
/** what am I composing? */
private String composeContentType;
/** timestamp last active compose */
private long lastActive;
/** After this time, active transitions to idle */
private long activeEnd;
/** the refresh period currently in effect */
private int refresh;
/** the chunk size to use when SENDing data */
private long chunkSize = 0;
/** URI identifying this session
* @uml.property name="_URI"
*/
private URI uri = null;
private String id;
/**
* @desc the {@link Connection} associated with this session
* @uml.property name="_connection"
* @uml.associationEnd inverse="_session:javax.net.msrp.Connection"
*/
private Connection connection = null;
/**
* The queue of messages to send.
*
* @uml.property name="sendQueue"
*/
private ArrayList sendQueue = new ArrayList();
/**
* stores sent/being sent messages (by message-ID) on request of the Success-Report field.
* @uml.property name="_messagesSent"
*/
private HashMap messagesSentOrSending =
new HashMap();
/**
* contains the messages (by message-ID) being received
*/
private HashMap messagesReceiving =
new HashMap();
/**
* The Report mechanism associated with this session.
*
* The mechanism is basically used to decide on the granularity of reports.
* Defaults to {@code DefaultReportMechanism}.
*
* @see DefaultReportMechanism
*/
private ReportMechanism reportMechanism = DefaultReportMechanism.getInstance();
/** Create an active session with the local address.
*
* The associated connection will be an active one
* (will connect automatically to target).
*
* Connection will be established once a call to {@link #setToPath(ArrayList)}
* defines the target-list.
*
* @param isSecure Is it a secure connection or not (use TLS - not implemented yet)?
* @param isRelay is this a relaying session?
* @param address the address to use as local endpoint.
* @return the created session
* @throws InternalErrorException if any error ocurred. More info about the
* error in the accompanying Throwable.
* @see #setToPath(ArrayList)
*/
public static Session create(boolean isSecure, boolean isRelay, InetAddress address)
throws InternalErrorException {
return new Session(isSecure, isRelay, address);
}
Session(boolean isSecure, boolean isRelay, InetAddress address)
throws InternalErrorException
{
this.localAddress = address;
this.isSecure = isSecure;
this.isRelay = isRelay;
try
{
connection = new Connection(address);
// Generate new URI and add to list of connection-URIs.
uri = connection.generateNewURI();
stack.addConnection(uri, connection);
logger.debug(String.format(
"%s MSRP session %s created: secure?[%b]], relay?[%b] InetAddress: %s",
toString(), getId(), isSecure, isRelay, address));
}
catch (Exception e)
{
throw new InternalErrorException(e);
}
}
/** Create a passive session with the local address
*
* The associated connection will be a passive one
* (will listen for a connection-request from the target).
*
* Messages will be queued until the destination contacts this session.
*
* @param isSecure Is it a secure connection or not (use TLS - not implemented yet)?
* @param isRelay is this a relaying session?
* @param toURI the destination URI that will contact this session.
* @param address the address to use as local endpoint.
* @return a passive session
* @throws InternalErrorException if any error ocurred. More info about the
* error in the accompanying Throwable.
*/
public static Session create(boolean isSecure, boolean isRelay, URI toURI, InetAddress address)
throws InternalErrorException
{
return new Session(isSecure, isRelay, toURI, address);
}
Session(boolean isSecure, boolean isRelay, URI toURI, InetAddress address)
throws InternalErrorException
{
this.localAddress = address;
this.isSecure = isSecure;
this.isRelay = isRelay;
try
{
connection = Stack.getConnectionsInstance(address);
uri = ((Connections) connection).generateAndStartNewUri();
stack.addConnection(uri, connection);
}
catch (Exception e) // wrap exceptions to InternalError
{
logger.error("Error creating Connections: ", e);
throw new InternalErrorException(e);
}
((Connections) connection).addUriToIdentify(uri, this);
toUris.add(toURI);
logger.debug(String.format(
"%s MSRP session %s created: secure?[%b]], relay?[%b], toURI=[%s], InetAddress: %s",
toString(), getId(), isSecure, isRelay, toURI, address));
}
@Override
public String toString()
{
return "[session:" + getId() + "]";
}
/**
* @return the 'session-id' part of the sessions' msrp-uri.
*/
public String getId()
{
if (id == null)
{
String path = uri.getPath();
id = path.substring(1, path.indexOf(';'));
}
return id;
}
/** Add your own {@link ReportMechanism} class.
* This'll enable you to define your own granularity.
* @param reportMechanism the {@code ReportMechanism} to use.
* @see DefaultReportMechanism
*/
public void setReportMechanism(ReportMechanism reportMechanism)
{
this.reportMechanism = reportMechanism;
}
/**
* @return the currently used {@code ReportMechanism} in this session.
* @see DefaultReportMechanism
*/
public ReportMechanism getReportMechanism()
{
return reportMechanism;
}
/*
* @deprecated, use {@link #setListener(SessionListener)} instead.
*/
@Deprecated
public void addListener(SessionListener listener)
{
if (listener == null)
throw new IllegalArgumentException("not a listener");
else
setListener(listener);
}
/** Set a listener on this session to catch any incoming traffic.
*
* @param listener the session listener to add/remove (when null)
* @throws IllegalArgumentException if listener of the wrong class.
* @see SessionListener
*/
public void setListener(SessionListener listener)
{
if (listener == null)
myListener = null;
else if (listener instanceof SessionListener)
{
myListener = listener;
logger.trace(this + " - Listener added");
}
else
{
String reason = this + " - Listener could not be added. " +
"It didn't match the criteria";
logger.error(reason);
throw new IllegalArgumentException(reason);
}
}
/*
* @deprecated, use {@link #setListener(null)} instead
*/
@Deprecated
public void removeListener(SessionListener listener)
{
if (listener != null && listener instanceof SessionListener && myListener == listener)
{
myListener = null;
}
}
/*
* @deprecated, use {@link #setToPath(ArrayList)}, instead
*/
@Deprecated
public void addToPath(ArrayList uris) throws IOException
{
setToPath(uris);
}
/**
* Adds the given destination URI's and establish the connection according RFC.
*
* This call should follow the creation of a {@link Session}.
*
* @param uris the to-path to use.
*
* @throws IOException if there was a connection problem.
* @throws IllegalArgumentException if the given URI's are not MSRP URIs
* @throws RuntimeException when called twice.
* @see #create(boolean, boolean, InetAddress)
*/
public void setToPath(ArrayList uris) throws IOException
{
if (!toUris.isEmpty()) // sanity check
throw new RuntimeException("Cannot set To-path twice");
for (URI uri : uris)
{
if (RegEx.isMsrpUri(uri))
toUris.add(uri);
else
throw new IllegalArgumentException("Invalid To-URI: " + uri);
}
connection.addEndPoint(getNextURI(), localAddress);
txManager = connection.getTransactionManager();
txManager.addSession(this);
txManager.initialize(this);
stack.addActiveSession(this);
logger.trace(this + " added "+ toUris.size() +" toPaths with URI[0]="
+ uris.get(0).toString());
}
/** send a bodiless message (keep-alive).
* @see Message
*/
public void sendAliveMessage()
{
sendMessage(new OutgoingAliveMessage());
}
/** send the given content over this session.
*
* @param contentType the type of content (refer to the MIME RFC's).
* @param content the content itself
* @return the message-object that will be send, can be used
* to abort large content.
* @see Message
*/
public OutgoingMessage sendMessage(String contentType, byte[] content)
{
return sendMessage(new OutgoingMessage(contentType, content));
}
/** Request the given nickname to be used with this session.
* The nickname request will be send to the chatroom at the other end of
* this session.
*
* A result will be reported in
* {@link SessionListener#receivedNickNameResult(Session, TransactionResponse)}
* @param nickname the name to use
* @return the actual msrp request that is sent out.
*/
public OutgoingMessage requestNickname(String nickname)
{
return sendMessage(new OutgoingMessage(nickname));
}
/** Wrap the given content in another type and send over this session.
* @param wrapType the (mime-)type to wrap it in.
* @param from from-field
* @param to to-field
* @param contentType the (mime-)type to wrap.
* @param content actual content
* @return the message-object that will be send, can be used
* to abort large content.
*/
public OutgoingMessage sendWrappedMessage(String wrapType, String from, String to,
String contentType, byte[] content)
{
Wrap wrap = Wrap.getInstance();
if (wrap.isWrapperType(wrapType)) {
WrappedMessage wm = wrap.getWrapper(wrapType);
return sendMessage(new OutgoingMessage(wrapType, wm.wrap(from, to, contentType, content)));
}
return null;
}
/** send the given file over this session.
*
* @param contentType the type of file.
* @param fileHandle the file itself
* @return the message-object that will be send, can be used
* to abort large content.
* @throws SecurityException not allowed to read and/or write
* @throws FileNotFoundException file not found
*/
public OutgoingMessage sendMessage(String contentType, File fileHandle)
throws FileNotFoundException, SecurityException
{
return sendMessage(new OutgoingMessage(contentType, fileHandle));
}
public OutgoingMessage sendMessage(OutgoingMessage message)
{
message.setSession(this);
if (message.hasData())
endComposing();
if (message.contentType != null)
addMessageToSend(message);
else
addMessageOnTop(message);
return message;
}
/**
* Reply ok to a NICKNAME request.
* @param request the originating request (transaction)
* @throws IllegalUseException arguments or state invalid
*/
public void sendNickResult(Transaction request) throws IllegalUseException
{
sendNickResult(request, ResponseCode.RC200, null);
}
/**
* Reply to a NICKNAME request.
* @param request the originating request (transaction)
* @param responseCode the result to send
* @param comment clarifying status comment to add
* @throws IllegalUseException arguments or state invalid
*/
public void sendNickResult(Transaction request, int responseCode,
String comment) throws IllegalUseException
{
if (request == null)
throw new InvalidParameterException("Null transaction specified");
if (request instanceof TransactionResponse)
request.transactionManager.addPriorityTransaction(request);
else
request.transactionManager.generateResponse(request, responseCode, comment);
}
/**
* Is the user of this session actively composing a message?
* @return active or idle
*/
public ImState getImState()
{
long now = System.currentTimeMillis();
if ((isComposing == ImState.active) && (activeEnd < now))
endComposing();
return isComposing;
}
private void endComposing() {
isComposing = ImState.idle;
activeEnd = 0;
}
/**
* What media is the user actively composing?
* @return the Content-Type being composed
*/
public String getComposeContentType()
{
return composeContentType;
}
/**
* Last time activity has been signalled.
* @return timestamp
*/
public long getLastActive()
{
return lastActive;
}
/**
* Indicate on session that chatter is composing a message.
* An active message indication will be sent when appropriate.
* @param contentType the type of message being composed.
* @param refresh interval before transitioning to idle.
*/
public void setActive(String contentType, int refresh)
{
if (contentType == null || contentType.length() == 0)
throw new IllegalArgumentException("Content-Type must be a valid string");
composeContentType = contentType;
if (shouldActiveTransitionBeSent(refresh))
{
sendMessage(new OutgoingStatusMessage(this, isComposing, composeContentType, refresh));
}
}
/**
* Set composer to active and see if we need to advertise this.
* @param refresh refresh period to use.
* @return
*/
private boolean shouldActiveTransitionBeSent(int refresh)
{
long now = System.currentTimeMillis();
isComposing = ImState.active;
lastActive = now;
if ((activeEnd < now) || (this.refresh > 0 && (activeEnd - (this.refresh / 2) < now)))
{
if (refresh < 60) /* SHOULD not be allowed */
refresh = 60;
this.refresh = refresh;
activeEnd = lastActive + (refresh * 1000);
return true;
}
return false;
}
/*
* Same as {@link #setActive(String, int)} but with a
* default refresh period of 120 sec.
*/
public void setActive(String contentType)
{
setActive(contentType, 120);
}
/**
* The conferencing-version of {@link #setActive(String, int)}. The
* indication will be wrapped within message/CPIM to retain conference
* participant information.
* @param contentType what's in it?
* @param refresh period
* @param from from-field content of the wrapped indication
* @param to to-field content of the wrapped indication
*/
public void setActive(String contentType, int refresh, String from, String to) {
if (contentType == null || contentType.length() == 0)
throw new IllegalArgumentException("Content-Type must be a valid string");
composeContentType = contentType;
if (shouldActiveTransitionBeSent(refresh))
{
sendMessage(new OutgoingStatusMessage(this, isComposing,
composeContentType, refresh, from, to));
}
}
/*
* Same as {@link #setActive(String, int, String, String)} but with a
* default refresh period of 120 sec.
*/
public void setActive(String contentType, String from, String to)
{
setActive(contentType, 120, from, to);
}
/**
* Indicate on session that chatter is idle.
* An idle message indication will be sent when appropriate.
*/
public void setIdle()
{
if (isComposing == ImState.active)
{
endComposing();
sendMessage(new OutgoingStatusMessage(this, isComposing, composeContentType, 0));
}
}
/**
* The conferencing-version of {@link #setIdle()}.
* The idle indication will be wrapped within message/CPIM to retain
* conference participant information.
* @param from from-field content of the wrapped indication
* @param to to-field content of the wrapped indication
*/
public void setIdle(String from, String to)
{
if (isComposing == ImState.active)
{
endComposing();
sendMessage(new OutgoingStatusMessage(this, isComposing,
composeContentType, 0, from, to));
}
}
/**
* Release all of the resources associated with this session.
* It could eventually, but not necessarily, close connections conforming to
* RFC 4975.
* After teardown, this session can no longer be used.
*/
public void tearDown()
{
logger.debug("teardown(" + toString() + ")");
// clear local resources
toUris = null;
if (sendQueue != null)
{
for (Message msg : sendQueue) {
msg.discard();
}
sendQueue = null;
}
if (txManager != null)
{
txManager.removeSession(this);
txManager = null;
}
// FIXME: (javax.net.msrp-31) allow connection reuse by sessions.
if (connection != null)
{
connection.close();
if (stack != null)
stack.removeConnection(connection);
connection = null;
}
if (stack != null)
{
stack.removeActiveSession(this);
stack = null;
}
if (reportMechanism != null && messagesReceiving != null)
{
for (Message message : messagesReceiving.values())
{
reportMechanism.removeMessage(message);
}
}
if (myListener != null)
{
myListener = null;
}
}
/** Return destination-path of this session
* @return the list of To:-URI's
*/
public ArrayList getToPath()
{
return toUris;
}
/**
* Get messages being received.
*
* @return just those.
*/
public HashMap getMessagesReceive()
{
return messagesReceiving;
}
/**
* Getter of the property _relays
*
* @return Is it a relay?.
* uml.property name="_relays"
*/
public boolean isRelay()
{
return isRelay;
}
/**
* Setter of the property _relays
*
* @param isRelay The _relays to set.
* uml.property name="_relays"
*/
public void setRelay(boolean isRelay)
{
this.isRelay = isRelay;
}
/**
* Setter of the property {@code connection}
*
* @param connection The _connection to set.
* uml.property name="_connection"
*/
protected void setConnection(Connection connection)
{
this.connection = connection;
}
/**
* Adds the given message to the top of the message to send queue
*
* Used when a message sending is paused so that when this
* session activity gets resumed it will continue sending this message
*
* @param message the message to be added on top of the message queue
*/
private void addMessageOnTop(Message message)
{
if (sendQueue != null)
{
sendQueue.add(0, message);
triggerSending();
}
}
/**
* Adds the given message to the end of the message to send queue.
* Kick off when queue is empty.
*
* @param message the message to be added to the end of the message queue
*/
private void addMessageToSend(Message message)
{
if (sendQueue != null)
{
sendQueue.add(message);
triggerSending();
}
}
/**
* Have txManager send awaiting messages from session.
*/
private void triggerSending()
{
if (txManager != null)
{
while (hasMessagesToSend())
txManager.generateTransactionsToSend(getMessageToSend());
}
}
/**
* @return true if this session has messages to send false otherwise
*/
public boolean hasMessagesToSend()
{
return (sendQueue != null) && (!sendQueue.isEmpty());
}
/**
* Returns and removes first message from the top of sendQueue
*
* @return first message to be sent from sendQueue
*/
public Message getMessageToSend()
{
if (sendQueue == null || sendQueue.isEmpty())
return null;
return sendQueue.remove(0);
}
/**
* Is session still valid (active)?
*
* at this point this is used by the generation of the success failureReport to
* assert if it should be sent or not quoting the RFC:
*
* "Endpoints SHOULD NOT send REPORT requests if they have reason to believe
* the request will not be delivered. For example, they SHOULD NOT send a
* REPORT request for a session that is no longer valid."
*
* @return true or false depending if this is a "valid" (active?!) session
* or not
*/
public boolean isActive()
{
// TODO implement some check.
return true;
}
/**
* Delete message from the send-queue.
* To be used only by {@link Message#abort(int, String)}
*
* @param message to delete
* @see Message#abort(int, String)
*/
protected void delMessageToSend(Message message)
{
if (sendQueue != null)
sendQueue.remove(message);
}
/**
* @return the txManager for this session.
*/
protected TransactionManager getTransactionManager()
{
return txManager;
}
/**
* Method that should only be called by {@link TransactionManager#addSession(Session)}
*
* @param transactionManager the txManager to set
*/
protected void setTransactionManager(TransactionManager transactionManager)
{
this.txManager = transactionManager;
}
/**
*
* retrieves a message from the sentMessages The sentMessages array may have
* messages that are currently being sent.
*
* They are only stored for REPORT purposes.
*
* @param messageID of the message to retrieve
* @return the message associated with the messageID
*/
protected Message getSentOrSendingMessage(String messageID)
{
return messagesSentOrSending.get(messageID);
}
/**
* method used by an incoming transaction to retrieve the message object
* associated with it, if it's already being received
*
* @param messageID of the message to
* @return the message being received associated with messageID, null if not found.
*/
protected Message getReceivingMessage(String messageID)
{
return messagesReceiving.get(messageID);
}
/**
* Put a message on the list of messages being received by this session.
*
* @param message the {@link IncomingMessage} to be put on the receive queue
* @see #messagesReceiving
*/
/* FIXME: in the future just put the queue of messages being received on
* the Stack as the Message object isn't necessarily bound to the Session
*/
protected void putReceivingMessage(IncomingMessage message)
{
messagesReceiving.put(message.getMessageID(), message);
}
/*
* Triggers to the Listener, not really sure if they are needed now, but
* later can be used to trigger some extra validations before actually
* calling the callback or cleanup after.
*/
/**
* trigger for the registered
* {@link SessionListener#receivedReport(Session, Transaction)} callback.
*
* @param report the transaction associated with the Report
* @see SessionListener
*/
protected void triggerReceivedReport(Transaction report)
{
traceCall("triggerReceivedReport");
myListener.receivedReport(this, report);
}
protected void triggerReceivedNickResult(TransactionResponse response)
{
traceCall("triggerReceivedNickResult");
myListener.receivedNickNameResult(this, response);
}
/**
* trigger for the registered
* {@link SessionListener#receivedMessage(Session, IncomingMessage)} callback.
*
* @param message the received message
* @see SessionListener
*/
protected void triggerReceiveMessage(IncomingMessage message)
{
traceCall("triggerReceiveMessage");
myListener.receivedMessage(this, message);
if (hasMessagesToSend())
triggerSending();
}
/**
* trigger for the registered
* {@link SessionListener#acceptHook(Session, IncomingMessage)} callback.
*
* @param message the message to accept or not
* @return true or false if we are accepting the message or not
* @see SessionListener
*/
protected boolean triggerAcceptHook(IncomingMessage message)
{
traceCall("triggerAcceptHook");
return myListener.acceptHook(this, message);
}
protected void triggerReceivedNickname(Transaction request)
{
traceCall("triggerReceivedNickname");
myListener.receivedNickname(this, request);
}
/**
* trigger for the registered
* {@link SessionListener#updateSendStatus(Session, Message, long)} callback.
*
* @param session to update
* @param outgoingMessage to send
*
* @see SessionListener
*/
protected void triggerUpdateSendStatus(Session session,
OutgoingMessage outgoingMessage)
{
traceCall("triggerUpdateSendStatus");
myListener.updateSendStatus(session, outgoingMessage,
outgoingMessage.getSentBytes());
}
private void traceCall(String call)
{
if (logger.isTraceEnabled())
logger.trace(String.format("%s %s() called", toString(), call));
}
/**
* trigger for the registered
* {@link SessionListener#abortedMessageEvent(MessageAbortedEvent)} callback.
*
* @param message the MSRP message that was aborted
* @param reason the reason
* @param extraReasonInfo the extra information about the reason if any is
* present (it can be transported on the body of a REPORT request)
* @param transaction the transaction associated with the abort event
*
* @see MessageAbortedEvent
*/
protected void fireMessageAbortedEvent(Message message, int reason,
String extraReasonInfo, Transaction transaction)
{
traceCall("fireMessageAbortedEvent");
MessageAbortedEvent abortedEvent =
new MessageAbortedEvent(message, this, reason, extraReasonInfo,
transaction);
SessionListener listener;
synchronized (myListener)
{
listener = myListener;
}
listener.abortedMessageEvent(abortedEvent);
}
/**
* trigger for the registered
* {@link SessionListener#connectionLost(Session, Throwable)} callback.
* @param cause Cause of the connection loss.
*/
protected void triggerConnectionLost(Throwable cause) {
traceCall("triggerConnectionLost");
myListener.connectionLost(this, cause);
}
/*
* End of triggers to the Listener
*/
/**
* Adds a message to the sent message list. Stored because of
* expected subsequent REPORT requests on this message
*
* @param message the message to add
*/
protected void addSentOrSendingMessage(Message message)
{
messagesSentOrSending.put(message.getMessageID(), message);
}
/**
* Delete a message that stopped being received from the
* being-received-queue of the Session.
*
* NOTE: currently only called for {@code IncomingMessage} objects
*
* @param message the message to be removed
*/
protected void delMessageToReceive(IncomingMessage message)
{
if (messagesReceiving.remove(message.getMessageID()) == null)
{
logger.warn(this + " receiving message to delete [" + message + "] not found");
}
}
/**
* @return the # of octets that will be sent in 1 chunk (0 = no limit)
*/
public long getChunkSize()
{
return chunkSize;
}
/**
* @param chunkSize the chunk size to use when SENDing data (0 = no limit)
*/
public void setChunkSize(long chunkSize)
{
if (chunkSize < 0)
throw new IllegalArgumentException("Chunk sizes cannot be negative");
this.chunkSize = chunkSize;
}
/**
* @return the local address
*/
public InetAddress getAddress() {
return localAddress;
}
/** Return the local URI (From:) of this session.
* @return the local URI
*/
public URI getURI() {
return uri;
}
/** Retrieve next hop from destination list
* @return the target URI (To:)
*/
public URI getNextURI() {
return toUris.get(0);
}
protected Connection getConnection() {
return connection;
}
}