diff --git a/Integrations/build.gradle b/Integrations/build.gradle index ec27eb8e7c7..41d99e895d3 100644 --- a/Integrations/build.gradle +++ b/Integrations/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':log-factory') testImplementation project(':engine-test-utils') + testImplementation TestTools.projectDependency(project, 'engine-time') testRuntimeOnly project(':log-to-slf4j') // add configs, and some runtime dependencies to test classpaths diff --git a/Plot/Plot.gradle b/Plot/Plot.gradle index 51bbc7d2710..1d6a6068bfa 100644 --- a/Plot/Plot.gradle +++ b/Plot/Plot.gradle @@ -27,6 +27,7 @@ dependencies { testImplementation TestTools.projectDependency(project, 'Util') testImplementation TestTools.projectDependency(project, 'engine-table') + testImplementation TestTools.projectDependency(project, 'engine-time') testRuntimeOnly project(path: ':configs') testRuntimeOnly project(path: ':test-configs') diff --git a/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransformBusinessCalendar.java b/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransformBusinessCalendar.java index 5251e6eac42..f782d0e2c00 100644 --- a/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransformBusinessCalendar.java +++ b/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransformBusinessCalendar.java @@ -3,16 +3,17 @@ */ package io.deephaven.plot.axistransformations; -import io.deephaven.base.testing.BaseArrayTestCase; import io.deephaven.time.DateTimeUtils; +import io.deephaven.time.calendar.CalendarInit; import io.deephaven.time.calendar.Calendars; +import junit.framework.TestCase; import java.nio.file.Paths; import java.time.Instant; import java.time.ZoneId; import java.util.Objects; -public class TestAxisTransformBusinessCalendar extends BaseArrayTestCase { +public class TestAxisTransformBusinessCalendar extends TestCase { private static final ZoneId TZ_JP = ZoneId.of("Asia/Tokyo"); @@ -41,6 +42,7 @@ public class TestAxisTransformBusinessCalendar extends BaseArrayTestCase { @Override public void setUp() throws Exception { + CalendarInit.init(); final String path = Paths .get(Objects.requireNonNull(TestAxisTransformBusinessCalendar.class.getResource("/JPOSE.calendar")) .toURI()) diff --git a/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransforms.java b/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransforms.java index aee90400503..c8c9c45126a 100644 --- a/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransforms.java +++ b/Plot/src/test/java/io/deephaven/plot/axistransformations/TestAxisTransforms.java @@ -4,7 +4,9 @@ package io.deephaven.plot.axistransformations; import io.deephaven.time.calendar.BusinessCalendar; +import io.deephaven.time.calendar.CalendarInit; import io.deephaven.time.calendar.Calendars; +import org.junit.Before; import org.junit.Test; import java.util.Arrays; @@ -28,6 +30,11 @@ public class TestAxisTransforms { final double d9 = -d4; private final double delta = 0.00001; + @Before + public void setUp() { + CalendarInit.init(); + } + @Test public void testLog() { final AxisTransform transform = AxisTransforms.LOG; diff --git a/engine/time/build.gradle b/engine/time/build.gradle index be02e75d3c5..6a854e5c222 100644 --- a/engine/time/build.gradle +++ b/engine/time/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation project(':Configuration') implementation project(':log-factory') implementation depJdom2 + Classpaths.inheritDagger(project) testImplementation TestTools.projectDependency(project, 'Base') diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendar.java b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendar.java index 539c2b1b681..c68543e832a 100644 --- a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendar.java +++ b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendar.java @@ -147,7 +147,7 @@ private YearData getYearData(final int year) { * is different from the schedule for a standard business day or weekend. * @throws RequirementFailure if any argument is null. */ - BusinessCalendar(final String name, final String description, final ZoneId timeZone, + public BusinessCalendar(final String name, final String description, final ZoneId timeZone, final LocalDate firstValidDate, final LocalDate lastValidDate, final CalendarDay standardBusinessDay, final Set weekendDays, final Map> holidays) { diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java index 14c3a133593..d62c4592d82 100644 --- a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java +++ b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java @@ -13,8 +13,10 @@ import org.jdom2.input.SAXBuilder; import org.jetbrains.annotations.NotNull; +import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.time.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -22,6 +24,7 @@ /** * A parser for reading business calendar XML files. * + *

* Business calendar XML files should be formatted as: * *

@@ -56,14 +59,14 @@
  *
  * In addition, legacy XML files are supported. These files have dates formatted as `yyyyMMdd` instead of ISO-8601
  * `yyy-MM-dd`. Additionally, legacy uses `businessPeriod` tags in place of the `businessTime` tags.
- * 
+ *
  * 
  * {@code
  * 
  * 09:3016:00
  * }
  * 
- * + * *
  * {@code
  * 
@@ -73,7 +76,7 @@
  *
  * The legacy format may be deprecated in a future release.
  */
-class BusinessCalendarXMLParser {
+public final class BusinessCalendarXMLParser {
 
     private static class BusinessCalendarInputs {
         private String calendarName;
@@ -108,42 +111,82 @@ public static BusinessCalendar loadBusinessCalendar(@NotNull final String file)
     public static BusinessCalendar loadBusinessCalendar(@NotNull final File file) {
         Require.neqNull(file, "file");
         final BusinessCalendarInputs in = parseBusinessCalendarInputs(file);
+        return new BusinessCalendar(in.calendarName, in.description,
+                in.timeZone, in.firstValidDate, in.lastValidDate,
+                in.standardBusinessDay, in.weekendDays, in.holidays);
+    }
 
+    /**
+     * Loads a business calendar from an XML input stream.
+     *
+     * @param inputStream XML input stream
+     * @return business calendar.
+     * @throws RequirementFailure if the input is null
+     */
+    public static BusinessCalendar loadBusinessCalendar(@NotNull final InputStream inputStream) {
+        Require.neqNull(inputStream, "inputStream");
+        final BusinessCalendarInputs in = parseBusinessCalendarInputs(inputStream);
         return new BusinessCalendar(in.calendarName, in.description,
                 in.timeZone, in.firstValidDate, in.lastValidDate,
                 in.standardBusinessDay, in.weekendDays, in.holidays);
     }
 
+    /**
+     * Loads a business calendar from an XML resource.
+     *
+     * @param resource XML input stream
+     * @return business calendar.
+     */
+    public static BusinessCalendar loadBusinessCalendarFromResource(String resource) throws IOException {
+        final InputStream in = Calendars.class.getResourceAsStream(resource);
+        if (in == null) {
+            throw new RuntimeException("Could not open resource " + resource + " from classpath");
+        }
+        try (final InputStream bin = new BufferedInputStream(in)) {
+            return loadBusinessCalendar(bin);
+        }
+    }
+
     private static BusinessCalendarInputs parseBusinessCalendarInputs(@NotNull final File file) {
         Require.neqNull(file, "file");
         try {
-            final BusinessCalendarInputs calendarElements = new BusinessCalendarInputs();
-
-            Element root = loadXMLRootElement(file);
-            calendarElements.calendarName = getText(getRequiredChild(root, "name"));
-            calendarElements.timeZone = TimeZoneAliases.zoneId(getText(getRequiredChild(root, "timeZone")));
-            calendarElements.description = getText(root.getChild("description"));
-            calendarElements.holidays = parseHolidays(root, calendarElements.timeZone);
-            final String firstValidDateStr = getText(root.getChild("firstValidDate"));
-            calendarElements.firstValidDate =
-                    firstValidDateStr == null ? Collections.min(calendarElements.holidays.keySet())
-                            : DateTimeUtils.parseLocalDate(firstValidDateStr);
-            final String lastValidDateStr = getText(root.getChild("lastValidDate"));
-            calendarElements.lastValidDate =
-                    lastValidDateStr == null ? Collections.max(calendarElements.holidays.keySet())
-                            : DateTimeUtils.parseLocalDate(lastValidDateStr);
-
-            // Set the default values
-            final Element defaultElement = getRequiredChild(root, "default");
-            calendarElements.weekendDays = parseWeekendDays(defaultElement);
-            calendarElements.standardBusinessDay = parseCalendarDaySchedule(defaultElement);
-
-            return calendarElements;
+            return fill(loadXMLRootElement(file));
         } catch (Exception e) {
             throw new RuntimeException("Unable to load calendar file: file=" + file.getPath(), e);
         }
     }
 
+    private static BusinessCalendarInputs parseBusinessCalendarInputs(@NotNull final InputStream in) {
+        Require.neqNull(in, "in");
+        try {
+            return fill(loadXMLRootElement(in));
+        } catch (Exception e) {
+            throw new RuntimeException("Unable to load calendar file: inputStream=" + in, e);
+        }
+    }
+
+    private static BusinessCalendarInputs fill(Element root) throws Exception {
+        final BusinessCalendarInputs calendarElements = new BusinessCalendarInputs();
+        calendarElements.calendarName = getText(getRequiredChild(root, "name"));
+        calendarElements.timeZone = TimeZoneAliases.zoneId(getText(getRequiredChild(root, "timeZone")));
+        calendarElements.description = getText(root.getChild("description"));
+        calendarElements.holidays = parseHolidays(root, calendarElements.timeZone);
+        final String firstValidDateStr = getText(root.getChild("firstValidDate"));
+        calendarElements.firstValidDate =
+                firstValidDateStr == null ? Collections.min(calendarElements.holidays.keySet())
+                        : DateTimeUtils.parseLocalDate(firstValidDateStr);
+        final String lastValidDateStr = getText(root.getChild("lastValidDate"));
+        calendarElements.lastValidDate =
+                lastValidDateStr == null ? Collections.max(calendarElements.holidays.keySet())
+                        : DateTimeUtils.parseLocalDate(lastValidDateStr);
+
+        // Set the default values
+        final Element defaultElement = getRequiredChild(root, "default");
+        calendarElements.weekendDays = parseWeekendDays(defaultElement);
+        calendarElements.standardBusinessDay = parseCalendarDaySchedule(defaultElement);
+        return calendarElements;
+    }
+
     private static Element loadXMLRootElement(File calendarFile) throws Exception {
         final Document doc;
 
@@ -159,6 +202,21 @@ private static Element loadXMLRootElement(File calendarFile) throws Exception {
         return doc.getRootElement();
     }
 
+    private static Element loadXMLRootElement(InputStream in) throws Exception {
+        final Document doc;
+
+        try {
+            final SAXBuilder builder = new SAXBuilder();
+            doc = builder.build(in);
+        } catch (JDOMException e) {
+            throw new Exception("Error parsing business calendar: inputStream=" + in, e);
+        } catch (IOException e) {
+            throw new Exception("Error loading business calendar: inputStream=" + in, e);
+        }
+
+        return doc.getRootElement();
+    }
+
     private static Element getRequiredChild(@NotNull final Element root, final String child) throws Exception {
         Element element = root.getChild(child);
         if (element != null) {
diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/Calendars.java b/engine/time/src/main/java/io/deephaven/time/calendar/Calendars.java
index 50b640cc4cc..c0743d01a35 100644
--- a/engine/time/src/main/java/io/deephaven/time/calendar/Calendars.java
+++ b/engine/time/src/main/java/io/deephaven/time/calendar/Calendars.java
@@ -12,10 +12,11 @@
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
-import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
-import java.util.function.Consumer;
 
 /**
  * A collection of business calendars.
@@ -26,84 +27,49 @@ public class Calendars {
     private static final String BUSINESS_CALENDAR_PROP_INTERNAL = "Calendar.importPath";
     private static final String BUSINESS_CALENDAR_PROP_USER = "Calendar.userImportPath";
     private static String defaultName = Configuration.getInstance().getProperty("Calendar.default");
+    private static final Map calMap = new TreeMap<>();
 
-    // Variable should only be accessed through getMap()
-    private static volatile Map calMap;
-
-    private Calendars() {}
-
-    // region Load
-
-    // Get the initialized map of calendars.
-    // Initilization is deferred to so that errors are easier for users to find.
-    private static Map getMap() {
-        if (calMap != null) {
-            return calMap;
-        }
-
-        synchronized (Calendars.class) {
-            if (calMap != null) {
-                return calMap;
-            }
-
-            calMap = new TreeMap<>();
-            final Configuration configuration = Configuration.getInstance();
-
-            loadProperty(configuration, BUSINESS_CALENDAR_PROP_INTERNAL);
-
-            if (configuration.hasProperty(BUSINESS_CALENDAR_PROP_USER)) {
-                loadProperty(configuration, BUSINESS_CALENDAR_PROP_USER);
-            }
-
-            return calMap;
+    /**
+     * Loads the line-separated calendar XML resources from the resource file configuration value
+     * {@value BUSINESS_CALENDAR_PROP_INTERNAL}. If the resource file configuration value
+     * {@value BUSINESS_CALENDAR_PROP_USER} exists, those line-separated calendar XML resources will be returned as
+     * well.
+     *
+     * @return the calendars
+     * @see BusinessCalendarXMLParser#loadBusinessCalendarFromResource(String)
+     */
+    public static List calendarsFromConfiguration() {
+        final Configuration configuration = Configuration.getInstance();
+        final List configurationCalendars = new ArrayList<>(
+                loadCalendarsFromResourceList(configuration.getProperty(BUSINESS_CALENDAR_PROP_INTERNAL)));
+        if (configuration.hasProperty(BUSINESS_CALENDAR_PROP_USER)) {
+            configurationCalendars.addAll(
+                    loadCalendarsFromResourceList(configuration.getProperty(BUSINESS_CALENDAR_PROP_USER)));
         }
+        return configurationCalendars;
     }
 
-    private static void loadProperty(final Configuration configuration, final String property) {
-        final String location = configuration.getProperty(property);
-        try {
-            load(location);
-        } catch (Exception e) {
-            logger.warn().append("Problem loading calendars. property=").append(property)
-                    .append(" importPath=").append(location).append(e).endl();
-            throw new RuntimeException("Problem loading calendars. property=" + property +
-                    " importPath=" + location, e);
-        }
-    }
+    private Calendars() {}
 
-    private static void load(final String businessCalendarConfig) throws NoSuchFileException {
-        final InputStream configToLoad = Calendars.class.getResourceAsStream(businessCalendarConfig);
+    // region Load
 
-        if (configToLoad == null) {
-            logger.warn("Could not find " + businessCalendarConfig + " on classpath");
-            throw new RuntimeException("Could not open " + businessCalendarConfig + " from classpath");
+    private static List loadCalendarsFromResourceList(String resource) {
+        final InputStream in = Calendars.class.getResourceAsStream(resource);
+        if (in == null) {
+            logger.warn("Could not find resource " + resource + " on classpath");
+            throw new RuntimeException("Could not open resource " + resource + " from classpath");
         }
-
-        final Consumer consumer = (filePath) -> {
-            try {
-                final InputStream inputStream = Calendars.class.getResourceAsStream(filePath);
-                if (inputStream != null) {
-                    final File calendarFile = inputStreamToFile(inputStream);
-                    final BusinessCalendar businessCalendar =
-                            BusinessCalendarXMLParser.loadBusinessCalendar(calendarFile);
-                    addCalendar(businessCalendar);
-                    // noinspection ResultOfMethodCallIgnored
-                    calendarFile.delete();
-                } else {
-                    logger.warn("Could not open " + filePath + " from classpath");
-                    throw new RuntimeException("Could not open " + filePath + " from classpath");
-                }
-            } catch (Exception e) {
-                logger.warn("Problem loading calendar: location=" + businessCalendarConfig, e);
-                throw new RuntimeException("Problem loading calendar: location=" + businessCalendarConfig, e);
+        final List calendars = new ArrayList<>();
+        try (final BufferedReader config = new BufferedReader(new InputStreamReader(in))) {
+            final Iterator it = config.lines().iterator();
+            while (it.hasNext()) {
+                final String calendarResource = it.next();
+                calendars.add(BusinessCalendarXMLParser.loadBusinessCalendarFromResource(calendarResource));
             }
-        };
-
-        try (final BufferedReader config = new BufferedReader(new InputStreamReader(configToLoad))) {
-            config.lines().forEach(consumer);
+            return calendars;
         } catch (Exception e) {
-            logger.warn("Problem loading calendar: location=" + businessCalendarConfig, e);
-            throw new RuntimeException("Problem loading calendar: location=" + businessCalendarConfig, e);
+            logger.warn("Problem loading calendar: location=" + resource, e);
+            throw new RuntimeException("Problem loading calendar: location=" + resource, e);
         }
     }
 
@@ -115,7 +81,7 @@ private static void load(final String businessCalendarConfig) throws NoSuchFileE
      */
     public synchronized static void removeCalendar(final String name) {
         Require.neqNull(name, "name");
-        getMap().remove(name);
+        calMap.remove(name);
     }
 
     /**
@@ -131,8 +97,7 @@ public synchronized static void addCalendar(final BusinessCalendar cal) {
         if (!NameValidator.isValidQueryParameterName(name)) {
             throw new IllegalArgumentException("Invalid name for calendar: name='" + name + "'");
         }
-
-        final Map map = getMap();
+        final Map map = calMap;
 
         if (map.containsKey(name)) {
             final Calendar oldCalendar = map.get(name);
@@ -219,7 +184,7 @@ public synchronized static BusinessCalendar calendar(final String name) {
         }
 
         final String n = name.toUpperCase();
-        final Map map = getMap();
+        final Map map = calMap;
 
         if (!map.containsKey(n)) {
             throw new IllegalArgumentException("No such calendar: " + name);
@@ -258,7 +223,7 @@ public synchronized static String calendarName() {
      * @return names of all available calendars
      */
     public synchronized static String[] calendarNames() {
-        return getMap().keySet().toArray(String[]::new);
+        return calMap.keySet().toArray(String[]::new);
     }
 
     // endregion
diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/CalendarsFromConfigurationModule.java b/engine/time/src/main/java/io/deephaven/time/calendar/CalendarsFromConfigurationModule.java
new file mode 100644
index 00000000000..213965056db
--- /dev/null
+++ b/engine/time/src/main/java/io/deephaven/time/calendar/CalendarsFromConfigurationModule.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.time.calendar;
+
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.ElementsIntoSet;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Provides the {@link BusinessCalendar business calendars} from {@link Calendars#calendarsFromConfiguration()}.
+ */
+@Module
+public interface CalendarsFromConfigurationModule {
+
+    @Provides
+    @ElementsIntoSet
+    static Set providesCalendarsFromConfiguration() {
+        return new HashSet<>(Calendars.calendarsFromConfiguration());
+    }
+}
diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/CalendarInit.java b/engine/time/src/test/java/io/deephaven/time/calendar/CalendarInit.java
new file mode 100644
index 00000000000..3fb0cc9c3a1
--- /dev/null
+++ b/engine/time/src/test/java/io/deephaven/time/calendar/CalendarInit.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.time.calendar;
+
+public final class CalendarInit {
+
+    private static volatile boolean initialized = false;
+
+    /**
+     * This is a guarded initialization of {@link Calendars#addCalendar(BusinessCalendar)} for all the
+     * {@link Calendars#calendarsFromConfiguration()}.
+     */
+    public static void init() {
+        if (!initialized) {
+            synchronized (CalendarInit.class) {
+                if (!initialized) {
+                    for (BusinessCalendar calendar : Calendars.calendarsFromConfiguration()) {
+                        Calendars.addCalendar(calendar);
+                    }
+                    initialized = true;
+                }
+            }
+        }
+    }
+}
diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendars.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendars.java
index 75549000ef0..b35fd8c124e 100644
--- a/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendars.java
+++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendars.java
@@ -12,6 +12,12 @@
 
 public class TestCalendars extends BaseArrayTestCase {
 
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        CalendarInit.init();
+    }
+
     public void testDefault() {
         final BusinessCalendar calendar = Calendars.calendar();
         assertEquals(Configuration.getInstance().getProperty("Calendar.default"), Calendars.calendarName());
diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestStaticCalendarMethods.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestStaticCalendarMethods.java
index 00a4c644194..f013bbe62f2 100644
--- a/engine/time/src/test/java/io/deephaven/time/calendar/TestStaticCalendarMethods.java
+++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestStaticCalendarMethods.java
@@ -21,7 +21,6 @@
  * See also {@code StaticCalendarMethodsGenerator}.
  */
 public class TestStaticCalendarMethods extends BaseArrayTestCase {
-
     private final Map, Object[]> data = new HashMap<>();
 
     {
@@ -93,6 +92,12 @@ private void executeTest(final Method m1, final Method m2)
         }
     }
 
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        CalendarInit.init();
+    }
+
     // test to make sure these methods work inside the query strings
     public void testAll() {
         final Set excludes = new HashSet<>();
diff --git a/py/embedded-server/java-runtime/src/main/java/io/deephaven/python/server/EmbeddedServer.java b/py/embedded-server/java-runtime/src/main/java/io/deephaven/python/server/EmbeddedServer.java
index 467ecdb15ba..c81f408a88d 100644
--- a/py/embedded-server/java-runtime/src/main/java/io/deephaven/python/server/EmbeddedServer.java
+++ b/py/embedded-server/java-runtime/src/main/java/io/deephaven/python/server/EmbeddedServer.java
@@ -13,6 +13,7 @@
 import io.deephaven.io.logger.LogBuffer;
 import io.deephaven.io.logger.LogBufferOutputStream;
 import io.deephaven.server.auth.CommunityAuthorizationModule;
+import io.deephaven.time.calendar.CalendarsFromConfigurationModule;
 import io.deephaven.server.console.ExecutionContextModule;
 import io.deephaven.server.console.groovy.GroovyConsoleSessionModule;
 import io.deephaven.server.console.python.PythonConsoleSessionModule;
@@ -56,6 +57,7 @@ public class EmbeddedServer {
             CommunityAuthorizationModule.class,
             ClientDefaultsModule.class,
             ObfuscatingErrorTransformerModule.class,
+            CalendarsFromConfigurationModule.class,
     })
     public interface PythonServerComponent extends JettyServerComponent {
         @Component.Builder
diff --git a/py/server/tests/test_calendar.py b/py/server/tests/test_calendar.py
index 00eec21dd2c..a2a4813c4f3 100644
--- a/py/server/tests/test_calendar.py
+++ b/py/server/tests/test_calendar.py
@@ -15,6 +15,11 @@
 
 
 class CalendarTestCase(BaseTestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        super().setUpClass()
+        jpy.get_type('io.deephaven.time.calendar.CalendarInit').init()
+
     def setUp(self) -> None:
         super().setUp()
 
diff --git a/server/src/main/java/io/deephaven/server/runner/CommunityDefaultsModule.java b/server/src/main/java/io/deephaven/server/runner/CommunityDefaultsModule.java
index f65c829f0b6..9e18fe625e9 100644
--- a/server/src/main/java/io/deephaven/server/runner/CommunityDefaultsModule.java
+++ b/server/src/main/java/io/deephaven/server/runner/CommunityDefaultsModule.java
@@ -5,6 +5,7 @@
 
 import dagger.Module;
 import io.deephaven.client.ClientDefaultsModule;
+import io.deephaven.time.calendar.CalendarsFromConfigurationModule;
 import io.deephaven.server.console.ExecutionContextModule;
 import io.deephaven.server.console.groovy.GroovyConsoleSessionModule;
 import io.deephaven.server.console.python.PythonConsoleSessionModule;
@@ -36,6 +37,7 @@
  * @see ExecutionContextModule
  * @see ClientDefaultsModule
  * @see ObfuscatingErrorTransformerModule
+ * @see CalendarsFromConfigurationModule
  */
 @Module(includes = {
         DeephavenApiServerModule.class,
@@ -49,6 +51,7 @@
         ExecutionContextModule.class,
         ClientDefaultsModule.class,
         ObfuscatingErrorTransformerModule.class,
+        CalendarsFromConfigurationModule.class,
 })
 public interface CommunityDefaultsModule {
 }
diff --git a/server/src/main/java/io/deephaven/server/runner/DeephavenApiServer.java b/server/src/main/java/io/deephaven/server/runner/DeephavenApiServer.java
index 54eea4795e8..18211618049 100644
--- a/server/src/main/java/io/deephaven/server/runner/DeephavenApiServer.java
+++ b/server/src/main/java/io/deephaven/server/runner/DeephavenApiServer.java
@@ -23,6 +23,8 @@
 import io.deephaven.server.plugin.PluginRegistration;
 import io.deephaven.server.session.SessionService;
 import io.deephaven.server.util.Scheduler;
+import io.deephaven.time.calendar.BusinessCalendar;
+import io.deephaven.time.calendar.Calendars;
 import io.deephaven.uri.resolver.UriResolver;
 import io.deephaven.uri.resolver.UriResolvers;
 import io.deephaven.uri.resolver.UriResolversInstance;
@@ -36,6 +38,7 @@
 import javax.inject.Provider;
 import java.io.IOException;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
@@ -53,6 +56,7 @@ public class DeephavenApiServer {
     private final GrpcServer server;
     private final UpdateGraph ug;
     private final LogInit logInit;
+    private final Provider> calendars;
     private final Scheduler scheduler;
     private final Provider scriptSessionProvider;
     private final PluginRegistration pluginRegistration;
@@ -68,6 +72,7 @@ public DeephavenApiServer(
             final GrpcServer server,
             @Named(PeriodicUpdateGraph.DEFAULT_UPDATE_GRAPH_NAME) final UpdateGraph ug,
             final LogInit logInit,
+            final Provider> calendars,
             final Scheduler scheduler,
             final Provider scriptSessionProvider,
             final PluginRegistration pluginRegistration,
@@ -80,6 +85,7 @@ public DeephavenApiServer(
         this.server = server;
         this.ug = ug;
         this.logInit = logInit;
+        this.calendars = calendars;
         this.scheduler = scheduler;
         this.scriptSessionProvider = scriptSessionProvider;
         this.pluginRegistration = pluginRegistration;
@@ -135,6 +141,10 @@ public DeephavenApiServer run() throws IOException, ClassNotFoundException, Time
         log.info().append("Creating/Clearing Script Cache...").endl();
         AbstractScriptSession.createScriptCache();
 
+        for (BusinessCalendar calendar : calendars.get()) {
+            Calendars.addCalendar(calendar);
+        }
+
         log.info().append("Initializing Script Session...").endl();
         checkScopeChanges(scriptSessionProvider.get());
         pluginRegistration.registerAll();
diff --git a/server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java b/server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java
index a064a1ef415..d3c683ce331 100644
--- a/server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java
+++ b/server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java
@@ -18,6 +18,7 @@
 import io.deephaven.proto.DeephavenChannelImpl;
 import io.deephaven.server.auth.AuthorizationProvider;
 import io.deephaven.server.auth.CommunityAuthorizationProvider;
+import io.deephaven.time.calendar.CalendarsFromConfigurationModule;
 import io.deephaven.server.config.ServerConfig;
 import io.deephaven.server.console.NoConsoleSessionModule;
 import io.deephaven.server.log.LogModule;
@@ -58,7 +59,8 @@ public abstract class DeephavenApiServerTestBase {
             ClientDefaultsModule.class,
             ObfuscatingErrorTransformerModule.class,
             JsPluginNoopConsumerModule.class,
-            SchedulerDelegatingImplModule.class
+            SchedulerDelegatingImplModule.class,
+            CalendarsFromConfigurationModule.class
     })
     public interface TestComponent {