Skip to content

fix(content): handle language-only locales without a country code#36405

Open
jcastro-dotcms wants to merge 5 commits into
mainfrom
issue-36106-language-only-locale-country-npe
Open

fix(content): handle language-only locales without a country code#36405
jcastro-dotcms wants to merge 5 commits into
mainfrom
issue-36106-language-only-locale-country-npe

Conversation

@jcastro-dotcms

@jcastro-dotcms jcastro-dotcms commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Guava's ImmutableMap.Builder.put() throws NullPointerException("null value in entry: country=null") when given a null value. LanguageViewStrategy.mapLanguage() passed Language.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, GraphQL LanguageDataFetcher).
  • Defaults country/countryCode to an empty string via CollectionsUtils.orElseGet(), matching the existing pattern already used in Language.toMap().
  • Adds LanguageViewStrategyTest covering: the null-country regression (wrapAsMap=false and wrapAsMap=true), the transform() entry point, and a non-regression check for locales that do have a country.
  • Also fixes VisitorAPIImpl.createVisitor(), which built a Locale via new Locale(language, country) — this throws NullPointerException when country is null, so a page could never render at all in a language-only locale (this runs on every request, via visitor creation). Switched to Language.asLocale(), which already null-guards this exact case (falls back to the 1-arg Locale constructor) and is already used elsewhere in the codebase (LanguageWebAPIImpl, JsLanguage). Same underlying null-country data shape as the LanguageViewStrategy bug, different code path.

Also included, unrelated to the fixes above: a change to VelocityHealthCheck that defers its readiness probe until dotcms.started.up=true. VelocityUtil.getEngine() triggers a lazy Velocity engine init that touches the DB/company context, which on a fresh install can fire before Task00001LoadSchema runs, 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

  • Added LanguageViewStrategyTest (4 tests) — reproduces the exact reported NullPointerException: null value in entry: country=null on the unpatched code, passes with the fix
  • ./mvnw -pl :dotcms-core compile -DskipTests — compiles clean
  • Manually reproduced the VisitorAPIImpl NPE (java.lang.NullPointerException at Locale.<init>) by rendering a page in a language-only locale; confirmed fixed with Language.asLocale()
  • Manually verify in the new Edit Content UI: create a new version for a language-only locale (e.g. es) and confirm no "Unknown Error" / 500
  • Manually verify rendering a page in a language-only locale no longer 500s

🤖 Generated with Claude Code

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @jcastro-dotcms's task in 1m 36s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🤖 dotBot Review (Bedrock)

Reviewed 6 file(s); 5 candidate(s) → 4 confirmed, 0 uncertain (unverified, kept for review).

Confirmed findings

  • 🔴 Critical dotCMS/src/test/java/com/dotmarketing/portlets/contentlet/transform/strategy/LanguageViewStrategyTest.java:93 — 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.
  • 🟡 Medium dotCMS/src/main/java/com/dotcms/health/checks/cdi/VelocityHealthCheck.java:84 — Using System.getProperty instead of Config.getStringProperty for dotcms.started.up
    The code checks 'dotcms.started.up' via System.getProperty(), violating dotCMS conventions that require Config.getStringProperty() for configuration values. While other existing code uses System.getProperty for this specific property, the PR introduces a new instance of this pattern. Config handles both system properties and dotCMS configuration files, ensuring consistent retrieval.
  • 🟡 Medium dotCMS/src/test/java/com/dotmarketing/portlets/contentlet/transform/strategy/LanguageViewStrategyTest.java:60 — Missing isoCode assertion in LanguageViewStrategyTest
    The test 'testMapLanguage_WithNullCountry_WhenWrapAsMap' in LanguageViewStrategyTest.java checks for country and countryCode fields but does not assert the presence/validity of the isoCode field when wrapAsMap=true. The Language.toMap() method includes isoCode, so the test should validate this key exists and matches the language's ISO code to ensure full correctness of the transformed map structure.
  • 🟡 Medium dotCMS/src/test/java/com/dotmarketing/portlets/contentlet/transform/strategy/LanguageViewStrategyTest.java:109 — Missing isoCode assertion in transform() test case
    The test method testTransform in LanguageViewStrategyTest.java verifies several fields of the transformed Language view but does not assert the isoCode value. This leaves a gap in test coverage for the ISO code generation logic, particularly important after changes to handle null country codes. The test should include an assertion like assertEquals("es", view.get("isoCode")) to validate correct ISO code formatting when country is absent.

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.
@jcastro-dotcms jcastro-dotcms force-pushed the issue-36106-language-only-locale-country-npe branch from 1054373 to 0156e31 Compare July 2, 2026 18:40
Mockito.when(languageAPI.getLanguage(4L)).thenReturn(language);

final APIProvider toolBox = Mockito.mock(APIProvider.class);
final Field languageApiField = APIProvider.class.getDeclaredField("languageAPI");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[DEFECT] New Edit Content: locale version creation crashes with 500 when using a language-only locale (no country)

1 participant