Skip to content

fix: normalize AD_Language in preferences and add typed PreferenceUtil getters#611

Open
EdwinBetanc0urt wants to merge 2 commits into
solop-develop:developfrom
EdwinBetanc0urt:bugfix/normalize-ad-language-preference
Open

fix: normalize AD_Language in preferences and add typed PreferenceUtil getters#611
EdwinBetanc0urt wants to merge 2 commits into
solop-develop:developfrom
EdwinBetanc0urt:bugfix/normalize-ad-language-preference

Conversation

@EdwinBetanc0urt

Copy link
Copy Markdown
Member

Summary

  • MPreference.beforeSave now normalizes the Language attribute to a valid AD_Language code at storage time, regardless of which caller wrote the row.
  • A new public MPreference.normalizeLanguageCode(String) exposes a tiered lookup (2-char ISO → LanguageISO, full code → AD_Language, anything else → Name/PrintName) as the single source of truth for resolving any language string into a canonical code.
  • PreferenceUtil gains typed, validated getters (getLanguagePreference, getRolePreference, getClientPreference, getOrganizationPreference, getWarehousePreference) and delegates its internal normalization to MPreference.normalizeLanguageCode, eliminating the duplicated SQL.
  • PreferenceUtil.saveSessionPreferences skips the language write when the supplied value does not normalize, so a junk caller cannot overwrite a previously good row.

Root cause

Legacy UI flows (most visibly the ZK UI login/role flow) persisted the localized display name returned by Language.getName() — e.g. "Español (MX)" — into AD_Preference.Value for the Language attribute. Downstream consumers that read the preference as an identifier (gRPC session-info, the Vue/Tepuy menu API, Elasticsearch index resolution, print engines) then forwarded that value as a query parameter, breaking with HTTP 500 when no index/locale could be resolved from a display name. With multiple callers writing to AD_Preference across the platform, a single point of normalization at the model layer is needed so the data shape is invariant to the writer.

Changes

org/compiere/model/MPreference.java

  • New constant ATTRIBUTE_LANGUAGE = "Language" used by beforeSave.
  • beforeSave now invokes the new normalizeLanguageCode for rows whose Attribute is Language, replacing the stored value with the canonical AD_Language code when a match is found. When no row matches, the raw value is left in place so existing data is not destroyed.
  • New public static String normalizeLanguageCode(String input) — tiered SQL lookup against AD_Language, returns null when the input does not resolve.
  • New imports: org.compiere.util.DB, org.compiere.util.Util.

org/spin/service/grpc/util/base/PreferenceUtil.java

  • New getPreferenceValue(int userId, String attributeName) — fetches the most recent value of one preference for a user.
  • New parseIdOrNegative(String value) — parses an AD_*_ID from preference text, returning -1 for missing/non-numeric values so callers can fall back to a system default and never trust unparseable data.
  • New typed getters: getLanguagePreference, getRolePreference, getClientPreference, getOrganizationPreference, getWarehousePreference. These collapse the previous loop-and-switch retrieval pattern at call sites and document the validation expectations (role/org callers MUST re-check access — privilege-escalation defense).
  • saveSessionPreferences: for the Language row, the new value is normalized via the shared MPreference helper before being written; when normalization returns null (the caller passed a display name or unknown string) the iteration continues, leaving the existing row untouched.
  • Internal normalizeLanguageCode is reduced to a thin wrapper that delegates to MPreference.normalizeLanguageCode, so the SQL lives in one place.
  • Removed unused org.compiere.util.DB import.

Compatibility

  • Existing rows in AD_Preference are not migrated. They auto-heal on the next save() cycle thanks to MPreference.beforeSave, and the read path (PreferenceUtil.getLanguagePreference) tolerates the legacy display-name format during the transition.
  • No public API was removed. getSessionPreferences and saveSessionPreferences keep their existing signatures; the new typed getters are additive.
  • No DDL changes; the new SQL is read-only and uses ROWNUM = 1 for cross-DB compatibility.

Test plan

  • On a database where AD_Preference contains rows with a display-name value for Attribute = 'Language' (e.g. Español (MX)), trigger any update of that row (role switch, login flow that saves preferences) and verify the value is rewritten to the matching AD_Language code (es_MX).
  • Insert a fresh AD_Preference row programmatically with the language attribute set to a 2-char ISO (es), save, and confirm Value is stored as the canonical AD_Language code.
  • Insert with an unknown string (e.g. xyz), save, and confirm the value is left in place (no exception, no overwrite to null).
  • Call PreferenceUtil.getLanguagePreference on a row known to hold a legacy display name and confirm it returns the canonical code.
  • Call PreferenceUtil.saveSessionPreferences with language = "Español (MX)" for a user who already has a valid stored language; confirm the stored value is NOT overwritten (skip-on-junk guarantee).
  • Build downstream services that consume adempiere-core (e.g. the gRPC server and report-engine) against this version and confirm no compilation regressions.

…l getters

## Summary
- `MPreference.beforeSave` now normalizes the `Language` attribute to a valid `AD_Language` code at storage time, regardless of which caller wrote the row.
- A new public `MPreference.normalizeLanguageCode(String)` exposes a tiered lookup (2-char ISO → `LanguageISO`, full code → `AD_Language`, anything else → `Name`/`PrintName`) as the single source of truth for resolving any language string into a canonical code.
- `PreferenceUtil` gains typed, validated getters (`getLanguagePreference`, `getRolePreference`, `getClientPreference`, `getOrganizationPreference`, `getWarehousePreference`) and delegates its internal normalization to `MPreference.normalizeLanguageCode`, eliminating the duplicated SQL.
- `PreferenceUtil.saveSessionPreferences` skips the language write when the supplied value does not normalize, so a junk caller cannot overwrite a previously good row.

## Root cause
Legacy UI flows (most visibly the ZK UI login/role flow) persisted the localized display name returned by `Language.getName()` — e.g. `"Español (MX)"` — into `AD_Preference.Value` for the `Language` attribute. Downstream consumers that read the preference as an identifier (gRPC `session-info`, the Vue/Tepuy menu API, Elasticsearch index resolution, print engines) then forwarded that value as a query parameter, breaking with HTTP 500 when no index/locale could be resolved from a display name. With multiple callers writing to `AD_Preference` across the platform, a single point of normalization at the model layer is needed so the data shape is invariant to the writer.

## Changes
### `org/compiere/model/MPreference.java`
- New constant `ATTRIBUTE_LANGUAGE = "Language"` used by `beforeSave`.
- `beforeSave` now invokes the new `normalizeLanguageCode` for rows whose `Attribute` is `Language`, replacing the stored value with the canonical `AD_Language` code when a match is found. When no row matches, the raw value is left in place so existing data is not destroyed.
- New `public static String normalizeLanguageCode(String input)` — tiered SQL lookup against `AD_Language`, returns `null` when the input does not resolve.
- New imports: `org.compiere.util.DB`, `org.compiere.util.Util`.

### `org/spin/service/grpc/util/base/PreferenceUtil.java`
- New `getPreferenceValue(int userId, String attributeName)` — fetches the most recent value of one preference for a user.
- New `parseIdOrNegative(String value)` — parses an `AD_*_ID` from preference text, returning `-1` for missing/non-numeric values so callers can fall back to a system default and never trust unparseable data.
- New typed getters: `getLanguagePreference`, `getRolePreference`, `getClientPreference`, `getOrganizationPreference`, `getWarehousePreference`. These collapse the previous loop-and-switch retrieval pattern at call sites and document the validation expectations (role/org callers MUST re-check access — privilege-escalation defense).
- `saveSessionPreferences`: for the `Language` row, the new value is normalized via the shared `MPreference` helper before being written; when normalization returns `null` (the caller passed a display name or unknown string) the iteration `continue`s, leaving the existing row untouched.
- Internal `normalizeLanguageCode` is reduced to a thin wrapper that delegates to `MPreference.normalizeLanguageCode`, so the SQL lives in one place.
- Removed unused `org.compiere.util.DB` import.

## Compatibility
- Existing rows in `AD_Preference` are not migrated. They auto-heal on the next `save()` cycle thanks to `MPreference.beforeSave`, and the read path (`PreferenceUtil.getLanguagePreference`) tolerates the legacy display-name format during the transition.
- No public API was removed. `getSessionPreferences` and `saveSessionPreferences` keep their existing signatures; the new typed getters are additive.
- No DDL changes; the new SQL is read-only and uses `ROWNUM = 1` for cross-DB compatibility.

## Test plan
- [ ] On a database where `AD_Preference` contains rows with a display-name value for `Attribute = 'Language'` (e.g. `Español (MX)`), trigger any update of that row (role switch, login flow that saves preferences) and verify the value is rewritten to the matching `AD_Language` code (`es_MX`).
- [ ] Insert a fresh `AD_Preference` row programmatically with the language attribute set to a 2-char ISO (`es`), save, and confirm `Value` is stored as the canonical AD_Language code.
- [ ] Insert with an unknown string (e.g. `xyz`), save, and confirm the value is left in place (no exception, no overwrite to `null`).
- [ ] Call `PreferenceUtil.getLanguagePreference` on a row known to hold a legacy display name and confirm it returns the canonical code.
- [ ] Call `PreferenceUtil.saveSessionPreferences` with `language = "Español (MX)"` for a user who already has a valid stored language; confirm the stored value is NOT overwritten (skip-on-junk guarantee).
- [ ] Build downstream services that consume `adempiere-core` (e.g. the gRPC server and report-engine) against this version and confirm no compilation regressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant