diff --git a/dotCMS/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java b/dotCMS/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java index 3cc83fa2e292..28419b49bd0a 100644 --- a/dotCMS/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java +++ b/dotCMS/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java @@ -20,6 +20,7 @@ */ import com.dotcms.rendering.velocity.services.DotResourceLoader; +import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import java.io.StringReader; import java.util.ArrayList; @@ -172,6 +173,15 @@ else if (libfiles instanceof String) macroLibVec.add(libfiles); } + // Feature flag: when true, engine init throws VelocityException if any + // configured velocimacro.library file fails to load. Default is false to + // preserve historical behavior; operators opt in once they've verified + // their library files load cleanly (see issue #35601). + final boolean failOnMissing = Config.getBooleanProperty( + "VELOCITY_LIBRARY_FAIL_ON_MISSING", false); + final List loadedLibraries = new ArrayList<>(); + final List failedLibraries = new ArrayList<>(); + for(int i = 0, is = macroLibVec.size(); i < is; i++) { String lib = (String) macroLibVec.get(i); @@ -205,9 +215,16 @@ else if (libfiles instanceof String) twonk.template = template; twonk.modificationTime = template.getLastModified(); libModMap.put(lib, twonk); + loadedLibraries.add(lib); } catch(ResourceNotFoundException rnse){ - Logger.warn(this.getClass(),rnse.getMessage()); + // Surfaces the failed library by name so operators can grep startup logs. + // See dotCMS issue #35601 — previously this was a generic WARN that + // produced silent macro-rendering failures after pod restarts. + Logger.error(this, + "Velocimacro : VM library not found : " + lib + + " : " + rnse.getMessage()); + failedLibraries.add(lib); } catch (Exception e) { @@ -221,6 +238,24 @@ else if (libfiles instanceof String) vmManager.setRegisterFromLib(false); } } + + Logger.info(this, + "Velocimacro libraries loaded: " + loadedLibraries + + " ; failed: " + failedLibraries); + + if (failOnMissing && !failedLibraries.isEmpty()) + { + // Terminal: this throw short-circuits the remainder of initVelocimacro() + // (permission setup, namespace usage, autoreload). The factory ends up in + // a partial state and MUST be discarded — do not catch this and reuse the + // instance. The documented call path is VelocityUtil.getEngine() → + // DotRuntimeException → InitServlet aborts → pod never becomes ready. + throw new VelocityException( + "Velocimacro : required VM libraries failed to load: " + + failedLibraries + + ". Set VELOCITY_LIBRARY_FAIL_ON_MISSING=false (or unset)" + + " to allow startup with missing libraries."); + } } /* diff --git a/dotCMS/src/test/java/org/apache/velocity/runtime/VelocimacroFactoryTest.java b/dotCMS/src/test/java/org/apache/velocity/runtime/VelocimacroFactoryTest.java new file mode 100644 index 000000000000..76a7be3c4dff --- /dev/null +++ b/dotCMS/src/test/java/org/apache/velocity/runtime/VelocimacroFactoryTest.java @@ -0,0 +1,111 @@ +package org.apache.velocity.runtime; + +import com.dotmarketing.util.Config; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.exception.VelocityException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for the {@code VELOCITY_LIBRARY_FAIL_ON_MISSING} dotCMS feature flag added in + * issue #35601. + * + *

The dotCMS-patched {@link VelocimacroFactory} previously swallowed + * {@link ResourceNotFoundException} when loading any configured {@code velocimacro.library} + * file, allowing engine init to succeed while leaving global macros unregistered. After + * pod restarts with transient I/O errors (notably EFS-backed K8s volumes), + * {@code #renderMarks} and {@code #editContentlet} would render as literal text on every + * page until the JVM was restarted. + * + *

The flag defaults to {@code false} — engine init preserves the legacy silent-warn + * behavior so existing customers are not broken by this PR. Operators opt in with + * {@code VELOCITY_LIBRARY_FAIL_ON_MISSING=true} (env var or system property) once they + * have verified their library files load cleanly. + */ +public class VelocimacroFactoryTest { + + private static final String FLAG_KEY = "VELOCITY_LIBRARY_FAIL_ON_MISSING"; + private static final String MISSING_LIBRARY = "this-library-does-not-exist.vm"; + + private RuntimeServices rsvc; + private MockedStatic configMock; + + @Before + public void setUp() { + rsvc = mock(RuntimeServices.class); + when(rsvc.getProperty(RuntimeConstants.VM_LIBRARY)).thenReturn(MISSING_LIBRARY); + when(rsvc.getTemplate(MISSING_LIBRARY)) + .thenThrow(new ResourceNotFoundException( + "Cannot find resource '" + MISSING_LIBRARY + "'")); + // Permission flags read after the library loop — return the supplied default + // so init completes when the fail-on-missing flag is off. + when(rsvc.getBoolean(anyString(), anyBoolean())).thenAnswer(invocation -> + invocation.getArgument(1)); + configMock = mockStatic(Config.class); + // Default behavior: return the supplied default for any flag we did not stub. + configMock.when(() -> Config.getBooleanProperty(anyString(), anyBoolean())) + .thenAnswer(invocation -> invocation.getArgument(1)); + } + + @After + public void tearDown() { + configMock.close(); + } + + @Test + public void initThrowsWhenFlagIsTrue() { + configMock.when(() -> Config.getBooleanProperty(eq(FLAG_KEY), anyBoolean())) + .thenReturn(true); + + final VelocimacroFactory factory = new VelocimacroFactory(rsvc); + try { + factory.initVelocimacro(); + fail("Expected VelocityException when " + FLAG_KEY + + "=true and a velocimacro.library file fails to load"); + } catch (VelocityException expected) { + assertTrue( + "Exception message should name the failed library: " + expected.getMessage(), + expected.getMessage().contains(MISSING_LIBRARY)); + // Pin the actionable opt-out hint so a future refactor cannot silently + // drop it from the exception message. + assertTrue( + "Exception message should reference the opt-out flag: " + expected.getMessage(), + expected.getMessage().contains(FLAG_KEY)); + } + } + + @Test + public void initDoesNotThrowByDefault() { + // Default behavior — flag is off, legacy silent-warn path preserved. + // The flag default (false) flows through the configMock's default thenAnswer. + final VelocimacroFactory factory = new VelocimacroFactory(rsvc); + factory.initVelocimacro(); + + // Verify the library-loading branch actually ran — guards against future + // refactors that might short-circuit before the load loop is reached. + verify(rsvc).getTemplate(MISSING_LIBRARY); + } + + @Test + public void initDoesNotThrowWhenFlagIsExplicitlyFalse() { + configMock.when(() -> Config.getBooleanProperty(eq(FLAG_KEY), anyBoolean())) + .thenReturn(false); + + final VelocimacroFactory factory = new VelocimacroFactory(rsvc); + factory.initVelocimacro(); + + verify(rsvc).getTemplate(MISSING_LIBRARY); + } +}