/*
* 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.plugin.addrbook.msoutlook.calendar;
import java.beans.*;
import java.text.*;
import java.util.*;
import java.util.regex.*;
import net.java.sip.communicator.plugin.addrbook.*;
import net.java.sip.communicator.plugin.addrbook.msoutlook.*;
import net.java.sip.communicator.service.calendar.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
/**
* A implementation of <tt>CalendarService</tt> for MS Outlook calendar.
* The class resolves the free busy status and also changes the presence status
* according to free busy status.
*
* @author Hristo Terezov
*/
public class CalendarServiceImpl implements CalendarService
{
/**
* Types for the MAPI properties values.
*/
public enum MAPIType
{
PT_SYSTIME,
PT_LONG,
PT_BOOL,
PT_BINARY
};
/**
* Response statuses of the calendar events (meeting objects).
*/
public static enum ResponseStatus
{
/**
* No response is required for this object.
*/
respNone(0x00000000),
/**
* This meeting belongs to the organizer.
*/
respOrganized(0x00000001),
/**
* This value on the attendee's meeting indicates that the attendee has
* tentatively accepted the meeting request.
*/
respTentative(0x00000002),
/**
* This value on the attendee's meeting t indicates that the attendee
* has accepted the meeting request.
*/
respAccepted(0x00000003),
/**
* This value on the attendee's meeting indicates that the attendee has
* declined the meeting request.
*/
respDeclined(0x00000004),
/**
* This value on the attendee's meeting indicates the attendee has not
* yet responded.
*/
respNotResponded(0x00000005);
/**
* The ID of the property
*/
private final long id;
private ResponseStatus(int id)
{
this.id = id;
}
/**
* Finds <tt>ResponseStatuse</tt> instance by given value of the status.
* @param value the value of the status we are searching for.
* @return the status or <tt>FREE</tt> if no status is found.
*/
public static ResponseStatus getFromLong(long value)
{
for(ResponseStatus state : values())
{
if(state.getID() == value)
{
return state;
}
}
return respNone;
}
/**
* Returns the ID of the status.
* @return the ID of the status.
*/
private long getID()
{
return id;
}
};
/**
* MAPI properties that we use to get information about the calendar items.
*/
public static enum MAPICalendarProperties
{
/**
* A property for the start date of the calendar item.
*/
PidLidAppointmentStartWhole(0x0000820D, MAPIType.PT_SYSTIME),
/**
* A property for the end date of the calendar item.
*/
PidLidAppointmentEndWhole(0x0000820E, MAPIType.PT_SYSTIME),
/**
* A property for the free busy status of the calendar item.
*/
PidLidBusyStatus(0x00008205, MAPIType.PT_LONG),
/**
* A property that indicates if the calendar item is recurring or not.
*/
PidLidRecurring(0x00008223, MAPIType.PT_BOOL),
/**
* A property with information about the recurrent pattern of the event.
*/
PidLidAppointmentRecur(0x00008216, MAPIType.PT_BINARY),
/**
* A property with information about the accepted state of the event.
*/
PidLidResponseStatus(0x00008218, MAPIType.PT_LONG);
/**
* The id of the property
*/
private final long id;
/**
* The <tt>MAPIType</tt> of the property.
*/
private final MAPIType type;
/**
* Constructs new property.
* @param id the id
* @param type the type
*/
MAPICalendarProperties(long id, MAPIType type)
{
this.id = id;
this.type = type;
}
/**
* Returns array of IDs of created properties.
* @return array of IDs of created properties.
*/
public static long[] getALLPropertyIDs()
{
MAPICalendarProperties properties[] = values();
long[] result = new long[properties.length];
for(int i = 0; i < properties.length; i++)
{
result[i] = properties[i].getID();
}
return result;
}
/**
* Returns the ID of the property.
* @return the ID of the property.
*/
public long getID()
{
return id;
}
/**
* Returns the type of the property
* @return the type of the property
*/
public MAPIType getType()
{
return type;
}
/**
* Returns <tt>MAPICalendarProperties</tt> instance by given order ID
* @param i the order ID
* @return <tt>MAPICalendarProperties</tt> instance
*/
public static MAPICalendarProperties getByOrderId(int i)
{
return values()[i];
}
}
/**
* The <tt>Logger</tt> used by the <tt>CalendarServiceImpl</tt>
* class and its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(CalendarServiceImpl.class);
/**
* A list with currently active <tt>CalendarItemTimerTask</tt>s
*/
private List<CalendarItemTimerTask> currentCalendarItems
= new LinkedList<CalendarItemTimerTask>();
/**
* A map with the calendar items IDs and <tt>CalendarItemTimerTask</tt>s.
* The map contains the current and future calendar items.
*/
private Map<String, CalendarItemTimerTask> taskMap
= new HashMap<String, CalendarItemTimerTask>();
/**
* The current free busy status.
*/
private BusyStatusEnum currentState = BusyStatusEnum.FREE;
/**
* Instance of <tt>InMeetingStatusPolicy</tt> class which is used to update
* the presence status according the current free busy status.
*/
private InMeetingStatusPolicy inMeetingStatusPolicy
= new InMeetingStatusPolicy();
public ProviderPresenceStatusListener presenceStatusListener
= new ProviderPresenceStatusListener()
{
@Override
public void providerStatusMessageChanged(PropertyChangeEvent evt)
{
}
@Override
public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt)
{
if(evt.getNewStatus().isOnline())
{
inMeetingStatusPolicy.handleProtocolProvider(
evt.getProvider(), null, false, true);
}
}
};
/**
* The flag which signals that MAPI strings should be returned in the
* unicode character set.
*/
public static final long MAPI_UNICODE = 0x80000000;
static
{
System.loadLibrary("jmsoutlookaddrbook");
}
/**
* Adds <tt>CalendarItemTimerTask</tt> to the map of tasks.
* @param id the id of the calendar item to be added.
* @param task the <tt>CalendarItemTimerTask</tt> instance to be added.
*/
public void addToTaskMap(String id, CalendarItemTimerTask task)
{
synchronized(taskMap)
{
taskMap.put(id, task);
}
}
/**
* Removes <tt>CalendarItemTimerTask</tt> from the map of tasks.
* @param id the id of the calendar item to be removed.
*/
public void removeFromTaskMap(String id)
{
synchronized(taskMap)
{
taskMap.remove(id);
}
}
/**
* Adds <tt>CalendarItemTimerTask</tt> to the list of current tasks.
* @param task the <tt>CalendarItemTimerTask</tt> instance to be added.
*/
public void addToCurrentItems(CalendarItemTimerTask task)
{
synchronized(currentCalendarItems)
{
currentCalendarItems.add(task);
}
}
/**
* Removes <tt>CalendarItemTimerTask</tt> from the list of current tasks.
* @param task the task of the calendar item to be removed.
*/
public void removeFromCurrentItems(CalendarItemTimerTask task)
{
synchronized(currentCalendarItems)
{
currentCalendarItems.remove(task);
}
}
/**
* Retrieves, parses and stores all the calendar items from the outlook.
*/
public void start()
{
getAllCalendarItems(new NotificationsDelegate());
}
/**
* Retrieves, parses and stores all the calendar items from the outlook.
* @param callback the callback object that receives the results.
*/
private static native void getAllCalendarItems(
NotificationsDelegate callback);
/**
* Returns array of property values for the given calendar item.
* @param entryId the entry id of the calendar item.
* @param propIds the IDs of the properties that we are interested for.
* @param flags the flags.
* @return array of property values for the given calendar item.
* @throws MsOutlookMAPIHResultException
*/
public static native Object[] IMAPIProp_GetProps(String entryId,
long[] propIds, long flags)
throws MsOutlookMAPIHResultException;
/**
* Gets the property values of given calendar item and creates
* <tt>CalendarItemTimerTask</tt> instance for it.
* @param id The outlook calendar item identifier.
*
* @throws MsOutlookMAPIHResultException if anything goes wrong while
* getting the properties of the calendar item.
*/
private synchronized void insert(String id)
throws MsOutlookMAPIHResultException
{
Object[] props = null;
props
= IMAPIProp_GetProps(id, MAPICalendarProperties.getALLPropertyIDs(),
MAPI_UNICODE);
addCalendarItem(props, id);
}
/**
* Parses the property values of calendar item and creates
* <tt>CalendarItemTimerTask</tt> instance for the calendar item.
* @param props the property values.
* @param id the ID of the calendar item.
*/
private void addCalendarItem(Object[] props, String id)
{
Date startTime = null, endTime = null;
BusyStatusEnum status = BusyStatusEnum.FREE;
ResponseStatus responseStatus = ResponseStatus.respNone;
boolean isRecurring = false;
byte[] recurringData = null;
for(int i = 0; i < props.length; i++)
{
if(props[i] == null)
continue;
MAPICalendarProperties propertyName
= MAPICalendarProperties.getByOrderId(i);
switch(propertyName)
{
case PidLidAppointmentStartWhole:
try
{
long time
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z")
.parse((String)props[i] + " UTC").getTime();
startTime = new Date(time);
}
catch (ParseException e)
{
logger.error("Cannot parse date string: " + props[i]);
return;
}
break;
case PidLidAppointmentEndWhole:
try
{
long time
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z")
.parse((String)props[i] + " UTC").getTime();
endTime = new Date(time);
}
catch (ParseException e)
{
logger.error("Cannot parse date string: " + props[i]);
return;
}
break;
case PidLidBusyStatus:
status = BusyStatusEnum.getFromLong((Long)props[i]);
break;
case PidLidRecurring:
isRecurring = (Boolean)props[i];
break;
case PidLidAppointmentRecur:
recurringData = ((byte[])props[i]);
break;
case PidLidResponseStatus:
responseStatus
= ResponseStatus.getFromLong((Long) props[i]);
break;
}
}
if(responseStatus != ResponseStatus.respNone
&& responseStatus != ResponseStatus.respAccepted
&& responseStatus != ResponseStatus.respOrganized)
return;
if(status == BusyStatusEnum.FREE || startTime == null || endTime == null)
return;
Date currentTime = new Date();
boolean executeNow = false;
if(startTime.before(currentTime) || startTime.equals(currentTime))
executeNow = true;
CalendarItemTimerTask task = null;
if(recurringData != null)
{
task = new CalendarItemTimerTask(status, startTime, endTime, id,
executeNow, null);
try
{
RecurringPattern pattern
= new RecurringPattern(recurringData, task);
task.setPattern(pattern);
}
catch(IndexOutOfBoundsException e)
{
logger.error(
"Error parsing reccuring pattern." + e.getMessage(),e);
logger.error("Reccuring data:\n" + bytesToHex(recurringData));
return;
}
}
if(endTime.before(currentTime) || endTime.equals(currentTime))
{
if(isRecurring)
{
task = task.getPattern().next(startTime, endTime);
}
else
return;
}
if(task == null)
task = new CalendarItemTimerTask(status, startTime, endTime, id,
executeNow, null);
task.scheduleTasks();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for(byte b: bytes)
sb.append(String.format("%02x", b & 0xff));
return sb.toString();
}
/**
* Changes the value of the current status
* @param state the new value.
*/
protected void setCurrentState(BusyStatusEnum state)
{
if(currentState == state)
return;
BusyStatusEnum oldState = currentState;
this.currentState = state;
if((oldState == BusyStatusEnum.FREE && state != BusyStatusEnum.FREE)
|| (oldState != BusyStatusEnum.FREE && state == BusyStatusEnum.FREE))
{
inMeetingStatusPolicy.freeBusyStateChanged();
}
}
/**
* Handles presence status changed from "On the Phone"
*
* @param presenceStatuses the remembered presence statuses
* @return <tt>true</tt> if the status is changed.
*/
public boolean onThePhoneStatusChanged(
Map<ProtocolProviderService,PresenceStatus> presenceStatuses)
{
if(currentState != BusyStatusEnum.FREE)
{
inMeetingStatusPolicy.onThePhoneStatusChanged(presenceStatuses);
return true;
}
return false;
}
/**
* Calculates and changes the value of current status using the current
* active calendar items and their statuses.
*/
public void updateStateFromCurrentItems()
{
BusyStatusEnum tmpState = BusyStatusEnum.FREE;
synchronized(currentCalendarItems)
{
for(CalendarItemTimerTask task : currentCalendarItems)
{
if(tmpState.getPriority() < task.getStatus().getPriority())
{
tmpState = task.getStatus();
}
}
}
setCurrentState(tmpState);
}
@Override
public BusyStatusEnum getStatus()
{
return currentState;
}
/**
* The method is not implemented yet.
*/
@Override
public void addFreeBusySateListener(FreeBusySateListener listener)
{
}
/**
* The method is not implemented yet.
*/
@Override
public void removeFreeBusySateListener(FreeBusySateListener listener)
{
}
/**
* Implements the policy to have the presence statuses of online accounts
* (i.e. registered <tt>ProtocolProviderService</tt>s) set to
* "In meeting" according the free busy status.
*
*/
private class InMeetingStatusPolicy
{
/**
* The regular expression which removes whitespace from the
* <tt>statusName</tt> property value of <tt>PresenceStatus</tt>
* instances in order to recognize the <tt>PresenceStatus</tt> which
* represents "In meeting".
*/
private final Pattern presenceStatusNameWhitespace
= Pattern.compile("\\p{Space}");
/**
* The <tt>PresenceStatus</tt>es of <tt>ProtocolProviderService</tt>s
* before they were changed to "In meeting" remembered so
* that they can be restored.
*/
private final Map<ProtocolProviderService,PresenceStatus>
presenceStatuses
= Collections.synchronizedMap(
new WeakHashMap<ProtocolProviderService,PresenceStatus>());
/**
* Notifies this instance that the free busy status has changed.
*/
public void freeBusyStateChanged()
{
run(false);
}
/**
* Handles presence status changed from "On the Phone"
* @param presenceStatuses the remembered presence statuses
*/
public void onThePhoneStatusChanged(
Map<ProtocolProviderService,PresenceStatus> presenceStatuses)
{
run(true);
for(ProtocolProviderService pps : presenceStatuses.keySet())
rememberPresenceStatus(pps, presenceStatuses.get(pps));
}
/**
* Returns the remembered presence statuses
* @return the remembered presence statuses
*/
public Map<ProtocolProviderService,PresenceStatus> getRememberedStatuses()
{
return presenceStatuses;
}
/**
* Finds the first <tt>PresenceStatus</tt> among the set of
* <tt>PresenceStatus</tt>es supported by a specific
* <tt>OperationSetPresence</tt> which represents
* "In meeting".
*
* @param presence the <tt>OperationSetPresence</tt> which represents
* the set of supported <tt>PresenceStatus</tt>es
* @return the first <tt>PresenceStatus</tt> among the set of
* <tt>PresenceStatus</tt>es supported by <tt>presence</tt> which
* represents "In meeting" if such a <tt>PresenceStatus</tt>
* was found; otherwise, <tt>null</tt>
*/
private PresenceStatus findInMeetingPresenceStatus(
OperationSetPresence presence)
{
for (Iterator<PresenceStatus> i = presence.getSupportedStatusSet();
i.hasNext();)
{
PresenceStatus presenceStatus = i.next();
if (presenceStatusNameWhitespace
.matcher(presenceStatus.getStatusName())
.replaceAll("")
.equalsIgnoreCase("InAMeeting"))
{
return presenceStatus;
}
}
return null;
}
/**
* Finds the first <tt>PresenceStatus</tt> among the set of
* <tt>PresenceStatus</tt>es supported by a specific
* <tt>OperationSetPresence</tt> which represents
* "On the phone".
*
* @param presence the <tt>OperationSetPresence</tt> which represents
* the set of supported <tt>PresenceStatus</tt>es
* @return the first <tt>PresenceStatus</tt> among the set of
* <tt>PresenceStatus</tt>es supported by <tt>presence</tt> which
* represents "On the phone" if such a <tt>PresenceStatus</tt>
* was found; otherwise, <tt>null</tt>
*/
private PresenceStatus findOnThePhonePresenceStatus(
OperationSetPresence presence)
{
for (Iterator<PresenceStatus> i = presence.getSupportedStatusSet();
i.hasNext();)
{
PresenceStatus presenceStatus = i.next();
if (presenceStatusNameWhitespace
.matcher(presenceStatus.getStatusName())
.replaceAll("")
.equalsIgnoreCase("OnThePhone"))
{
return presenceStatus;
}
}
return null;
}
/**
* Removes the remembered presence status for given provider
* @param pps the provider
* @return the removed value
*/
private PresenceStatus forgetPresenceStatus(ProtocolProviderService pps)
{
return presenceStatuses.remove(pps);
}
/**
* Removes all remembered presence statuses.
*/
private void forgetPresenceStatuses()
{
presenceStatuses.clear();
}
/**
* Determines whether the free busy status is busy or not
*
* @return <tt>true</tt> if the status is busy and <tt>false</tt> if the
* status is free
*/
private boolean isInMeeting()
{
return currentState != BusyStatusEnum.FREE;
}
/**
* Invokes
* {@link OperationSetPresence#publishPresenceStatus(PresenceStatus,
* String)} on a specific <tt>OperationSetPresence</tt> with a specific
* <tt>PresenceStatus</tt> and catches any exceptions.
*
* @param presence the <tt>OperationSetPresence</tt> on which the method
* is to be invoked
* @param presenceStatus the <tt>PresenceStatus</tt> to provide as the
* respective method argument value
*/
private void publishPresenceStatus(
OperationSetPresence presence,
PresenceStatus presenceStatus)
{
try
{
presence.publishPresenceStatus(presenceStatus, null);
}
catch (Throwable t)
{
if (t instanceof InterruptedException)
Thread.currentThread().interrupt();
else if (t instanceof ThreadDeath)
throw (ThreadDeath) t;
}
}
private PresenceStatus rememberPresenceStatus(
ProtocolProviderService pps,
PresenceStatus presenceStatus)
{
return presenceStatuses.put(pps, presenceStatus);
}
/**
* Applies this policy to the current state of the application.
*/
private void run(boolean onThePhoneStatusChanged)
{
List<ProtocolProviderService> providers
= AddrBookActivator.getProtocolProviders();
if ((providers == null) || (providers.size() == 0))
{
forgetPresenceStatuses();
}
else
{
boolean isInMeeting = isInMeeting();
for (ProtocolProviderService pps : providers)
{
if (pps == null)
continue;
handleProtocolProvider(pps, isInMeeting,
onThePhoneStatusChanged, false);
}
}
}
public void handleProtocolProvider(ProtocolProviderService pps,
Boolean isInMeeting, boolean onThePhoneStatusChanged,
boolean dontAddListeners)
{
if(isInMeeting == null)
isInMeeting = isInMeeting();
OperationSetPresence presence
= pps.getOperationSet(OperationSetPresence.class);
if (presence == null)
{
/*
* "In meeting" is a PresenceStatus so it is available
* only to accounts which support presence in the first
* place.
*/
forgetPresenceStatus(pps);
}
else if (pps.isRegistered())
{
PresenceStatus inMeetingPresenceStatus
= findInMeetingPresenceStatus(presence);
PresenceStatus onThePhone
= findOnThePhonePresenceStatus(presence);
if (inMeetingPresenceStatus == null)
{
/*
* If do not know how to define "On the phone" for
* an OperationSetPresence, then we'd better not
* mess with it in the first place.
*/
forgetPresenceStatus(pps);
}
else if (isInMeeting)
{
if(!dontAddListeners)
{
presence.addProviderPresenceStatusListener(
presenceStatusListener);
}
PresenceStatus presenceStatus
= presence.getPresenceStatus();
if (presenceStatus == null)
{
logger.info("HANDLE provider 55");
/*
* It is strange that an OperationSetPresence
* does not have a PresenceStatus so it may be
* safer to not mess with it.
*/
forgetPresenceStatus(pps);
presence.removeProviderPresenceStatusListener(
presenceStatusListener);
}
else if (!inMeetingPresenceStatus.equals(
presenceStatus)
&& (!presenceStatus.equals(onThePhone)
|| onThePhoneStatusChanged))
{
if(!dontAddListeners)
{
if(!presenceStatus.isOnline())
{
return;
}
presence.removeProviderPresenceStatusListener(
presenceStatusListener);
}
publishPresenceStatus(
presence,
inMeetingPresenceStatus);
if (inMeetingPresenceStatus.equals(
presence.getPresenceStatus()))
{
rememberPresenceStatus(pps, presenceStatus);
}
else
{
forgetPresenceStatus(pps);
}
}
else
{
presence.removeProviderPresenceStatusListener(
presenceStatusListener);
}
}
else
{
PresenceStatus presenceStatus
= forgetPresenceStatus(pps);
if ((presenceStatus != null)
&& inMeetingPresenceStatus.equals(
presence.getPresenceStatus()))
{
publishPresenceStatus(presence, presenceStatus);
}
}
}
else
{
forgetPresenceStatus(pps);
}
}
}
/**
* Delegate class to be notified for calendar changes.
*/
public class NotificationsDelegate
{
/**
* Callback method when receiving notifications for inserted items.
*/
public void inserted(String id)
{
try
{
insert(id);
}
catch (MsOutlookMAPIHResultException e)
{
e.printStackTrace();
}
}
/**
* Callback method when receiving notifications for updated items.
*/
public void updated(String id)
{
try
{
synchronized(taskMap)
{
CalendarItemTimerTask task = taskMap.get(id);
//Expired tasks can be removed earlier from the taskMap.
if(task != null)
{
task.remove();
}
}
insert(id);
}
catch (MsOutlookMAPIHResultException e)
{
e.printStackTrace();
}
}
/**
* Callback method when receiving notifications for deleted items.
*/
public void deleted(String id)
{
synchronized(taskMap)
{
CalendarItemTimerTask task = taskMap.get(id);
if(task != null)
{
task.remove();
}
}
}
/**
* Callback method when receiving notifications for deleted items.
*/
public boolean callback(String id)
{
try
{
insert(id);
}
catch (MsOutlookMAPIHResultException e)
{
e.printStackTrace();
}
return true;
}
}
public void handleProviderAdded(ProtocolProviderService pps)
{
inMeetingStatusPolicy.handleProtocolProvider(pps, null, false, false);
}
/**
* Returns the remembered presence statuses
* @return the remembered presence statuses
*/
public Map<ProtocolProviderService,PresenceStatus> getRememberedStatuses()
{
return inMeetingStatusPolicy.getRememberedStatuses();
}
}