package charts.reference; import java.awt.Dimension; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import play.Logger; import play.api.libs.MimeTypes; import charts.Chart; import charts.ChartType; import charts.Region; import charts.builder.DataSource; import charts.builder.DataSourceFactory; import charts.builder.DefaultChartBuilder; import charts.builder.spreadsheet.XlsDataSource; import charts.builder.spreadsheet.XlsxDataSource; import charts.representations.Format; import charts.representations.Representation; import com.google.common.collect.Maps; import com.google.inject.Singleton; @Singleton public class ChartRefCache { private static class CacheEntry { private final String resource; private final File svgChart; public CacheEntry(String resource, File svgChart) { super(); this.resource = resource; this.svgChart = svgChart; } public String resource() { return resource; } public File svgChart() { return svgChart; } } private static final String[] RESOURCES = new String[] { "coral.xls", "annual_rainfall.xlsx", "Chloro.xlsx", "cots_outbreak.xlsx", "groundcover_below_50.xlsx", "groundcover.xlsx", "loads.xlsx", "management_practice_systems.xlsx", "marine2.xlsx", "progress_table.xlsx", "riparian_2010.xlsx", "seagrass_cover2.xlsx", "tracking_towards_targets2.xlsx", "total_suspended_solids.xlsx", "wetlands.xlsx", "pesticides.xlsx", "marine_v2.xls" }; private final DefaultChartBuilder chartBuilder; private Map<ChartType, CacheEntry> cache = Maps.newHashMap(); private volatile boolean run; private volatile boolean initialized; private Thread t; public ChartRefCache() { this.chartBuilder = new DefaultChartBuilder(new DataSourceFactory() { @Override public DataSource getDataSource(String id) throws Exception { InputStream in = getResource(id); if(in != null) { if(StringUtils.endsWithIgnoreCase(id, "xlsx")) { return new XlsxDataSource(in); } else { return new XlsDataSource(in); } } else { return null; } }}); } public void start() { if(t != null) { return; } run = true; final Runnable r = new Runnable() { @Override public void run() { while(run) { if(needsRebuild()) { rebuildCache(); } if(run) { initialized = true; try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) {} } } clearCache(); } }; t = new Thread(r, "chart cache reference"); t.start(); } public void stop() { run = false; if(t!=null) { t.interrupt(); t = null; } } private InputStream getResource(String name) { final String resource = "/chartref/"+name; InputStream in = this.getClass().getResourceAsStream(resource); if(in == null) { Logger.warn("resource not found "+resource); } return in; } public CacheResult cached(final ChartType type) { final File f; final CacheEntry entry; synchronized(cache) { entry = cache.get(type); f = getSvgChart(entry); } if(entry == null || !f.exists()) { return null; } else { return new CacheResult() { @Override public Date created() { return new Date(f.lastModified()); } @Override public InputStream content() { try { return FileUtils.openInputStream(f); } catch (IOException e) { Logger.debug(String.format("while getting chart type %s" + " from chart reference cache",type.name()), e); return null; } } @Override public InputStream datasource() { return getResource(entry.resource()); } @Override public String datasourceMimetype() { return mimetype(entry.resource()); } @Override public String datasourceExtension() { return FilenameUtils.getExtension(entry.resource()); } }; } } public boolean initialized() { return initialized; } DefaultChartBuilder builder() { return chartBuilder; } private boolean needsRebuild() { synchronized(cache) { if(cache.isEmpty()) { return true; } for(CacheEntry entry : cache.values()) { final File f = getSvgChart(entry); if( (f!= null) && !f.exists()) { return true; } } } return false; } private void rebuildCache() { for(String r : RESOURCES) { try { List<Chart> charts = chartBuilder.getCharts(r, null, Collections.<Region>emptyList(), null); for(Chart c : charts) { if(!run) { return; } updateCache(r, c); } } catch (Exception e) { Logger.warn("while rebuilding chart reference cache", e); } } checkComplete(); } private void checkComplete() { for(ChartType type : ChartType.values()) { synchronized(cache) { final File f = getSvgChart(cache.get(type)); if(f == null) { Logger.warn(String.format("chart type %s missing in chart reference." + " please add a spreadsheet into resources/chartref" + " that yields this type of chart.", type.name())); } else if(!f.exists()) { Logger.debug(String.format("chart type %s exists in chart cache" + " but the file %s does not"), type.name(), f.getAbsolutePath()); } } } } private File getSvgChart(CacheEntry entry) { return entry != null? entry.svgChart() : null; } private void clearCache() { synchronized(cache) { for(CacheEntry entry : cache.values()) { final File f = getSvgChart(entry); FileUtils.deleteQuietly(f); } } } private void updateCache(final String resource, final Chart chart) { synchronized(cache) { final ChartType type = chart.getDescription().getType(); File f = getSvgChart(cache.get(type)); if(f == null || !f.exists()) { try { Representation r = chart.outputAs(Format.SVG, new Dimension()); f = File.createTempFile(type.name(), ".svg"); f.deleteOnExit(); FileUtils.writeByteArrayToFile(f, r.getContent()); cache.put(type, new CacheEntry(resource, f)); } catch(Exception e) { Logger.warn("while updating chart reference cache", e); } } } } private String mimetype(String filename) { final scala.Option<String> guessed = MimeTypes.forFileName(filename); return guessed.nonEmpty() ? guessed.get() : null; } }