package gov.nysenate.openleg.service.law.data; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import gov.nysenate.openleg.dao.law.data.LawDataDao; import gov.nysenate.openleg.model.cache.CacheEvictIdEvent; import gov.nysenate.openleg.model.cache.CacheWarmEvent; import gov.nysenate.openleg.model.law.*; import gov.nysenate.openleg.model.cache.CacheEvictEvent; import gov.nysenate.openleg.model.cache.ContentCache; import gov.nysenate.openleg.service.base.data.CachingService; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.config.CacheConfiguration; import net.sf.ehcache.config.MemoryUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.ehcache.EhCacheCache; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDate; import java.util.*; import static java.util.stream.Collectors.toList; /** * Service interface for retrieving and saving NYS Law data. */ @Service public class CachedLawDataService implements LawDataService, CachingService<LawVersionId> { private static final Logger logger = LoggerFactory.getLogger(CachedLawDataService.class); @Autowired private LawDataDao lawDataDao; @Autowired private CacheManager cacheManager; @Autowired private EventBus eventBus; @Value("${law.cache.size}") private long lawTreeCacheHeapSize; private EhCacheCache lawTreeCache; private Map<String, LocalDate> maxPubDates = new HashMap<>(); @PostConstruct private void init() { eventBus.register(this); setupCaches(); maxPubDates = lawDataDao.getLastPublishedMap(); } @PreDestroy private void cleanUp() { evictCaches(); cacheManager.removeCache(ContentCache.LAW.name()); maxPubDates.clear(); } /** --- CachingService implementation --- */ /** {@inheritDoc} */ @Override public List<Ehcache> getCaches() { return Arrays.asList(lawTreeCache.getNativeCache()); } /** {@inheritDoc} */ @Override public void setupCaches() { Cache cache = new Cache(new CacheConfiguration().name(ContentCache.LAW.name()) .eternal(true) .maxBytesLocalHeap(lawTreeCacheHeapSize, MemoryUnit.MEGABYTES) .sizeOfPolicy(defaultSizeOfPolicy())); cacheManager.addCache(cache); this.lawTreeCache = new EhCacheCache(cache); } /** {@inheritDoc} */ @Override @Subscribe public void handleCacheEvictEvent(CacheEvictEvent evictEvent) { if (evictEvent.affects(ContentCache.LAW)) { evictCaches(); maxPubDates.clear(); } } /** {@inheritDoc} */ @Subscribe @Override public void handleCacheEvictIdEvent(CacheEvictIdEvent<LawVersionId> evictIdEvent) { if (evictIdEvent.affects(ContentCache.LAW)) { evictContent(evictIdEvent.getContentId()); } } /** {@inheritDoc} */ @Override public void evictContent(LawVersionId lawVersionId) { lawTreeCache.evict(lawVersionId); maxPubDates.clear(); } /** {@inheritDoc} */ @Override public void warmCaches() { try { logger.info("Warming up law cache.."); getLawInfos().forEach(lawInfo -> getLawTree(lawInfo.getLawId(), LocalDate.now())); logger.info("Finished warming up law cache.."); } catch (LawTreeNotFoundEx ex) { logger.warn("Failed to warm up law cache!.", ex); } } /** {@inheritDoc} */ @Override @Subscribe public void handleCacheWarmEvent(CacheWarmEvent warmEvent) { if (warmEvent.affects(ContentCache.LAW)) { warmCaches(); } } /** --- LawDataService implementation --- */ /** {@inheritDoc} */ @Override public List<LawInfo> getLawInfos() { return lawDataDao.getLawInfos().stream().sorted().collect(toList()); } /** {@inheritDoc} */ @Override public LawTree getLawTree(String lawId, LocalDate endPublishedDate) throws LawTreeNotFoundEx { if (lawId == null) throw new IllegalArgumentException("Supplied lawId cannot be null"); try { if (endPublishedDate == null) { if (maxPubDates.isEmpty()) { maxPubDates = lawDataDao.getLastPublishedMap(); } endPublishedDate = maxPubDates.get(lawId); } LawVersionId lawVersionId = new LawVersionId(lawId.toUpperCase(), endPublishedDate); LawTree lawTree; if (lawTreeCache.get(lawVersionId) != null) { lawTree = (LawTree) lawTreeCache.get(lawVersionId).get(); } else { lawTree = lawDataDao.getLawTree(lawId, endPublishedDate); lawTreeCache.put(lawTree.getLawVersionId(), lawTree); } return lawTree; } catch (EmptyResultDataAccessException ex) { throw new LawTreeNotFoundEx(lawId, endPublishedDate, ex.getMessage()); } } /** {@inheritDoc} */ @Override public LawDocInfo getLawDocInfo(String documentId, LocalDate endPublishedDate) throws LawDocumentNotFoundEx { if (documentId == null || documentId.length() < 4) { throw new IllegalArgumentException("Document id cannot be less than 4 characters"); } Optional<LawTreeNode> node = getLawTree(documentId.substring(0, 3), endPublishedDate).find(documentId); if (node.isPresent()) { return node.get().getLawDocInfo(); } else { throw new LawDocumentNotFoundEx(documentId, endPublishedDate, "Law tree was found but document was not matched"); } } /** {@inheritDoc} */ @Override public LawDocument getLawDocument(String documentId, LocalDate endPublishedDate) throws LawDocumentNotFoundEx { if (documentId == null) throw new IllegalArgumentException("Supplied documentId cannot be null"); if (endPublishedDate == null) endPublishedDate = LocalDate.now(); try { return lawDataDao.getLawDocument(documentId.toUpperCase(), endPublishedDate); } catch (EmptyResultDataAccessException ex) { throw new LawDocumentNotFoundEx(documentId, endPublishedDate, ""); } } /** {@inheritDoc} */ @Override public Map<String, LawDocument> getLawDocuments(String lawId, LocalDate endPublishedDate) { if (lawId == null) throw new IllegalArgumentException("Supplied lawId cannot be null"); if (endPublishedDate == null) endPublishedDate = LocalDate.now(); return lawDataDao.getLawDocuments(lawId.toUpperCase(), endPublishedDate); } /** {@inheritDoc} */ @Override public void saveLawTree(LawFile lawFile, LawTree lawTree) { if (lawTree == null) throw new IllegalArgumentException("Supplied lawTree cannot be null"); lawDataDao.updateLawTree(lawFile, lawTree); lawTreeCache.put(lawTree.getLawVersionId(), lawTree); maxPubDates.clear(); } /** {@inheritDoc} */ @Override public void saveLawDocument(LawFile lawFile, LawDocument lawDocument) { if (lawDocument == null) throw new IllegalArgumentException("Supplied lawDocument cannot be null"); if (lawFile == null) throw new IllegalArgumentException("Supplied lawFile cannot be null"); lawDataDao.updateLawDocument(lawFile, lawDocument); } }