/* * 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; import java.util.*; import java.util.regex.*; import net.java.sip.communicator.plugin.addrbook.*; import net.java.sip.communicator.service.contactsource.*; import net.java.sip.communicator.util.*; /** * Implements <tt>ContactSourceService</tt> for the Address Book of Microsoft * Outlook. * * @author Lyubomir Marinov * @author Vincent Lucas */ public class MsOutlookAddrBookContactSourceService extends AsyncContactSourceService implements EditableContactSourceService, PrefixedContactSourceService { /** * The <tt>Logger</tt> used by the * <tt>MsOutlookAddrBookContactSourceService</tt> class and its instances * for logging output. */ private static final Logger logger = Logger.getLogger(MsOutlookAddrBookContactSourceService.class); /** * The outlook address book prefix. */ public static final String OUTLOOK_ADDR_BOOK_PREFIX = "net.java.sip.communicator.plugin.addrbook.OUTLOOK_ADDR_BOOK_PREFIX"; /** * Boolean property that defines whether using this contact source service * as result for the search field is authorized. This is useful when an * external plugin looks for result of this contact source service, but want * to display the search field result from its own (avoid duplicate * results). */ public static final String PNAME_OUTLOOK_ADDR_BOOK_SEARCH_FIELD_DISABLED = "net.java.sip.communicator.plugin.addrbook.OUTLOOK_ADDR_BOOK_SEARCH_FIELD_DISABLED"; /** * Boolean property that defines whether the warning for the default mail * client should be shown or not. */ public static final String PNAME_OUTLOOK_ADDR_BOOK_SHOW_DEFAULTMAILCLIENT_WARNING = "net.java.sip.communicator.plugin.addrbook.SHOW_DEFAULTMAILCLIENT_WARNING"; private static final long MAPI_INIT_VERSION = 0; private static final long MAPI_MULTITHREAD_NOTIFICATIONS = 0x00000001; private static final int NATIVE_LOGGER_LEVEL_INFO = 0; private static final int NATIVE_LOGGER_LEVEL_TRACE = 1; /** * The thread used to collect the notifications. */ private NotificationThread notificationThread = null; /** * The mutex used to synchronized the notification thread. */ private Object notificationThreadMutex = new Object(); /** * The latest query created. */ private MsOutlookAddrBookContactQuery latestQuery = null; /** * Indicates whether MAPI is initialized or not. */ private static boolean isMAPIInitialized = false; static { String lib = "jmsoutlookaddrbook"; try { System.loadLibrary(lib); } catch (Throwable t) { logger.error( "Failed to load native library " + lib + ": " + t.getMessage()); throw new RuntimeException(t); } /* * We have multiple reports of an "UnsatisfiedLinkError: no * jmsoutlookaddrbook in java.library.path" at * MsOutlookAddrBookContactSourceService#queryContactSource() which * seems strange since getting there means that we have already * successfully gone through the System.loadLibrary() above. Try to load * MsOutlookAddrBookContactQuery here and see how it goes. */ try { Class.forName(MsOutlookAddrBookContactQuery.class.getName()); } catch (ClassNotFoundException cnfe) { throw new RuntimeException(cnfe); } int bitness = getOutlookBitnessVersion(); int version = getOutlookVersion(); if(bitness != -1 && version != -1) { logger.info( "Outlook " + version + "-x" + bitness + " is installed."); } } /** * Initializes a new <tt>MsOutlookAddrBookContactSourceService</tt> * instance. * @param notificationDelegate the object to be notified for addressbook * changes * @throws MsOutlookMAPIHResultException if anything goes wrong while * initializing the new <tt>MsOutlookAddrBookContactSourceService</tt> * instance */ public static void initMAPI(NotificationsDelegate notificationDelegate) throws MsOutlookMAPIHResultException { if(!isMAPIInitialized) { boolean isOutlookDefaultMailClient = isOutlookDefaultMailClient(); boolean showWarning = AddrBookActivator.getConfigService().getBoolean( PNAME_OUTLOOK_ADDR_BOOK_SHOW_DEFAULTMAILCLIENT_WARNING, true); if(!isOutlookDefaultMailClient && showWarning) { DefaultMailClientMessageDialog dialog = new DefaultMailClientMessageDialog(); int result = dialog.showDialog(); if((result & DefaultMailClientMessageDialog .DONT_ASK_SELECTED_MASK) != 0) { AddrBookActivator.getConfigService().setProperty( PNAME_OUTLOOK_ADDR_BOOK_SHOW_DEFAULTMAILCLIENT_WARNING, false); } if((result & DefaultMailClientMessageDialog .DEFAULT_MAIL_CLIENT_SELECTED_MASK) != 0) { RegistryHandler.setOutlookAsDefaultMailClient(); } } if(isOutlookDefaultMailClient && !showWarning) { AddrBookActivator.getConfigService().setProperty( PNAME_OUTLOOK_ADDR_BOOK_SHOW_DEFAULTMAILCLIENT_WARNING, true); } String logFileName = ""; String homeLocation = System.getProperty( "net.java.sip.communicator.SC_LOG_DIR_LOCATION"); String dirName = System.getProperty( "net.java.sip.communicator.SC_HOME_DIR_NAME"); if(homeLocation != null && dirName != null) { logFileName = homeLocation + "\\" + dirName + "\\log\\"; } int logLevel = NATIVE_LOGGER_LEVEL_INFO; if(logger.isTraceEnabled()) { logLevel = NATIVE_LOGGER_LEVEL_TRACE; } logger.info("Init mapi with log level " + logLevel + " and log file" + " path " + logFileName); MAPIInitialize( MAPI_INIT_VERSION, MAPI_MULTITHREAD_NOTIFICATIONS, notificationDelegate, logFileName, logLevel); isMAPIInitialized = true; } } /** * Creates new <tt>NotificationsDelegate</tt> instance. * @return the <tt>NotificationsDelegate</tt> instance */ public NotificationsDelegate createNotificationDelegate() { return new NotificationsDelegate(); } /** * Gets a human-readable <tt>String</tt> which names this * <tt>ContactSourceService</tt> implementation. * * @return a human-readable <tt>String</tt> which names this * <tt>ContactSourceService</tt> implementation * @see ContactSourceService#getDisplayName() */ public String getDisplayName() { return "Microsoft Outlook"; } /** * Gets a <tt>String</tt> which uniquely identifies the instances of the * <tt>MsOutlookAddrBookContactSourceService</tt> implementation. * * @return a <tt>String</tt> which uniquely identifies the instances of the * <tt>MsOutlookAddrBookContactSourceService</tt> implementation * @see ContactSourceService#getType() */ public int getType() { return SEARCH_TYPE; } private static native void MAPIInitialize( long version, long flags, NotificationsDelegate callback, String logFileName, int logLevel) throws MsOutlookMAPIHResultException; /** * Uninitializes MAPI. */ public static void UninitializeMAPI() { if(isMAPIInitialized) { MAPIUninitialize(); isMAPIInitialized = false; } } private static native void MAPIUninitialize(); public static native int getOutlookBitnessVersion(); public static native int getOutlookVersion(); private static native boolean isOutlookDefaultMailClient(); /** * Creates query that searches for <tt>SourceContact</tt>s * which match a specific <tt>query</tt> <tt>Pattern</tt>. * * @param query the <tt>Pattern</tt> which this * <tt>ContactSourceService</tt> is being queried for * @return a <tt>ContactQuery</tt> which represents the query of this * <tt>ContactSourceService</tt> implementation for the specified * <tt>Pattern</tt> and via which the matching <tt>SourceContact</tt>s (if * any) will be returned * @see ExtendedContactSourceService#createContactQuery(Pattern) */ public ContactQuery createContactQuery(Pattern query) { if(latestQuery != null) latestQuery.clear(); latestQuery = new MsOutlookAddrBookContactQuery(this, query); return latestQuery; } /** * Stops this <tt>ContactSourceService</tt> implementation and prepares it * for garbage collection. * * @see AsyncContactSourceService#stop() */ @Override public void stop() { if(latestQuery != null) { latestQuery.clear(); latestQuery = null; } UninitializeMAPI(); } /** * Returns the global phone number prefix to be used when calling contacts * from this contact source. * * @return the global phone number prefix */ @Override public String getPhoneNumberPrefix() { return AddrBookActivator.getConfigService() .getString(OUTLOOK_ADDR_BOOK_PREFIX); } /** * Returns the index of the contact source in the result list. * * @return the index of the contact source in the result list */ public int getIndex() { return -1; } /** * Delegate class to be notified for addressbook changes. */ public class NotificationsDelegate { /** * Callback method when receiving notifications for inserted items. */ public void inserted(String id) { if(latestQuery != null) addNotification(id, 'i'); } /** * Callback method when receiving notifications for updated items. */ public void updated(String id) { if(latestQuery != null) addNotification(id, 'u'); } /** * Callback method when receiving notifications for deleted items. */ public void deleted(String id) { if(latestQuery != null) addNotification(id, 'd'); } } /** * Creates a new contact from the database (i.e "contacts" or * "msoutlook", etc.). * * @return The ID of the contact to remove. NULL if failed to create a new * contact. */ public String createContact() { return MsOutlookAddrBookContactQuery.createContact(); } /** * Adds a new empty contact, which will be filled in later. * * @param id The ID of the contact to add. */ public void addEmptyContact(String id) { if(id != null && latestQuery != null) { latestQuery.addEmptyContact(id); } } /** * Removes the given contact from the database (i.e "contacts" or * "msoutlook", etc.). * * @param id The ID of the contact to remove. */ public void deleteContact(String id) { if(id != null && MsOutlookAddrBookContactQuery.deleteContact(id)) { if(latestQuery != null) { latestQuery.deleted(id); } } } /** * Defines whether using this contact source service can be used as result * for the search field. This is useful when an external plugin looks for * result of this contact source service, but want to display the search * field result from its own (avoid duplicate results). * * @return True if this contact source service can be used to perform search * for contacts. False otherwise. */ @Override public boolean canBeUsedToSearchContacts() { return !AddrBookActivator.getConfigService().getBoolean( PNAME_OUTLOOK_ADDR_BOOK_SEARCH_FIELD_DISABLED, false); } /** * Collects a new notification and adds it to the notification thread. * * @param id The contact id. * @param function The kind of notification: 'd' for deleted, 'u' for * updated and 'i' for inserted. */ public void addNotification(String id, char function) { synchronized(notificationThreadMutex) { if(notificationThread == null || !notificationThread.isAlive()) { notificationThread = new NotificationThread(); notificationThread.start(); } notificationThread.add(id, function); } } /** * Thread used to collect the notification. */ private class NotificationThread extends Thread { /** * The list of notification collected. */ private Vector<NotificationIdFunction> contactIds = new Vector<NotificationIdFunction>(); /** * Initializes a new notification thread. */ public NotificationThread() { super("MsOutlookAddrbookContactSourceService notification thread"); } /** * Dispatchs the collected notifications. */ public void run() { boolean hasMore; NotificationIdFunction idFunction = null; String id; char function; synchronized(notificationThreadMutex) { hasMore = (contactIds.size() > 0); if(hasMore) { idFunction = contactIds.get(0); } } while(hasMore) { if(latestQuery != null) { id = idFunction.getId(); function = idFunction.getFunction(); if(function == 'd') { latestQuery.deleted(id); } else if(function == 'u') { latestQuery.updated(id); } else if(function == 'i') { latestQuery.inserted(id); } } synchronized(notificationThreadMutex) { contactIds.remove(0); hasMore = (contactIds.size() > 0); if(hasMore) { idFunction = contactIds.get(0); } } } } /** * Adds a new notification. Avoids previous notification for the given * contact. * * @param id The contact id. * @param function The kind of notification: 'd' for deleted, 'u' for * updated and 'i' for inserted. */ public void add(String id, char function) { NotificationIdFunction idFunction = new NotificationIdFunction(id, function); synchronized(notificationThreadMutex) { contactIds.remove(idFunction); contactIds.add(idFunction); } } /** * Returns the number of contact notifications to deal with. * * @return The number of contact notifications to deal with. */ public int getNbRemainingNotifications() { return contactIds.size(); } /** * Clear the current results. */ public void clear() { synchronized(notificationThreadMutex) { contactIds.clear(); } } } /** * Defines a notification: a combination of a contact identifier and a * function. */ private class NotificationIdFunction { /** * The contact identifier. */ private String id; /** * The kind of notification: 'd' for deleted, 'u' for updated and 'i' * for inserted. */ private char function; /** * Creates a new notification. * * @param id The contact id. * @param function The kind of notification: 'd' for deleted, 'u' for * updated and 'i' for inserted. */ public NotificationIdFunction(String id, char function) { this.id = id; this.function = function; } /** * Returns the contact identifier. * * @return The contact identifier. */ public String getId() { return this.id; } /** * Returns the kind of notification. * * @return 'd' for deleted, 'u' for updated and 'i' for inserted. */ public char getFunction() { return this.function; } /** * Returns if this notification is about the same contact has the one * given in parameter. * * @param obj An NotificationIdFunction to compare with. * * @return True if this notification is about the same contact has the * one given in parameter. False otherwise. */ public boolean equals(Object obj) { return (this.id == null && obj == null || obj instanceof String && this.id.equals((String) obj)); } /** * Returns the hash code corresponding to the contact identifier. * * @return The hash code corresponding to the contact identifier. */ public int hashCode() { return this.id.hashCode(); } } /** * Returns the bitness of this contact source service. * * @return The bitness of this contact source service. */ public int getBitness() { return getOutlookBitnessVersion(); } /** * Returns the version of this contact source service. * * @return The version of this contact source service. */ public int getVersion() { return getOutlookVersion(); } /** * Returns the number of contact notifications to deal with. * * @return The number of contact notifications to deal with. */ public int getNbRemainingNotifications() { int nbNotifications = 0; synchronized(notificationThreadMutex) { if(notificationThread != null) { nbNotifications = notificationThread.getNbRemainingNotifications(); } } return nbNotifications; } /** * Cancels the contact notifications. */ public void clearRemainingNotifications() { if(notificationThread != null) { notificationThread.clear(); } } }