diff --git a/Service/src/main/java/org/gusdb/wdk/service/init/ServiceCacheInitializer.java b/Service/src/main/java/org/gusdb/wdk/service/init/ServiceCacheInitializer.java new file mode 100644 index 000000000..1a4b4b3a6 --- /dev/null +++ b/Service/src/main/java/org/gusdb/wdk/service/init/ServiceCacheInitializer.java @@ -0,0 +1,46 @@ +package org.gusdb.wdk.service.init; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.apache.log4j.Logger; +import org.gusdb.wdk.controller.ContextLookup; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.service.service.RecordService; + +/** + * Servlet context listener that initializes service-layer caches after the WdkModel has been created. + * This must run after ApplicationInitListener has set up the model. + * + * To use this listener, add it to web.xml AFTER ApplicationInitListener: + */ +public class ServiceCacheInitializer implements ServletContextListener { + + private static final Logger LOG = Logger.getLogger(ServiceCacheInitializer.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + LOG.info("Initializing service caches..."); + + WdkModel wdkModel = ContextLookup.getWdkModel(sce.getServletContext()); + + if (wdkModel == null) { + throw new RuntimeException("WdkModel not found in context. Ensure ApplicationInitListener runs before ServiceCacheInitializer."); + } + + // Generate expanded record classes cache for /record-types?format=expanded endpoint + LOG.info("Generating expanded record classes cache..."); + RecordService.generateExpandedRecordClassesCache(wdkModel); + LOG.info("Service cache initialization complete."); + + } catch (Exception e) { + throw new RuntimeException("Unable to initialize WDK service caches.", e); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // Nothing to clean up + } +} diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java b/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java index 3fa19fd1e..094db7c86 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java @@ -2,8 +2,15 @@ import static org.gusdb.fgputil.FormatUtil.NL; +import java.io.BufferedWriter; import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -12,7 +19,6 @@ import javax.ws.rs.GET; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; -import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; @@ -48,11 +54,12 @@ public class RecordService extends AbstractWdkService { public static final String RECORD_TYPE_PARAM_SEGMENT = "{" + RECORD_TYPE_PATH_PARAM + "}"; public static final String NAMED_RECORD_TYPE_SEGMENT_PAIR = RECORD_TYPES_PATH + "/" + RECORD_TYPE_PARAM_SEGMENT; - @SuppressWarnings("unused") private static final Logger LOG = Logger.getLogger(RecordService.class); private static final String RECORDCLASS_RESOURCE = "RecordClass with name "; + private static final String EXPANDED_RECORD_CLASSES_CACHE_FILE = "expanded-record-classes.json"; + private static final Counter TABLE_REQUEST_COUNTER = Counter.build() .name("wdk_table_requests") .help("Times individual tables are requested at the /records endpoint") @@ -75,8 +82,55 @@ public Response getRecordClassList(@QueryParam("format") String format) { } protected InputStream getExpandedRecordClassesJsonStream(WdkModel wdkModel) { - JSONArray allRecordClassesJson = RecordClassFormatter.getExpandedRecordClassesJson(wdkModel.getAllRecordClasses(), wdkModel.getRecordClassQuestionMap()); - return new ByteArrayInputStream(allRecordClassesJson.toString().getBytes()); + try { + Path cacheFile = getExpandedRecordClassesCacheFile(wdkModel); + + if (Files.exists(cacheFile)) { + LOG.debug("Serving expanded record classes from cache file: " + cacheFile); + return new FileInputStream(cacheFile.toFile()); + } else { + LOG.warn("Cache file does not exist at: " + cacheFile + ". Falling back to in-memory generation."); + // Fallback to in-memory generation if cache doesn't exist + JSONArray allRecordClassesJson = RecordClassFormatter.getExpandedRecordClassesJson( + wdkModel.getAllRecordClasses(), wdkModel.getRecordClassQuestionMap()); + return new ByteArrayInputStream(allRecordClassesJson.toString().getBytes()); + } + } catch (IOException e) { + LOG.error("Failed to read cache file, falling back to in-memory generation", e); + // Fallback to in-memory generation if cache read fails + JSONArray allRecordClassesJson = RecordClassFormatter.getExpandedRecordClassesJson( + wdkModel.getAllRecordClasses(), wdkModel.getRecordClassQuestionMap()); + return new ByteArrayInputStream(allRecordClassesJson.toString().getBytes()); + } + } + + /** + * Gets the path to the expanded record classes cache file. + */ + public static Path getExpandedRecordClassesCacheFile(WdkModel wdkModel) { + Path wdkTempDir = wdkModel.getModelConfig().getWdkTempDir(); + return Paths.get(wdkTempDir.toString(), EXPANDED_RECORD_CLASSES_CACHE_FILE); + } + + /** + * Generates the expanded record classes cache file. + * This should be called during application initialization. + */ + public static void generateExpandedRecordClassesCache(WdkModel wdkModel) throws IOException { + Path cacheFile = getExpandedRecordClassesCacheFile(wdkModel); + + LOG.info("Generating expanded record classes cache file at: " + cacheFile); + + // Generate JSON + JSONArray allRecordClassesJson = RecordClassFormatter.getExpandedRecordClassesJson( + wdkModel.getAllRecordClasses(), wdkModel.getRecordClassQuestionMap()); + + // Write to file + try (BufferedWriter writer = new BufferedWriter(new FileWriter(cacheFile.toFile()))) { + writer.write(allRecordClassesJson.toString()); + } + + LOG.info("Cache file generation complete. File size: " + Files.size(cacheFile) + " bytes"); } @GET