package gov.nysenate.openleg.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.SubscriberExceptionContext;
import com.google.common.eventbus.SubscriberExceptionHandler;
import gov.nysenate.openleg.model.agenda.Agenda;
import gov.nysenate.openleg.model.agenda.AgendaId;
import gov.nysenate.openleg.model.bill.BaseBillId;
import gov.nysenate.openleg.model.bill.Bill;
import gov.nysenate.openleg.model.calendar.CalendarId;
import gov.nysenate.openleg.model.sobi.SobiFragment;
import gov.nysenate.openleg.processor.base.IngestCache;
import gov.nysenate.openleg.util.AsciiArt;
import gov.nysenate.openleg.util.OpenlegThreadFactory;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.SizeOfPolicyConfiguration;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.index.query.SimpleQueryParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
@EnableCaching
public class ApplicationConfig implements CachingConfigurer
{
private static final Logger logger = LoggerFactory.getLogger(ApplicationConfig.class);
/** --- Eh Cache Spring Configuration --- */
@Value("${cache.max.size}") private String cacheMaxHeapSize;
@Bean(destroyMethod = "shutdown")
public net.sf.ehcache.CacheManager pooledCacheManger() {
// Set the upper limit when computing heap size for objects. Once it reaches the limit
// it stops computing further. Some objects can contain many references so we set the limit
// fairly high.
SizeOfPolicyConfiguration sizeOfConfig = new SizeOfPolicyConfiguration();
sizeOfConfig.setMaxDepth(100000);
sizeOfConfig.setMaxDepthExceededBehavior("continue");
// Configure the default cache to be used as a template for actual caches.
CacheConfiguration cacheConfiguration = new CacheConfiguration();
cacheConfiguration.setMemoryStoreEvictionPolicy("LRU");
cacheConfiguration.addSizeOfPolicy(sizeOfConfig);
// Configure the cache manager.
net.sf.ehcache.config.Configuration config = new net.sf.ehcache.config.Configuration();
config.setMaxBytesLocalHeap(cacheMaxHeapSize + "M");
config.addDefaultCache(cacheConfiguration);
config.setUpdateCheck(false);
return net.sf.ehcache.CacheManager.newInstance(config);
}
@Override
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(pooledCacheManger());
}
@Override
@Bean
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
/** --- Elastic Search Configuration --- */
@Value("${elastic.search.cluster.name:elasticsearch}") private String elasticSearchCluster;
@Value("${elastic.search.host:localhost}") private String elasticSearchHost;
@Value("${elastic.search.port:9300}") private int elasticSearchPort;
@Bean(destroyMethod = "close")
public Client elasticSearchNode() {
logger.info("Connecting to elastic search cluster {}", elasticSearchCluster);
Settings settings = Settings.settingsBuilder()
.put("cluster.name", elasticSearchCluster).build();
try {
TransportClient tc = TransportClient.builder().settings(settings).build().addTransportAddress(
new InetSocketTransportAddress(new InetSocketAddress(elasticSearchHost, elasticSearchPort)));
if (tc.connectedNodes().size() == 0) {
tc.close();
throw new ElasticsearchException("Failed to connect to elastic search node!");
}
return tc;
}
catch (ElasticsearchException ex) {
logger.error("Error while initializing elasticsearch client:\n" + ExceptionUtils.getStackTrace(ex));
logger.error("Elastic search cluster {} at host: {}:{} needs to be running prior to deployment!",
elasticSearchCluster, elasticSearchHost, elasticSearchPort);
logger.error(AsciiArt.START_ELASTIC_SEARCH.getText());
return null;
}
}
/** --- Guava Event Bus Configuration --- */
@Bean
public EventBus eventBus() {
SubscriberExceptionHandler errorHandler = (exception, context) -> {
logger.error("Event Bus Exception thrown during event handling within {}: {}, {}", context.getSubscriberMethod(),
exception, ExceptionUtils.getStackTrace(exception));
};
return new EventBus(errorHandler);
}
@Bean
public AsyncEventBus asyncEventBus() {
SubscriberExceptionHandler errorHandler = (exception, context) -> {
logger.error("Async Event Bus Exception thrown during event handling within {}: {}, {}",
context.getSubscriberMethod(), exception, ExceptionUtils.getStackTrace(exception));
};
ExecutorService executor = Executors.newCachedThreadPool(new OpenlegThreadFactory("async-eventbus"));
return new AsyncEventBus(executor, errorHandler);
}
/** --- Object Mapper --- */
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new GuavaModule());
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
/** --- Processing Instances --- */
@Value("${sobi.batch.process.size:100}")
private int sobiBatchSize;
@Bean(name = "billIngestCache")
public IngestCache<BaseBillId, Bill, SobiFragment> billIngestCache() {
return new IngestCache<>(sobiBatchSize);
}
@Bean(name = "agendaIngestCache")
public IngestCache<AgendaId, Agenda, SobiFragment> agendaIngestCache() {
return new IngestCache<>(100);
}
@Bean(name = "calendarIngestCache")
public IngestCache<CalendarId, Calendar, SobiFragment> calendarIngestCache() {
return new IngestCache<>(100);
}
}