fix(content): handle language-only locales without a country code#36405
fix(content): handle language-only locales without a country code#36405jcastro-dotcms wants to merge 5 commits into
Conversation
|
Claude finished @jcastro-dotcms's task in 1m 36s —— View job I'll analyze this and get back to you. |
🤖 dotBot Review (Bedrock)Reviewed 6 file(s); 5 candidate(s) → 4 confirmed, 0 uncertain (unverified, kept for review). Confirmed findings
us.deepseek.r1-v1:0 · Run: #28623805983 · tokens: in: 46474 · out: 9719 · total: 56193 · calls: 16 · est. ~$0.115 |
…Language Guava's ImmutableMap.Builder.put() throws NullPointerException on a null value. Language-only locales (no country code) legitimately have a null country/countryCode, which crashed any transform path that maps a contentlet's Language (new Edit Content save, History tab, GraphQL). Adds LanguageViewStrategyTest covering the null-country regression, the wrapAsMap=true path, the transform() entry point, and a non-regression check for locales that do have a country. Fixes #36106
Not related to the #36106 language locale fix in this PR. This addresses a separate, unrelated startup-time exception: VelocityUtil.getEngine() triggers a lazy Velocity engine init that touches the DB/company context, which on a fresh install can fire before Task00001LoadSchema has run, throwing a "No Company!" exception during boot. Deferring the probe until InitServlet sets dotcms.started.up=true avoids that noisy exception.
1054373 to
0156e31
Compare
| Mockito.when(languageAPI.getLanguage(4L)).thenReturn(language); | ||
|
|
||
| final APIProvider toolBox = Mockito.mock(APIProvider.class); | ||
| final Field languageApiField = APIProvider.class.getDeclaredField("languageAPI"); |
There was a problem hiding this comment.
🔴 [Critical] Reflection sets non-existent field on interface mock
The test uses reflection to set a 'languageAPI' field on the APIProvider interface mock (apiProviderMock). However, APIProvider is an interface which cannot have instance fields. This will throw NoSuchFieldException at runtime since the field doesn't exist. The correct approach would be to mock the getLanguageAPI() method instead of trying to inject via reflection.
VisitorAPIImpl.createVisitor() built a Locale via new Locale(language, country), which throws NullPointerException when country is null. Language-only locales (no country code) hit this on every page render, since VisitorAPIImpl.getVisitor() runs on every request. Language.asLocale() already null-guards this exact case (falls back to the 1-arg Locale constructor), so use it instead of constructing the Locale manually. Same underlying null-country data shape as the LanguageViewStrategy fix in this PR, different code path (page rendering / visitor creation vs. content transform).
PublishQueueElementTransformer.getMapForLanguage() built a java.util.Map.of(...) directly from language.getCountryCode(), which is null for a language-only locale (e.g. "es", no country). Map.of() throws NullPointerException on any null value, same failure mode as Guava's ImmutableMap.Builder fixed elsewhere in this PR. This crashed the Push Publishing queue view for any queued Language asset in a language-only locale. Defaults countryCode to blank before building the map, and adds PublishQueueElementTransformerTest covering the null-country regression and a non-regression check for locales with a country.
| Mockito.when(languageAPI.getLanguage(4L)).thenReturn(language); | ||
|
|
||
| final APIProvider toolBox = Mockito.mock(APIProvider.class); | ||
| final Field languageApiField = APIProvider.class.getDeclaredField("languageAPI"); |
There was a problem hiding this comment.
🔴 [Critical] Reflection sets non-existent 'languageAPI' field on APIProvider interface
The test uses reflection to set the 'languageAPI' field on the APIProvider interface, which doesn't have this field. The concrete APIImpl classes (like APILocator) contain the actual 'languageAPI' field. Using the interface type causes NoSuchFieldException when running tests.
Summary
ImmutableMap.Builder.put()throwsNullPointerException("null value in entry: country=null")when given a null value.LanguageViewStrategy.mapLanguage()passedLanguage.getCountry()/getCountryCode()straight through, which are legitimately null for language-only locales (e.g.es, no country) — crashing any transform that maps a contentlet's Language (new Edit Content save, History tab, GraphQLLanguageDataFetcher).country/countryCodeto an empty string viaCollectionsUtils.orElseGet(), matching the existing pattern already used inLanguage.toMap().LanguageViewStrategyTestcovering: the null-country regression (wrapAsMap=falseandwrapAsMap=true), thetransform()entry point, and a non-regression check for locales that do have a country.VisitorAPIImpl.createVisitor(), which built aLocalevianew Locale(language, country)— this throwsNullPointerExceptionwhencountryis null, so a page could never render at all in a language-only locale (this runs on every request, via visitor creation). Switched toLanguage.asLocale(), which already null-guards this exact case (falls back to the 1-argLocaleconstructor) and is already used elsewhere in the codebase (LanguageWebAPIImpl,JsLanguage). Same underlying null-country data shape as theLanguageViewStrategybug, different code path.Also included, unrelated to the fixes above: a change to
VelocityHealthCheckthat defers its readiness probe untildotcms.started.up=true.VelocityUtil.getEngine()triggers a lazy Velocity engine init that touches the DB/company context, which on a fresh install can fire beforeTask00001LoadSchemaruns, throwing a noisy"No Company!"exception during startup. This just quiets that one-time startup exception and is not part of the #36106 fix.Closes #36106
Test plan
LanguageViewStrategyTest(4 tests) — reproduces the exact reportedNullPointerException: null value in entry: country=nullon the unpatched code, passes with the fix./mvnw -pl :dotcms-core compile -DskipTests— compiles cleanVisitorAPIImplNPE (java.lang.NullPointerExceptionatLocale.<init>) by rendering a page in a language-only locale; confirmed fixed withLanguage.asLocale()es) and confirm no "Unknown Error" / 500🤖 Generated with Claude Code