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