001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.messagingconnector;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Date;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Objects;
026import java.util.Set;
027import java.util.concurrent.TimeUnit;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.core.user.CurrentUserProvider;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.user.population.UserPopulationDAO;
038import org.ametys.core.userpref.UserPreferencesException;
039import org.ametys.core.userpref.UserPreferencesManager;
040import org.ametys.plugins.explorer.calendars.EventRecurrenceTypeEnum;
041import org.ametys.plugins.messagingconnector.MessagingConnectorException.ExceptionType;
042import org.ametys.runtime.config.Config;
043import org.ametys.runtime.plugin.component.AbstractLogEnabled;
044
045import com.google.common.cache.Cache;
046import com.google.common.cache.CacheBuilder;
047
048/**
049 * Abstract implementation of {@link MessagingConnector} with cache.
050 *
051 */
052public abstract class AbstractMessagingConnector extends AbstractLogEnabled implements MessagingConnector, Initializable, Serviceable
053{
054    /** Duration of the cache for timeout errors, no calls will be done again for one user */
055    private static final int TIMEOUT_CACHE_DURATION_SECONDS = 30;
056    
057    /** The user population DAO */
058    protected UserPopulationDAO _userPopulationDAO;
059    /** The user preferences */
060    protected UserPreferencesManager _userPref;
061    /** The crypto helper */
062    protected CryptoHelper _cryptoHelper;
063    /** The current user provider */
064    protected CurrentUserProvider _currentUserProvider;
065
066    private Cache<EventCacheKey, List<CalendarEvent>> _eventsCache;
067    private Cache<EventCountCacheKey, Integer> _eventsCountCache;
068    private Cache<EmailCacheKey, List<EmailMessage>> _emailsCache;
069    private Cache<UserIdentity, Integer> _emailsCountCache;
070    // Error caches to avoid calling the server again too quickly
071    
072    private Cache<MessagingConnectorException.ExceptionType, Set<UserIdentity>> _errorCache;
073    private Cache<MessagingConnectorException.ExceptionType, Set<UserIdentity>> _timeoutErrorCache;
074    
075    private List<String> _populationIds;
076
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
080        _userPref = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE);
081        _cryptoHelper = (CryptoHelper) manager.lookup(CryptoHelper.ROLE);
082        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
083    }
084    
085    @Override
086    public void initialize()
087    {
088        _populationIds = new ArrayList<>();
089        
090        String populationIdsAsString = Config.getInstance().getValue("org.ametys.plugins.messagingconnector.population");
091        if (StringUtils.isNotBlank(populationIdsAsString))
092        {
093            List<String> userPopulationsIds = _userPopulationDAO.getUserPopulationsIds();
094            String[] populationIds = StringUtils.split(populationIdsAsString, ",");
095            
096            List<String> wrongPopulationIds = new ArrayList<>();
097            for (String populationId : populationIds)
098            {
099                String populationIdTrimed = StringUtils.trim(populationId);
100                if (!userPopulationsIds.contains(populationIdTrimed))
101                {
102                    wrongPopulationIds.add(populationIdTrimed);
103                }
104                else
105                {
106                    _populationIds.add(populationIdTrimed);
107                }
108            }
109            
110            if (!wrongPopulationIds.isEmpty())
111            {
112                throw new IllegalStateException("The following population ids defined in the configuration parameter 'population id' for the messaging connector do not exist : " + wrongPopulationIds);
113            }
114        }
115        
116        Long maxCacheSizeConf = Config.getInstance().getValue("org.ametys.plugins.messagingconnector.cache.maxsize");
117        Long maxCacheSize = (long) (maxCacheSizeConf != null ? maxCacheSizeConf.intValue() : 1000);
118        
119        Long cacheTtlConf = Config.getInstance().getValue("org.ametys.plugins.messagingconnector.cache.ttl");
120        Long cacheTtl = (long) (cacheTtlConf != null && cacheTtlConf.intValue() > 0 ? cacheTtlConf.intValue() : 60);
121
122        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().expireAfterWrite(cacheTtl, TimeUnit.MINUTES);
123        CacheBuilder<Object, Object> timeoutCacheBuilder = CacheBuilder.newBuilder().expireAfterWrite(TIMEOUT_CACHE_DURATION_SECONDS, TimeUnit.SECONDS);
124        
125        if (maxCacheSize > 0)
126        {
127            cacheBuilder.maximumSize(maxCacheSize);
128            timeoutCacheBuilder.maximumSize(maxCacheSize);
129        }
130        
131        _eventsCache = cacheBuilder.<EventCacheKey, List<CalendarEvent>>build();
132        _eventsCountCache = cacheBuilder.<EventCountCacheKey, Integer>build(); 
133        _emailsCache = cacheBuilder.<EmailCacheKey, List<EmailMessage>>build();
134        _emailsCountCache = cacheBuilder.<UserIdentity, Integer>build();
135        
136        _errorCache = cacheBuilder.<MessagingConnectorException.ExceptionType, Set<UserIdentity>>build();
137        _timeoutErrorCache = timeoutCacheBuilder.<MessagingConnectorException.ExceptionType, Set<UserIdentity>>build();
138    }
139    
140    @Override
141    public List<String> getAllowedPopulationIds()
142    {
143        if (_populationIds.isEmpty())
144        {
145            List<String> userPopulationsIds = _userPopulationDAO.getUserPopulationsIds();
146            if (userPopulationsIds.size() == 1)
147            {
148                return userPopulationsIds;
149            }
150            
151            throw new IllegalStateException("There is more than one population defined. You must set the configuration parameter 'population id' for the messaging connector");
152        }
153        else
154        {
155            return _populationIds;
156        }
157    }
158    
159    /**
160     * True if the user is allowed
161     * @param userIdentity the user identity
162     * @return true if the user is allowed
163     */
164    protected boolean isAllowed(UserIdentity userIdentity)
165    {
166        if (userIdentity == null)
167        {
168            getLogger().warn("There is no connected user to get user's mails or events from messaging connector");
169            return false;
170        }
171        
172        List<String> allowedPopulations = getAllowedPopulationIds();
173        if (!allowedPopulations.contains(userIdentity.getPopulationId()))
174        {
175            getLogger().warn("The user " + userIdentity + " does not belong to any authorized user populations for messaging connector " + allowedPopulations);
176            return false;
177        }
178        
179        return true;
180    }
181    
182    @Override
183    public List<CalendarEvent> getEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException
184    {
185        if (!isAllowed(userIdentity))
186        {
187            return new ArrayList<>();
188        }
189
190        // Check if one of the last calls returned an exception and throw it directly if needed
191        _throwMessagingConnectorExceptionIfInCache(userIdentity);
192        
193        try
194        {
195            EventCacheKey eventCacheKey = new EventCacheKey(userIdentity, maxDays, maxEvents);
196            List<CalendarEvent> events = _eventsCache.getIfPresent(eventCacheKey);
197            if (events == null)
198            {
199                events = internalGetEvents(userIdentity, maxDays, maxEvents);
200                _eventsCache.put(eventCacheKey, events);
201            }
202            return events;
203        }
204        catch (MessagingConnectorException e)
205        {
206            // Save the exception in cache to avoid to call the server again
207            _putExceptionInCache(userIdentity, e.getType());
208            throw e;
209        }
210    }
211    
212    @Override
213    public int getEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException
214    {
215        if (!isAllowed(userIdentity))
216        {
217            return 0;
218        }
219        
220        // Check if one of the last calls returned an exception and throw it directly if needed
221        _throwMessagingConnectorExceptionIfInCache(userIdentity);
222        
223        try
224        {
225            EventCountCacheKey eventCountCacheKey = new EventCountCacheKey(userIdentity, maxDays);
226            Integer eventsCount = _eventsCountCache.getIfPresent(eventCountCacheKey);
227            if (eventsCount == null)
228            {
229                eventsCount = internalGetEventsCount(userIdentity, maxDays);
230                _eventsCountCache.put(eventCountCacheKey, eventsCount);
231            }
232            return eventsCount;
233        }
234        catch (MessagingConnectorException e)
235        {
236            // Save the exception in cache to avoid to call the server again
237            _putExceptionInCache(userIdentity, e.getType());
238            throw e;
239        }
240    }
241
242    @Override
243    public List<EmailMessage> getUnreadEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException
244    {
245        if (!isAllowed(userIdentity))
246        {
247            return new ArrayList<>();
248        }
249        
250        // Check if one of the last calls returned an exception and throw it directly if needed
251        _throwMessagingConnectorExceptionIfInCache(userIdentity);
252        
253        try
254        {
255            EmailCacheKey emailCacheKey = new EmailCacheKey(userIdentity, maxEmails);
256            List<EmailMessage> emails = _emailsCache.getIfPresent(emailCacheKey);
257            if (emails == null)
258            {
259                emails = internalGetEmails(userIdentity, maxEmails);
260                _emailsCache.put(emailCacheKey, emails);
261            }
262            return emails;
263        }
264        catch (MessagingConnectorException e)
265        {
266            // Save the exception in cache to avoid to call the server again
267            _putExceptionInCache(userIdentity, e.getType());
268            throw e;
269        }
270    }
271
272    @Override
273    public int getUnreadEmailCount(UserIdentity userIdentity) throws MessagingConnectorException
274    {
275        if (!isAllowed(userIdentity))
276        {
277            return 0;
278        }
279        
280        // Check if one of the last calls returned an exception and throw it directly if needed
281        _throwMessagingConnectorExceptionIfInCache(userIdentity);
282        
283        try
284        {
285            Integer emailsCount = _emailsCountCache.getIfPresent(userIdentity);
286            if (emailsCount == null)
287            {
288                emailsCount = internalGetEmailsCount(userIdentity);
289                _emailsCountCache.put(userIdentity, emailsCount);
290            }
291            return emailsCount;
292        }
293        catch (MessagingConnectorException e)
294        {
295            // Save the exception in cache to avoid to call the server again
296            _putExceptionInCache(userIdentity, e.getType());
297            throw e;
298        }
299    }
300    
301    /**
302     * Get upcoming events (no caching)
303     * @param userIdentity The user identity
304     * @param maxDays The maximum number of days to search for
305     * @param maxEvents The maximum number of events to retrieve
306     * @return The calendar events
307     * @throws MessagingConnectorException if failed to get events from server
308     */
309    protected abstract List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException;
310    
311    /**
312     * Get upcoming events count (no caching)
313     * @param userIdentity The user identity
314     * @param maxDays The maximum number of days to search for
315     * @return The number of calendar events
316     * @throws MessagingConnectorException if failed to get events from server
317     */
318    protected abstract int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException;
319    
320    /**
321     * Get emails (no caching)
322     * @param userIdentity The user identity
323     * @param maxEmails The maximum number of emails to retrieve
324     * @return The emails
325     * @throws MessagingConnectorException if failed to get events from server
326     */
327    protected abstract List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException;
328    
329    /**
330     * Get the user password for the messaging connector
331     * @param userIdentity user to check
332     * @return the decrypted user password
333     * @throws UserPreferencesException error while reading user preferences
334     */
335    protected String getUserPassword(UserIdentity userIdentity) throws UserPreferencesException
336    {
337        if (supportUserCredential())
338        {
339            String encryptedValue = getUserCryptedPassword(userIdentity);
340            return _cryptoHelper.decrypt(encryptedValue);
341        }
342        else
343        {
344            throw new MessagingConnectorException("Cannot get password for user " + userIdentity + ": user credential are not supported by this messaging connector", MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION);
345        }
346    }
347    
348    /**
349     * Get the user password, still crypted
350     * @param userIdentity user to check
351     * @return the still crypted user password
352     * @throws UserPreferencesException error while reading user preferences
353     */
354    protected String getUserCryptedPassword(UserIdentity userIdentity) throws UserPreferencesException
355    {
356        return _userPref.getUserPreferenceAsString(userIdentity, "/messaging-connector", Collections.emptyMap(), "messaging-connector-password");
357    }
358    
359    @Override
360    public void setUserPassword(UserIdentity userIdentity, String password) throws UserPreferencesException, MessagingConnectorException
361    {
362        if (supportUserCredential())
363        {
364            String cryptedPassword = _cryptoHelper.encrypt(password);
365            _userPref.addUserPreference(userIdentity, "/messaging-connector", Collections.emptyMap(), "messaging-connector-password", cryptedPassword);
366            // Unauthorized cache is invalidated for this user
367            _invalidateExceptionForUserInCache(userIdentity, ExceptionType.UNAUTHORIZED);
368        }
369        else
370        {
371            throw new MessagingConnectorException("Cannot set password for user " + userIdentity + ": user credential are not supported by this messaging connector", MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION);
372        }
373    }
374
375    /**
376     * Get emails count (no caching)
377     * @param userIdentity The user identity
378     * @return The emails count
379     * @throws MessagingConnectorException if failed to get events from server
380     */
381    protected abstract int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException;
382    
383    @Override
384    public boolean supportInvitation() throws MessagingConnectorException
385    {
386        return false;
387    }
388
389    @Override
390    public boolean isEventExist(String eventId, UserIdentity organiser) throws MessagingConnectorException
391    {
392        // Check if one of the last calls returned an exception and throw it directly if needed
393        _throwMessagingConnectorExceptionIfInCache(organiser);
394        try
395        {
396            return internalIsEventExist(eventId, organiser);
397        }
398        catch (MessagingConnectorException e)
399        {
400            // Save the exception in cache to avoid to call the server again
401            _putExceptionInCache(organiser, e.getType());
402            throw e;
403        }
404    }
405
406    /**
407     * True if the event exist in the messaging connector
408     * @param eventId the event id
409     * @param organiser the organiser
410     * @return true if the event exist
411     * @throws MessagingConnectorException if an error occurred
412     */
413    protected boolean internalIsEventExist(String eventId, UserIdentity organiser) throws MessagingConnectorException
414    {
415        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
416    }
417    
418    @Override
419    public String createEvent(String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
420    {
421        // Check if one of the last calls returned an exception and throw it directly if needed
422        _throwMessagingConnectorExceptionIfInCache(organiser);
423        try
424        {
425            return internalCreateEvent(title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees, organiser);
426        }
427        catch (MessagingConnectorException e)
428        {
429            // Save the exception in cache to avoid to call the server again
430            _putExceptionInCache(organiser, e.getType());
431            throw e;
432        }
433    }
434    /**
435     * Create an event
436     * @param title the event title
437     * @param description the event description
438     * @param place the event place
439     * @param isAllDay if the event is all day
440     * @param startDate the event start date
441     * @param endDate the event end date
442     * @param recurrenceType recurrence type
443     * @param untilDate until date of the recurring event
444     * @param attendees the map of attendees (email -&gt; optional or requested) to set
445     * @param organiser the event organiser
446     * @return the id of the event created
447     * @throws MessagingConnectorException if failed to get events from server
448     */
449    protected String internalCreateEvent(String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
450    {
451        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
452    }
453
454    @Override
455    public void updateEvent(String eventId, String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
456    {
457        // Check if one of the last calls returned an exception and throw it directly if needed
458        _throwMessagingConnectorExceptionIfInCache(organiser);
459        try
460        {
461            internalUpdateEvent(eventId, title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees, organiser);
462        }
463        catch (MessagingConnectorException e)
464        {
465            // Save the exception in cache to avoid to call the server again
466            _putExceptionInCache(organiser, e.getType());
467            throw e;
468        }
469    }
470
471    /**
472     * Update an event
473     * @param eventId the event id to delete
474     * @param title the event title
475     * @param description the event description
476     * @param place the event place
477     * @param isAllDay if the event is all day
478     * @param startDate the event start date
479     * @param endDate the event end date
480     * @param recurrenceType recurrence type
481     * @param untilDate until date of the recurring event
482     * @param attendees the map of attendees (email -&gt; optional or requested) to set
483     * @param organiser the event organiser
484     * @throws MessagingConnectorException if failed to get events from server
485     */
486    protected void internalUpdateEvent(String eventId, String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
487    {
488        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
489    }
490
491    @Override
492    public void deleteEvent(String eventId, UserIdentity organiser) throws MessagingConnectorException
493    {
494        // Check if one of the last calls returned an exception and throw it directly if needed
495        _throwMessagingConnectorExceptionIfInCache(organiser);
496        try
497        {
498            internalDeleteEvent(eventId, organiser);
499        }
500        catch (MessagingConnectorException e)
501        {
502            // Save the exception in cache to avoid to call the server again
503            _putExceptionInCache(organiser, e.getType());
504            throw e;
505        }
506    }
507
508    /**
509     * Delete an event
510     * @param eventId the event id to delete
511     * @param organiser the event organiser
512     * @throws MessagingConnectorException if failed to get events from server
513     */
514    protected void internalDeleteEvent(String eventId, UserIdentity organiser) throws MessagingConnectorException
515    {
516        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
517    }
518
519    @Override
520    public Map<String, AttendeeInformation> getAttendees(String eventId, UserIdentity organiser) throws MessagingConnectorException
521    {
522        // Check if one of the last calls returned an exception and throw it directly if needed
523        _throwMessagingConnectorExceptionIfInCache(organiser);
524        try
525        {
526            return internalGetAttendees(eventId, organiser);
527        }
528        catch (MessagingConnectorException e)
529        {
530            // Save the exception in cache to avoid to call the server again
531            _putExceptionInCache(organiser, e.getType());
532            throw e;
533        }
534    }
535
536    /**
537     * Get the map of attendees for an event
538     * @param eventId the event id
539     * @param organiser the event organiser
540     * @return the map of attendees (email -&gt; attendee information)
541     * @throws MessagingConnectorException if failed to get events from server
542     */
543    protected Map<String, AttendeeInformation> internalGetAttendees(String eventId, UserIdentity organiser) throws MessagingConnectorException
544    {
545        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
546    }
547
548    @Override
549    public void setAttendees(String eventId, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
550    {
551        // Check if one of the last calls returned an exception and throw it directly if needed
552        _throwMessagingConnectorExceptionIfInCache(organiser);
553        try
554        {
555            internalSetAttendees(eventId, attendees, organiser);
556        }
557        catch (MessagingConnectorException e)
558        {
559            // Save the exception in cache to avoid to call the server again
560            _putExceptionInCache(organiser, e.getType());
561            throw e;
562        }
563    }
564
565    /**
566     * Set attendees for an event
567     * @param eventId the event id
568     * @param attendees the map of attendees (email -&gt; optional or requested) to set
569     * @param organiser the event organiser
570     * @throws MessagingConnectorException if failed to get events from server
571     */
572    protected void internalSetAttendees(String eventId, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
573    {
574        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
575    }
576
577    @Override
578    public Map<String, FreeBusyStatus> getFreeBusy(Date startDate, Date endDate, boolean isAllDay, Set<String> attendees, UserIdentity organiser) throws MessagingConnectorException
579    {
580        // Check if one of the last calls returned an exception and throw it directly if needed
581        _throwMessagingConnectorExceptionIfInCache(organiser);
582        try
583        {
584            return internalGetFreeBusy(startDate, endDate, isAllDay, attendees, organiser);
585        }
586        catch (MessagingConnectorException e)
587        {
588            // Save the exception in cache to avoid to call the server again
589            _putExceptionInCache(organiser, e.getType());
590            throw e;
591        }
592    }
593
594    /**
595     * Get free/busy status for attendees for a time window
596     * @param startDate the start date
597     * @param endDate the end date
598     * @param isAllDay true if is an allday event
599     * @param attendees the list of attendees email
600     * @param organiser the event organiser
601     * @return the map of attendees (email -&gt; freeBusy status)
602     * @throws MessagingConnectorException if failed to get events from server
603     */
604    protected Map<String, FreeBusyStatus> internalGetFreeBusy(Date startDate, Date endDate, boolean isAllDay, Set<String> attendees, UserIdentity organiser) throws MessagingConnectorException
605    {
606        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
607    }
608
609    @Override
610    public boolean userCredentialNeeded()
611    {
612        boolean credentialNeeded = false;
613        if (supportUserCredential())
614        {
615            UserIdentity user = _currentUserProvider.getUser();
616            if (user != null)
617            {
618                try
619                {
620                    String password = getUserPassword(user);
621                    if (StringUtils.isEmpty(password))
622                    {
623                        credentialNeeded = true;
624                    }
625                }
626                catch (UserPreferencesException e)
627                {
628                    credentialNeeded = true;
629                }
630            }
631        }
632        return credentialNeeded;
633    }
634
635    @Override
636    public boolean supportUserCredential()
637    {
638        return false;
639    }
640    
641    @Override
642    public boolean isUserExist(UserIdentity userIdentity) throws MessagingConnectorException
643    {
644        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
645    }
646    
647    private void _invalidateExceptionForUserInCache(UserIdentity userIdentity, MessagingConnectorException.ExceptionType type)
648    {
649        Set<UserIdentity> usersInCache = _errorCache.getIfPresent(type);
650        if (usersInCache != null)
651        {
652            usersInCache.remove(userIdentity);
653        }
654    }
655    
656    private void _putExceptionInCache(UserIdentity userIdentity, MessagingConnectorException.ExceptionType type)
657    {
658        Cache<ExceptionType, Set<UserIdentity>> cache = null;
659        switch (type)
660        {
661            case TIMEOUT:
662                cache = _timeoutErrorCache;
663                break;
664            case CONFIGURATION_EXCEPTION:
665            case UNAUTHORIZED:
666            case UNKNOWN:
667            default:
668                cache = _errorCache;
669                break;
670        }
671        
672        Set<UserIdentity> usersInCache = cache.getIfPresent(type);
673        if (usersInCache == null)
674        {
675            usersInCache = new HashSet<>();
676            usersInCache.add(userIdentity);
677            cache.put(type, usersInCache);
678        }
679        else
680        {
681            usersInCache.add(userIdentity);
682        }
683    }
684    
685    private MessagingConnectorException.ExceptionType _getExceptionTypeFromCache(UserIdentity userIdentity)
686    {
687        for (Entry<ExceptionType, Set<UserIdentity>> entry : _errorCache.asMap().entrySet())
688        {
689            if (entry.getValue().contains(userIdentity))
690            {
691                // Get the first exception type found for this user (assume that no multiple exception can exist for a same user)
692                return entry.getKey();
693            }
694        }
695        
696        for (Entry<ExceptionType, Set<UserIdentity>> entry : _timeoutErrorCache.asMap().entrySet())
697        {
698            if (entry.getValue().contains(userIdentity))
699            {
700                return entry.getKey();
701            }
702        }
703        
704        return null;
705    }
706    
707    private void _throwMessagingConnectorExceptionIfInCache(UserIdentity userIdentity) throws MessagingConnectorException
708    {
709        MessagingConnectorException.ExceptionType type = _getExceptionTypeFromCache(userIdentity);
710        if (type != null)
711        {
712            throw new MessagingConnectorException(type.name() + " exception was found in cache for user " + userIdentity + ". See previous exception to get the real cause.", type);
713        }
714    }
715    
716    /**
717     * Internal class for key of events cache
718     *
719     */
720    class EventCacheKey
721    {
722        private UserIdentity _userIdentity;
723        private int _maxDays;
724        private int _maxEvents;
725
726        public EventCacheKey (UserIdentity userIdentity, int maxDays, int maxEvents)
727        {
728            _userIdentity = userIdentity;
729            _maxDays = maxDays;
730            _maxEvents = maxEvents;
731        }
732        
733        UserIdentity getUserIdentity()
734        {
735            return _userIdentity;
736        }
737        
738        int getMaxDays()
739        {
740            return _maxDays;
741        }
742        
743        int getMaxEvents()
744        {
745            return _maxEvents;
746        }
747        
748        @Override
749        public int hashCode()
750        {
751            return Objects.hash(_userIdentity, _maxDays, _maxEvents);
752        }
753        
754        @Override
755        public boolean equals(Object obj)
756        {
757            if (obj == null)
758            {
759                return false;
760            }
761            
762            if (!(obj instanceof EventCacheKey))
763            {
764                return false;
765            }
766            
767            EventCacheKey toCompare = (EventCacheKey) obj;
768            
769            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxDays == toCompare.getMaxDays() && _maxEvents == toCompare.getMaxEvents();
770        }
771    }
772    
773    /**
774     * Internal class for key of events count cache
775     *
776     */
777    class EventCountCacheKey
778    {
779        private UserIdentity _userIdentity;
780        private int _maxDays;
781
782        public EventCountCacheKey (UserIdentity userIdentity, int maxDays)
783        {
784            _userIdentity = userIdentity;
785            _maxDays = maxDays;
786        }
787        
788        UserIdentity getUserIdentity()
789        {
790            return _userIdentity;
791        }
792        
793        int getMaxDays()
794        {
795            return _maxDays;
796        }
797        
798        
799        @Override
800        public int hashCode()
801        {
802            return Objects.hash(_userIdentity, _maxDays);
803        }
804        
805        @Override
806        public boolean equals(Object obj)
807        {
808            if (obj == null)
809            {
810                return false;
811            }
812            
813            if (!(obj instanceof EventCountCacheKey))
814            {
815                return false;
816            }
817            
818            EventCountCacheKey toCompare = (EventCountCacheKey) obj;
819            
820            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxDays == toCompare.getMaxDays();
821        }
822    }
823    
824    /**
825     * Internal class for key of events count cache
826     *
827     */
828    class EmailCacheKey
829    {
830        private UserIdentity _userIdentity;
831        private int _maxEmails;
832
833        public EmailCacheKey (UserIdentity userIdentity, int maxEmails)
834        {
835            _userIdentity = userIdentity;
836            _maxEmails = maxEmails;
837        }
838        
839        UserIdentity getUserIdentity()
840        {
841            return _userIdentity;
842        }
843
844        int getMaxEmails()
845        {
846            return _maxEmails;
847        }
848        
849        @Override
850        public int hashCode()
851        {
852            return Objects.hash(_userIdentity, _maxEmails);
853        }
854        
855        @Override
856        public boolean equals(Object obj)
857        {
858            if (obj == null)
859            {
860                return false;
861            }
862            
863            if (!(obj instanceof EmailCacheKey))
864            {
865                return false;
866            }
867            
868            EmailCacheKey toCompare = (EmailCacheKey) obj;
869            
870            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxEmails == toCompare.getMaxEmails();
871        }
872    }
873
874}