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(TIMEOUT_CACHE_DURATION_SECONDS));
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()
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 IllegalStateException("There is more than one population defined. You must set the configuration parameter 'population id' for the messaging connector");
202        }
203        else
204        {
205            return _populationIds;
206        }
207    }
208    
209    /**
210     * True if the user is allowed
211     * @param userIdentity the user identity
212     * @return true if the user is allowed
213     */
214    protected boolean isAllowed(UserIdentity userIdentity)
215    {
216        if (userIdentity == null)
217        {
218            getLogger().warn("There is no connected user to get user's mails or events from messaging connector");
219            return false;
220        }
221        
222        List<String> allowedPopulations = getAllowedPopulationIds();
223        if (!allowedPopulations.contains(userIdentity.getPopulationId()))
224        {
225            getLogger().warn("The user " + userIdentity + " does not belong to any authorized user populations for messaging connector " + allowedPopulations);
226            return false;
227        }
228        
229        return true;
230    }
231    
232    @Override
233    public List<CalendarEvent> getEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException
234    {
235        if (!isAllowed(userIdentity))
236        {
237            return new ArrayList<>();
238        }
239
240        // Check if one of the last calls returned an exception and throw it directly if needed
241        _throwMessagingConnectorExceptionIfInCache(userIdentity);
242        
243        try
244        {
245            EventCacheKey eventCacheKey = new EventCacheKey(userIdentity, maxDays, maxEvents);
246            return _getEventsCache().get(eventCacheKey, key -> internalGetEvents(userIdentity, maxDays, maxEvents));
247        }
248        catch (CacheException e)
249        {
250            if (e.getCause() instanceof MessagingConnectorException)
251            {
252                MessagingConnectorException mce = (MessagingConnectorException) e.getCause();
253                // Save the exception in cache to avoid to call the server again
254                _putExceptionInCache(userIdentity, mce.getType());
255                throw mce;
256            }
257            throw e;
258        }
259    }
260    
261    @Override
262    public int getEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException
263    {
264        if (!isAllowed(userIdentity))
265        {
266            return 0;
267        }
268        
269        // Check if one of the last calls returned an exception and throw it directly if needed
270        _throwMessagingConnectorExceptionIfInCache(userIdentity);
271        
272        try
273        {
274            EventCountCacheKey eventCountCacheKey = new EventCountCacheKey(userIdentity, maxDays);
275            return _getEventsCountCache().get(eventCountCacheKey, key -> internalGetEventsCount(userIdentity, maxDays));
276        }
277        catch (MessagingConnectorException e)
278        {
279            // Save the exception in cache to avoid to call the server again
280            _putExceptionInCache(userIdentity, e.getType());
281            throw e;
282        }
283    }
284
285    @Override
286    public List<EmailMessage> getUnreadEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException
287    {
288        if (!isAllowed(userIdentity))
289        {
290            return new ArrayList<>();
291        }
292        
293        // Check if one of the last calls returned an exception and throw it directly if needed
294        _throwMessagingConnectorExceptionIfInCache(userIdentity);
295        
296        try
297        {
298            EmailCacheKey emailCacheKey = new EmailCacheKey(userIdentity, maxEmails);
299            return _getEmailsCache().get(emailCacheKey, key -> internalGetEmails(userIdentity, maxEmails));
300        }
301        catch (CacheException e)
302        {
303            if (e.getCause() instanceof MessagingConnectorException)
304            {
305                MessagingConnectorException mce = (MessagingConnectorException) e.getCause();
306                // Save the exception in cache to avoid to call the server again
307                _putExceptionInCache(userIdentity, mce.getType());
308                throw mce;
309            }
310            throw e;
311        }
312    }
313
314    @Override
315    public int getUnreadEmailCount(UserIdentity userIdentity) throws MessagingConnectorException
316    {
317        if (!isAllowed(userIdentity))
318        {
319            return 0;
320        }
321        
322        // Check if one of the last calls returned an exception and throw it directly if needed
323        _throwMessagingConnectorExceptionIfInCache(userIdentity);
324        
325        try
326        {
327            return _getEmailsCountCache().get(userIdentity, key -> internalGetEmailsCount(userIdentity));
328        }
329        catch (CacheException e)
330        {
331            if (e.getCause() instanceof MessagingConnectorException)
332            {
333                MessagingConnectorException mce = (MessagingConnectorException) e.getCause();
334                // Save the exception in cache to avoid to call the server again
335                _putExceptionInCache(userIdentity, mce.getType());
336                throw mce;
337            }
338            throw e;
339        }
340    }
341    
342    /**
343     * Get upcoming events (no caching)
344     * @param userIdentity The user identity
345     * @param maxDays The maximum number of days to search for
346     * @param maxEvents The maximum number of events to retrieve
347     * @return The calendar events
348     * @throws MessagingConnectorException if failed to get events from server
349     */
350    protected abstract List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException;
351    
352    /**
353     * Get upcoming events count (no caching)
354     * @param userIdentity The user identity
355     * @param maxDays The maximum number of days to search for
356     * @return The number of calendar events
357     * @throws MessagingConnectorException if failed to get events from server
358     */
359    protected abstract int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException;
360    
361    /**
362     * Get emails (no caching)
363     * @param userIdentity The user identity
364     * @param maxEmails The maximum number of emails to retrieve
365     * @return The emails
366     * @throws MessagingConnectorException if failed to get events from server
367     */
368    protected abstract List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException;
369    
370    /**
371     * Get the user password for the messaging connector
372     * @param userIdentity user to check
373     * @return the decrypted user password
374     * @throws UserPreferencesException error while reading user preferences
375     */
376    protected String getUserPassword(UserIdentity userIdentity) throws UserPreferencesException
377    {
378        if (supportUserCredential(userIdentity))
379        {
380            String encryptedValue = getUserCryptedPassword(userIdentity);
381            return _cryptoHelper.decrypt(encryptedValue);
382        }
383        else
384        {
385            throw new MessagingConnectorException("Cannot get password for user " + userIdentity + ": user credential are not supported by this messaging connector", MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION);
386        }
387    }
388    
389    /**
390     * Get the user password, still crypted
391     * @param userIdentity user to check
392     * @return the still crypted user password
393     * @throws UserPreferencesException error while reading user preferences
394     */
395    protected String getUserCryptedPassword(UserIdentity userIdentity) throws UserPreferencesException
396    {
397        return _userPref.getUserPreferenceAsString(userIdentity, "/messaging-connector", Collections.emptyMap(), "messaging-connector-password");
398    }
399    
400    @Override
401    public void setUserPassword(UserIdentity userIdentity, String password) throws UserPreferencesException, MessagingConnectorException
402    {
403        if (supportUserCredential(userIdentity))
404        {
405            String cryptedPassword = _cryptoHelper.encrypt(password);
406            _userPref.addUserPreference(userIdentity, "/messaging-connector", Collections.emptyMap(), "messaging-connector-password", cryptedPassword);
407            // Unauthorized cache is invalidated for this user
408            _invalidateExceptionForUserInCache(userIdentity, ExceptionType.UNAUTHORIZED);
409        }
410        else
411        {
412            throw new MessagingConnectorException("Cannot set password for user " + userIdentity + ": user credential are not supported by this messaging connector", MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION);
413        }
414    }
415
416    /**
417     * Get emails count (no caching)
418     * @param userIdentity The user identity
419     * @return The emails count
420     * @throws MessagingConnectorException if failed to get events from server
421     */
422    protected abstract int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException;
423    
424    @Override
425    public boolean supportInvitation() throws MessagingConnectorException
426    {
427        return false;
428    }
429
430    @Override
431    public boolean isEventExist(String eventId, UserIdentity organiser) throws MessagingConnectorException
432    {
433        // Check if one of the last calls returned an exception and throw it directly if needed
434        _throwMessagingConnectorExceptionIfInCache(organiser);
435        try
436        {
437            return internalIsEventExist(eventId, organiser);
438        }
439        catch (MessagingConnectorException e)
440        {
441            // Save the exception in cache to avoid to call the server again
442            _putExceptionInCache(organiser, e.getType());
443            throw e;
444        }
445    }
446
447    /**
448     * True if the event exist in the messaging connector
449     * @param eventId the event id
450     * @param organiser the organiser
451     * @return true if the event exist
452     * @throws MessagingConnectorException if an error occurred
453     */
454    protected boolean internalIsEventExist(String eventId, UserIdentity organiser) throws MessagingConnectorException
455    {
456        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
457    }
458    
459    @Override
460    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
461    {
462        // Check if one of the last calls returned an exception and throw it directly if needed
463        _throwMessagingConnectorExceptionIfInCache(organiser);
464        try
465        {
466            return internalCreateEvent(title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees, organiser);
467        }
468        catch (MessagingConnectorException e)
469        {
470            // Save the exception in cache to avoid to call the server again
471            _putExceptionInCache(organiser, e.getType());
472            throw e;
473        }
474    }
475    /**
476     * Create an event
477     * @param title the event title
478     * @param description the event description
479     * @param place the event place
480     * @param isAllDay if the event is all day
481     * @param startDate the event start date
482     * @param endDate the event end date
483     * @param recurrenceType recurrence type
484     * @param untilDate until date of the recurring event
485     * @param attendees the map of attendees (email -&gt; optional or requested) to set
486     * @param organiser the event organiser
487     * @return the id of the event created
488     * @throws MessagingConnectorException if failed to get events from server
489     */
490    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
491    {
492        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
493    }
494
495    @Override
496    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
497    {
498        // Check if one of the last calls returned an exception and throw it directly if needed
499        _throwMessagingConnectorExceptionIfInCache(organiser);
500        try
501        {
502            internalUpdateEvent(eventId, title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees, organiser);
503        }
504        catch (MessagingConnectorException e)
505        {
506            // Save the exception in cache to avoid to call the server again
507            _putExceptionInCache(organiser, e.getType());
508            throw e;
509        }
510    }
511
512    /**
513     * Update an event
514     * @param eventId the event id to delete
515     * @param title the event title
516     * @param description the event description
517     * @param place the event place
518     * @param isAllDay if the event is all day
519     * @param startDate the event start date
520     * @param endDate the event end date
521     * @param recurrenceType recurrence type
522     * @param untilDate until date of the recurring event
523     * @param attendees the map of attendees (email -&gt; optional or requested) to set
524     * @param organiser the event organiser
525     * @throws MessagingConnectorException if failed to get events from server
526     */
527    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
528    {
529        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
530    }
531
532    @Override
533    public void deleteEvent(String eventId, UserIdentity organiser) throws MessagingConnectorException
534    {
535        // Check if one of the last calls returned an exception and throw it directly if needed
536        _throwMessagingConnectorExceptionIfInCache(organiser);
537        try
538        {
539            internalDeleteEvent(eventId, organiser);
540        }
541        catch (MessagingConnectorException e)
542        {
543            // Save the exception in cache to avoid to call the server again
544            _putExceptionInCache(organiser, e.getType());
545            throw e;
546        }
547    }
548
549    /**
550     * Delete an event
551     * @param eventId the event id to delete
552     * @param organiser the event organiser
553     * @throws MessagingConnectorException if failed to get events from server
554     */
555    protected void internalDeleteEvent(String eventId, UserIdentity organiser) throws MessagingConnectorException
556    {
557        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
558    }
559
560    @Override
561    public Map<String, AttendeeInformation> getAttendees(String eventId, UserIdentity organiser) throws MessagingConnectorException
562    {
563        // Check if one of the last calls returned an exception and throw it directly if needed
564        _throwMessagingConnectorExceptionIfInCache(organiser);
565        try
566        {
567            return internalGetAttendees(eventId, organiser);
568        }
569        catch (MessagingConnectorException e)
570        {
571            // Save the exception in cache to avoid to call the server again
572            _putExceptionInCache(organiser, e.getType());
573            throw e;
574        }
575    }
576
577    /**
578     * Get the map of attendees for an event
579     * @param eventId the event id
580     * @param organiser the event organiser
581     * @return the map of attendees (email -&gt; attendee information)
582     * @throws MessagingConnectorException if failed to get events from server
583     */
584    protected Map<String, AttendeeInformation> internalGetAttendees(String eventId, UserIdentity organiser) throws MessagingConnectorException
585    {
586        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
587    }
588
589    @Override
590    public void setAttendees(String eventId, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
591    {
592        // Check if one of the last calls returned an exception and throw it directly if needed
593        _throwMessagingConnectorExceptionIfInCache(organiser);
594        try
595        {
596            internalSetAttendees(eventId, attendees, organiser);
597        }
598        catch (MessagingConnectorException e)
599        {
600            // Save the exception in cache to avoid to call the server again
601            _putExceptionInCache(organiser, e.getType());
602            throw e;
603        }
604    }
605
606    /**
607     * Set attendees for an event
608     * @param eventId the event id
609     * @param attendees the map of attendees (email -&gt; optional or requested) to set
610     * @param organiser the event organiser
611     * @throws MessagingConnectorException if failed to get events from server
612     */
613    protected void internalSetAttendees(String eventId, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
614    {
615        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
616    }
617
618    @Override
619    public Map<String, FreeBusyStatus> getFreeBusy(Date startDate, Date endDate, boolean isAllDay, Set<String> attendees, UserIdentity organiser) throws MessagingConnectorException
620    {
621        // Check if one of the last calls returned an exception and throw it directly if needed
622        _throwMessagingConnectorExceptionIfInCache(organiser);
623        try
624        {
625            return internalGetFreeBusy(startDate, endDate, isAllDay, attendees, organiser);
626        }
627        catch (MessagingConnectorException e)
628        {
629            // Save the exception in cache to avoid to call the server again
630            _putExceptionInCache(organiser, e.getType());
631            throw e;
632        }
633    }
634
635    /**
636     * Get free/busy status for attendees for a time window
637     * @param startDate the start date
638     * @param endDate the end date
639     * @param isAllDay true if is an allday event
640     * @param attendees the list of attendees email
641     * @param organiser the event organiser
642     * @return the map of attendees (email -&gt; freeBusy status)
643     * @throws MessagingConnectorException if failed to get events from server
644     */
645    protected Map<String, FreeBusyStatus> internalGetFreeBusy(Date startDate, Date endDate, boolean isAllDay, Set<String> attendees, UserIdentity organiser) throws MessagingConnectorException
646    {
647        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
648    }
649
650    @Override
651    public boolean userCredentialNeeded(UserIdentity userIdentity)
652    {
653        boolean credentialNeeded = false;
654        if (supportUserCredential(userIdentity))
655        {
656            UserIdentity user = _currentUserProvider.getUser();
657            if (user != null)
658            {
659                try
660                {
661                    String password = getUserPassword(user);
662                    if (StringUtils.isEmpty(password))
663                    {
664                        credentialNeeded = true;
665                    }
666                }
667                catch (UserPreferencesException e)
668                {
669                    credentialNeeded = true;
670                }
671            }
672        }
673        return credentialNeeded;
674    }
675
676    @Override
677    public boolean supportUserCredential(UserIdentity userIdentity)
678    {
679        return false;
680    }
681    
682    @Override
683    public boolean isUserExist(UserIdentity userIdentity) throws MessagingConnectorException
684    {
685        throw new UnsupportedOperationException("Invitation is not implemented for messaging connector");
686    }
687    
688    private void _invalidateExceptionForUserInCache(UserIdentity userIdentity, MessagingConnectorException.ExceptionType type)
689    {
690        Set<UserIdentity> usersInCache = _getErrorCache().get(type);
691        if (usersInCache != null)
692        {
693            usersInCache.remove(userIdentity);
694        }
695    }
696    
697    private void _putExceptionInCache(UserIdentity userIdentity, MessagingConnectorException.ExceptionType type)
698    {
699        Cache<ExceptionType, Set<UserIdentity>> cache = null;
700        switch (type)
701        {
702            case TIMEOUT:
703                cache = _getTimeoutErrorCache();
704                break;
705            case CONFIGURATION_EXCEPTION:
706            case UNAUTHORIZED:
707            case UNKNOWN:
708            default:
709                cache = _getErrorCache();
710                break;
711        }
712        
713        Set<UserIdentity> usersInCache = cache.get(type);
714        if (usersInCache == null)
715        {
716            usersInCache = new HashSet<>();
717            usersInCache.add(userIdentity);
718            cache.put(type, usersInCache);
719        }
720        else
721        {
722            usersInCache.add(userIdentity);
723        }
724    }
725    
726    private MessagingConnectorException.ExceptionType _getExceptionTypeFromCache(UserIdentity userIdentity)
727    {
728        for (Entry<ExceptionType, Set<UserIdentity>> entry : _getErrorCache().asMap().entrySet())
729        {
730            if (entry.getValue().contains(userIdentity))
731            {
732                // Get the first exception type found for this user (assume that no multiple exception can exist for a same user)
733                return entry.getKey();
734            }
735        }
736        
737        for (Entry<ExceptionType, Set<UserIdentity>> entry : _getTimeoutErrorCache().asMap().entrySet())
738        {
739            if (entry.getValue().contains(userIdentity))
740            {
741                return entry.getKey();
742            }
743        }
744        
745        return null;
746    }
747    
748    private void _throwMessagingConnectorExceptionIfInCache(UserIdentity userIdentity) throws MessagingConnectorException
749    {
750        MessagingConnectorException.ExceptionType type = _getExceptionTypeFromCache(userIdentity);
751        if (type != null)
752        {
753            throw new MessagingConnectorException(type.name() + " exception was found in cache for user " + userIdentity + ". See previous exception to get the real cause.", type);
754        }
755    }
756    
757    /**
758     * Internal class for key of events cache
759     *
760     */
761    static class EventCacheKey
762    {
763        private UserIdentity _userIdentity;
764        private int _maxDays;
765        private int _maxEvents;
766
767        public EventCacheKey (UserIdentity userIdentity, int maxDays, int maxEvents)
768        {
769            _userIdentity = userIdentity;
770            _maxDays = maxDays;
771            _maxEvents = maxEvents;
772        }
773        
774        UserIdentity getUserIdentity()
775        {
776            return _userIdentity;
777        }
778        
779        int getMaxDays()
780        {
781            return _maxDays;
782        }
783        
784        int getMaxEvents()
785        {
786            return _maxEvents;
787        }
788        
789        @Override
790        public int hashCode()
791        {
792            return Objects.hash(_userIdentity, _maxDays, _maxEvents);
793        }
794        
795        @Override
796        public boolean equals(Object obj)
797        {
798            if (obj == null)
799            {
800                return false;
801            }
802            
803            if (!(obj instanceof EventCacheKey))
804            {
805                return false;
806            }
807            
808            EventCacheKey toCompare = (EventCacheKey) obj;
809            
810            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxDays == toCompare.getMaxDays() && _maxEvents == toCompare.getMaxEvents();
811        }
812    }
813    
814    /**
815     * Internal class for key of events count cache
816     *
817     */
818    static class EventCountCacheKey
819    {
820        private UserIdentity _userIdentity;
821        private int _maxDays;
822
823        public EventCountCacheKey (UserIdentity userIdentity, int maxDays)
824        {
825            _userIdentity = userIdentity;
826            _maxDays = maxDays;
827        }
828        
829        UserIdentity getUserIdentity()
830        {
831            return _userIdentity;
832        }
833        
834        int getMaxDays()
835        {
836            return _maxDays;
837        }
838        
839        
840        @Override
841        public int hashCode()
842        {
843            return Objects.hash(_userIdentity, _maxDays);
844        }
845        
846        @Override
847        public boolean equals(Object obj)
848        {
849            if (obj == null)
850            {
851                return false;
852            }
853            
854            if (!(obj instanceof EventCountCacheKey))
855            {
856                return false;
857            }
858            
859            EventCountCacheKey toCompare = (EventCountCacheKey) obj;
860            
861            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxDays == toCompare.getMaxDays();
862        }
863    }
864    
865    /**
866     * Internal class for key of events count cache
867     *
868     */
869    static class EmailCacheKey
870    {
871        private UserIdentity _userIdentity;
872        private int _maxEmails;
873
874        public EmailCacheKey (UserIdentity userIdentity, int maxEmails)
875        {
876            _userIdentity = userIdentity;
877            _maxEmails = maxEmails;
878        }
879        
880        UserIdentity getUserIdentity()
881        {
882            return _userIdentity;
883        }
884
885        int getMaxEmails()
886        {
887            return _maxEmails;
888        }
889        
890        @Override
891        public int hashCode()
892        {
893            return Objects.hash(_userIdentity, _maxEmails);
894        }
895        
896        @Override
897        public boolean equals(Object obj)
898        {
899            if (obj == null)
900            {
901                return false;
902            }
903            
904            if (!(obj instanceof EmailCacheKey))
905            {
906                return false;
907            }
908            
909            EmailCacheKey toCompare = (EmailCacheKey) obj;
910            
911            return _userIdentity.equals(toCompare.getUserIdentity()) && _maxEmails == toCompare.getMaxEmails();
912        }
913    }
914
915    private Cache<EventCacheKey, List<CalendarEvent>> _getEventsCache() 
916    {
917        return this._cacheManager.get(EVENTS_CACHE);
918    }
919    
920    private Cache<EventCountCacheKey, Integer> _getEventsCountCache() 
921    {
922        return this._cacheManager.get(EVENTS_COUNT_CACHE);
923    }
924    
925    private Cache<EmailCacheKey, List<EmailMessage>> _getEmailsCache() 
926    {
927        return this._cacheManager.get(EMAILS_CACHE);
928    }
929    
930    private Cache<UserIdentity, Integer> _getEmailsCountCache() 
931    {
932        return this._cacheManager.get(EMAILS_COUNT_CACHE);
933    }
934    
935    private Cache<ExceptionType, Set<UserIdentity>> _getErrorCache() 
936    {
937        return this._cacheManager.get(ERROR_CACHE);
938    }
939    
940    private Cache<ExceptionType, Set<UserIdentity>> _getTimeoutErrorCache() 
941    {
942        return this._cacheManager.get(TIMEOUT_ERROR_CACHE);
943    }
944
945}