/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* 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 net.java.sip.communicator.service.protocol;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
/**
* Represents a default implementation of
* {@link OperationSetBasicInstantMessaging} in order to make it easier for
* implementers to provide complete solutions while focusing on
* implementation-specific details.
*
* @author Lyubomir Marinov
*/
public abstract class AbstractOperationSetBasicInstantMessaging
implements OperationSetBasicInstantMessaging
{
/**
* The Logger used by the
* AbstractOperationSetBasicInstantMessaging class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(AbstractOperationSetBasicInstantMessaging.class);
/**
* A list of listeners registered for message events.
*/
private final List messageListeners =
new LinkedList();
/**
* Registers a MessageListener with this operation set so that it gets
* notifications of successful message delivery, failure or reception of
* incoming messages..
*
* @param listener the MessageListener to register.
*/
public void addMessageListener(MessageListener listener)
{
synchronized (messageListeners)
{
if (!messageListeners.contains(listener))
{
messageListeners.add(listener);
}
}
}
/**
* Create a Message instance for sending arbitrary MIME-encoding content.
*
* @param content content value
* @param contentType the MIME-type for content
* @param encoding encoding used for content
* @param subject a String subject or null for now
* subject.
* @return the newly created message.
*/
public Message createMessage(byte[] content, String contentType,
String encoding, String subject)
{
String contentAsString = null;
boolean useDefaultEncoding = true;
if (encoding != null)
{
try
{
contentAsString = new String(content, encoding);
useDefaultEncoding = false;
}
catch (UnsupportedEncodingException ex)
{
logger.warn("Failed to decode content using encoding "
+ encoding, ex);
// We'll use the default encoding.
}
}
if (useDefaultEncoding)
{
encoding = Charset.defaultCharset().name();
contentAsString = new String(content);
}
return createMessage(contentAsString, contentType, encoding, subject);
}
/**
* Create a Message instance for sending a simple text messages with default
* (text/plain) content type and encoding.
*
* @param messageText the string content of the message.
* @return Message the newly created message
*/
public Message createMessage(String messageText)
{
return createMessage(messageText, DEFAULT_MIME_TYPE,
DEFAULT_MIME_ENCODING, null);
}
/**
* Create a Message instance with the specified UID, content type
* and a default encoding.
* This method can be useful when message correction is required. One can
* construct the corrected message to have the same UID as the message
* before correction.
*
* @param messageText the string content of the message.
* @param contentType the MIME-type for content
* @param messageUID the unique identifier of this message.
* @return Message the newly created message
*/
public Message createMessageWithUID(
String messageText, String contentType, String messageUID)
{
return createMessage(messageText);
}
public abstract Message createMessage(
String content, String contentType, String encoding, String subject);
/**
* Notifies all registered message listeners that a message has been
* delivered successfully to its addressee..
*
* @param message the Message that has been delivered.
* @param to the Contact that message was delivered to.
*/
protected void fireMessageDelivered(Message message, Contact to)
{
fireMessageEvent(
new MessageDeliveredEvent(message, to, new Date()));
}
protected void fireMessageDeliveryFailed(
Message message,
Contact to,
int errorCode)
{
fireMessageEvent(
new MessageDeliveryFailedEvent(message, to, errorCode));
}
enum MessageEventType{
None,
MessageDelivered,
MessageReceived,
MessageDeliveryFailed,
MessageDeliveryPending,
}
/**
* Delivers the specified event to all registered message listeners.
*
* @param evt the EventObject that we'd like delivered to all
* registered message listeners.
*/
protected void fireMessageEvent(EventObject evt)
{
Collection listeners = null;
synchronized (this.messageListeners)
{
listeners = new ArrayList(this.messageListeners);
}
if (logger.isDebugEnabled())
logger.debug("Dispatching Message Listeners=" + listeners.size()
+ " evt=" + evt);
/*
* TODO Create a super class like this MessageEventObject that would
* contain the MessageEventType. Also we could fire an event for the
* MessageDeliveryPending event type (modify MessageListener and
* OperationSetInstantMessageTransform).
*/
MessageEventType eventType = MessageEventType.None;
if (evt instanceof MessageDeliveredEvent)
{
eventType = MessageEventType.MessageDelivered;
}
else if (evt instanceof MessageReceivedEvent)
{
eventType = MessageEventType.MessageReceived;
}
else if (evt instanceof MessageDeliveryFailedEvent)
{
eventType = MessageEventType.MessageDeliveryFailed;
}
// Transform the event.
EventObject[] events = messageTransform(evt, eventType);
for (EventObject event : events)
{
try
{
if (event == null)
return;
for (MessageListener listener : listeners)
{
switch (eventType)
{
case MessageDelivered:
listener.messageDelivered((MessageDeliveredEvent) event);
break;
case MessageDeliveryFailed:
listener
.messageDeliveryFailed((MessageDeliveryFailedEvent) event);
break;
case MessageReceived:
listener.messageReceived((MessageReceivedEvent) event);
break;
default:
/*
* We either have nothing to do or we do not know what
* to do. Anyway, we'll silence the compiler.
*/
break;
}
}
}
catch (Throwable e)
{
logger.error("Error delivering message", e);
}
}
}
/**
* Notifies all registered message listeners that a message has been
* received.
*
* @param message the Message that has been received.
* @param from the Contact that message was received from.
*/
protected void fireMessageReceived(Message message, Contact from)
{
fireMessageEvent(
new MessageReceivedEvent(message, from, new Date()));
}
/**
* Unregisters listener so that it won't receive any further
* notifications upon successful message delivery, failure or reception of
* incoming messages..
*
* @param listener the MessageListener to unregister.
*/
public void removeMessageListener(MessageListener listener)
{
synchronized (messageListeners)
{
messageListeners.remove(listener);
}
}
/**
* Messages pending delivery to be transformed.
*
* @param evt the message delivery event
* @return returns message delivery events
*/
public MessageDeliveredEvent[] messageDeliveryPendingTransform(
final MessageDeliveredEvent evt)
{
EventObject[] transformed = messageTransform(
evt, MessageEventType.MessageDeliveryPending);
final int size = transformed.length;
MessageDeliveredEvent[] events =
new MessageDeliveredEvent[size];
System.arraycopy(transformed, 0, events, 0, size);
return events;
}
/**
* Transform provided source event by processing transform layers in
* sequence.
*
* @param evt the source event to transform
* @param eventType the event type of the source event
* @return returns the resulting (transformed) events, if any. (I.e. an
* array of 0 or more size containing events.)
*/
private EventObject[] messageTransform(final EventObject evt,
final MessageEventType eventType)
{
if (evt == null)
{
return new EventObject[0];
}
ProtocolProviderService protocolProvider;
switch (eventType)
{
case MessageDelivered:
protocolProvider
= ((MessageDeliveredEvent) evt)
.getDestinationContact().getProtocolProvider();
break;
case MessageDeliveryFailed:
protocolProvider
= ((MessageDeliveryFailedEvent) evt)
.getDestinationContact().getProtocolProvider();
break;
case MessageDeliveryPending:
protocolProvider
= ((MessageDeliveredEvent) evt)
.getDestinationContact().getProtocolProvider();
break;
case MessageReceived:
protocolProvider
= ((MessageReceivedEvent) evt)
.getSourceContact().getProtocolProvider();
break;
default:
return new EventObject[] {evt};
}
OperationSetInstantMessageTransformImpl opSetMessageTransform
= (OperationSetInstantMessageTransformImpl) protocolProvider
.getOperationSet(OperationSetInstantMessageTransform.class);
if (opSetMessageTransform == null)
return new EventObject[] {evt};
// 'current' contains the events that need to be transformed. It should
// not contain null values.
final LinkedList current = new LinkedList();
// Add source event as start of transformation.
current.add(evt);
// 'next' contains the resulting events after transformation in the
// current iteration. It should not contain null values.
final LinkedList next = new LinkedList();
for (Map.Entry> entry
: opSetMessageTransform.transformLayers.entrySet())
{
for (TransformLayer transformLayer : entry.getValue())
{
next.clear();
while (!current.isEmpty())
{
final EventObject event = current.remove();
switch (eventType)
{
case MessageDelivered:
MessageDeliveredEvent transformedDelivered =
transformLayer.messageDelivered(
(MessageDeliveredEvent) event);
if (transformedDelivered != null)
{
next.add(transformedDelivered);
}
break;
case MessageDeliveryPending:
MessageDeliveredEvent[] evts = transformLayer
.messageDeliveryPending(
(MessageDeliveredEvent) event);
for (MessageDeliveredEvent mde : evts)
{
if (mde != null)
{
next.add(mde);
}
}
break;
case MessageDeliveryFailed:
MessageDeliveryFailedEvent transformedDeliveryFailed =
transformLayer.messageDeliveryFailed(
(MessageDeliveryFailedEvent) event);
if (transformedDeliveryFailed != null)
{
next.add(transformedDeliveryFailed);
}
break;
case MessageReceived:
MessageReceivedEvent transformedReceived =
transformLayer
.messageReceived((MessageReceivedEvent) event);
if (transformedReceived != null)
{
next.add(transformedReceived);
}
break;
default:
next.add(event);
/*
* We either have nothing to do or we do not know
* what to do. Anyway, we'll silence the compiler.
*/
break;
}
}
// Set events for next round of transformations.
current.addAll(next);
}
}
return current.toArray(new EventObject[current.size()]);
}
/**
* Determines whether the protocol supports the supplied content type
* for the given contact.
*
* @param contentType the type we want to check
* @param contact contact which is checked for supported contentType
* @return true if the contact supports it and
* false otherwise.
*/
public boolean isContentTypeSupported(String contentType, Contact contact)
{
// by default we support default mime type, for other mime-types
// method must be overridden
if(contentType.equals(DEFAULT_MIME_TYPE))
return true;
return false;
}
/**
* Sends the message to the destination indicated by the
* to. Provides a default implementation of this method.
*
* @param to the Contact to send message to
* @param toResource the resource to which the message should be send
* @param message the Message to send.
* @throws java.lang.IllegalStateException if the underlying ICQ stack is
* not registered and initialized.
* @throws java.lang.IllegalArgumentException if to is not an
* instance belonging to the underlying implementation.
*/
public void sendInstantMessage( Contact to,
ContactResource toResource,
Message message)
throws IllegalStateException,
IllegalArgumentException
{
sendInstantMessage(to, message);
}
/**
* Returns the inactivity timeout in milliseconds.
*
* @return The inactivity timeout in milliseconds. Or -1 if undefined
*/
public long getInactivityTimeout()
{
return -1;
}
}