package gov.nysenate.openleg.service.entity.member.data; import com.google.common.collect.TreeMultimap; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import gov.nysenate.openleg.dao.base.LimitOffset; import gov.nysenate.openleg.dao.base.SearchIndex; import gov.nysenate.openleg.dao.base.SortOrder; import gov.nysenate.openleg.dao.entity.member.data.MemberDao; import gov.nysenate.openleg.model.base.SessionYear; import gov.nysenate.openleg.model.cache.CacheEvictEvent; import gov.nysenate.openleg.model.cache.CacheEvictIdEvent; import gov.nysenate.openleg.model.cache.CacheWarmEvent; import gov.nysenate.openleg.model.cache.ContentCache; import gov.nysenate.openleg.model.entity.*; import gov.nysenate.openleg.model.search.RebuildIndexEvent; import gov.nysenate.openleg.processor.base.ParseError; import gov.nysenate.openleg.service.base.data.CachingService; import gov.nysenate.openleg.service.entity.member.event.UnverifiedMemberEvent; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.config.CacheConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service public class CachedMemberService implements MemberService, CachingService<Integer> { private static final Logger logger = LoggerFactory.getLogger(CachedMemberService.class); @Autowired private EventBus eventBus; private Cache memberCache; @Autowired private CacheManager cacheManager; @Resource(name = "sqlMember") private MemberDao memberDao; @PostConstruct private void init() { eventBus.register(this); setupCaches(); warmCaches(); } @PreDestroy private void cleanUp() { evictCaches(); cacheManager.removeCache(ContentCache.MEMBER.name()); } /** --- Caching Service Implementation --- */ /** {@inheritDoc} */ @Override public void setupCaches() { this.memberCache = new Cache(new CacheConfiguration().name(ContentCache.MEMBER.name()).eternal(true)); cacheManager.addCache(this.memberCache); } /** {@inheritDoc} */ @Override public List<Ehcache> getCaches() { return Arrays.asList(memberCache); } /** {@inheritDoc} */ @Override @Subscribe public void handleCacheEvictEvent(CacheEvictEvent evictEvent) { if (evictEvent.affects(ContentCache.MEMBER)) { evictCaches(); } } /** {@inheritDoc} */ @Subscribe @Override public void handleCacheEvictIdEvent(CacheEvictIdEvent<Integer> evictIdEvent) { if (evictIdEvent.affects(ContentCache.MEMBER)) { evictContent(evictIdEvent.getContentId()); } } /** {@inheritDoc} */ @Override public void evictContent(Integer sessionMemberId) { memberCache.remove(sessionMemberId); } /** {@inheritDoc} */ @Override public void warmCaches() { evictCaches(); logger.info("Warming up member cache"); memberDao.getAllMembers(SortOrder.ASC, LimitOffset.ALL).stream().forEach(this::putMemberInCache); logger.info("Done warming up member cache"); } /** {@inheritDoc} */ @Override @Subscribe public void handleCacheWarmEvent(CacheWarmEvent warmEvent) { if (warmEvent.affects(ContentCache.MEMBER)) { warmCaches(); } } /** --- MemberService implementation --- */ /** {@inheritDoc} */ @Override public SessionMember getMemberById(int memberId, SessionYear sessionYear) throws MemberNotFoundEx { if (memberId <= 0) { throw new IllegalArgumentException("Member Id cannot be less than or equal to 0."); } try { return memberDao.getMemberById(memberId, sessionYear); } catch (EmptyResultDataAccessException ex) { throw new MemberNotFoundEx(memberId, sessionYear); } } @Override public TreeMultimap<SessionYear, SessionMember> getMemberById(int memberId) throws MemberNotFoundEx { if (memberId <= 0) { throw new IllegalArgumentException("Member Id cannot be less than or equal to 0."); } TreeMultimap<SessionYear, SessionMember> sessionMemberMap = memberDao.getMemberById(memberId); if (sessionMemberMap.isEmpty()) { throw new MemberNotFoundEx(memberId); } return sessionMemberMap; } /** {@inheritDoc} */ @Override public SessionMember getMemberBySessionId(int sessionMemberId) throws MemberNotFoundEx { SimpleKey key = new SimpleKey(sessionMemberId); if (memberCache.isKeyInCache(key)) { return (SessionMember) memberCache.get(key).getObjectValue(); } try { SessionMember member = memberDao.getMemberBySessionId(sessionMemberId); putMemberInCache(member); return member; } catch (EmptyResultDataAccessException ex) { throw new MemberNotFoundEx(sessionMemberId); } } /** {@inheritDoc} */ @Override public SessionMember getMemberByShortName(String lbdcShortName, SessionYear sessionYear, Chamber chamber) throws MemberNotFoundEx { if (lbdcShortName == null || chamber == null) { throw new IllegalArgumentException("Shortname and/or chamber cannot be null."); } try { return memberDao.getMemberByShortName(lbdcShortName, sessionYear, chamber); } catch (EmptyResultDataAccessException ex) { throw new MemberNotFoundEx(lbdcShortName, sessionYear, chamber); } } /** {@inheritDoc} */ @Override public SessionMember getMemberByShortNameEnsured(String lbdcShortName, SessionYear sessionYear, Chamber chamber) throws ParseError { try { return getMemberByShortName(lbdcShortName, sessionYear, chamber); } catch (MemberNotFoundEx ex) { SessionMember member = SessionMember.newMakeshiftMember(lbdcShortName, sessionYear, chamber); memberDao.updatePerson(member); memberDao.updateMember(member); memberDao.updateSessionMember(member); eventBus.post(new UnverifiedMemberEvent(member, LocalDateTime.now())); return member; } } /** {@inheritDoc} */ @Override public List<SessionMember> getAllMembers(SortOrder sortOrder, LimitOffset limOff) { return memberDao.getAllMembers(sortOrder, limOff); } /** {@inheritDoc} */ @Override public List<FullMember> getAllFullMembers() { return getAllMembers(SortOrder.ASC, LimitOffset.ALL).stream() .collect(Collectors.groupingBy(SessionMember::getMemberId, LinkedHashMap::new, Collectors.toList())) .values().stream() .map(FullMember::new) .collect(Collectors.toList()); } /** {@inheritDoc} */ @Override public void updateMembers(List<SessionMember> sessionMembers) { Collection<? extends Person> persons = sessionMembers.stream() .collect(Collectors.toMap(Person::getPersonId, Function.identity())) .values(); Collection<SessionMember> members = sessionMembers.stream() .collect(Collectors.toMap(SessionMember::getMemberId, Function.identity())) .values(); persons.forEach(memberDao::updatePerson); members.forEach(memberDao::updateMember); sessionMembers.forEach(memberDao::updateSessionMember); memberDao.clearOrphans(); // We need to rebuild cache and search index to account for session members that were // tangentially modified via a person or member update eventBus.post(new CacheWarmEvent(Collections.singleton(ContentCache.MEMBER))); eventBus.post(new RebuildIndexEvent(Collections.singleton(SearchIndex.MEMBER))); } /** --- Internal Methods --- */ private void putMemberInCache(SessionMember member) { memberCache.put(new Element(new SimpleKey(member.getSessionMemberId()), member, true)); } }