fix: normalize AD_Language in preferences and add typed PreferenceUtil getters#611
Open
EdwinBetanc0urt wants to merge 2 commits into
Open
Conversation
…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.
### Additional context fixes solop-develop/adempiere-solop#2948
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MPreference.beforeSavenow normalizes theLanguageattribute to a validAD_Languagecode at storage time, regardless of which caller wrote the row.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.PreferenceUtilgains typed, validated getters (getLanguagePreference,getRolePreference,getClientPreference,getOrganizationPreference,getWarehousePreference) and delegates its internal normalization toMPreference.normalizeLanguageCode, eliminating the duplicated SQL.PreferenceUtil.saveSessionPreferencesskips 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)"— intoAD_Preference.Valuefor theLanguageattribute. Downstream consumers that read the preference as an identifier (gRPCsession-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 toAD_Preferenceacross 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.javaATTRIBUTE_LANGUAGE = "Language"used bybeforeSave.beforeSavenow invokes the newnormalizeLanguageCodefor rows whoseAttributeisLanguage, replacing the stored value with the canonicalAD_Languagecode when a match is found. When no row matches, the raw value is left in place so existing data is not destroyed.public static String normalizeLanguageCode(String input)— tiered SQL lookup againstAD_Language, returnsnullwhen the input does not resolve.org.compiere.util.DB,org.compiere.util.Util.org/spin/service/grpc/util/base/PreferenceUtil.javagetPreferenceValue(int userId, String attributeName)— fetches the most recent value of one preference for a user.parseIdOrNegative(String value)— parses anAD_*_IDfrom preference text, returning-1for missing/non-numeric values so callers can fall back to a system default and never trust unparseable data.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 theLanguagerow, the new value is normalized via the sharedMPreferencehelper before being written; when normalization returnsnull(the caller passed a display name or unknown string) the iterationcontinues, leaving the existing row untouched.normalizeLanguageCodeis reduced to a thin wrapper that delegates toMPreference.normalizeLanguageCode, so the SQL lives in one place.org.compiere.util.DBimport.Compatibility
AD_Preferenceare not migrated. They auto-heal on the nextsave()cycle thanks toMPreference.beforeSave, and the read path (PreferenceUtil.getLanguagePreference) tolerates the legacy display-name format during the transition.getSessionPreferencesandsaveSessionPreferenceskeep their existing signatures; the new typed getters are additive.ROWNUM = 1for cross-DB compatibility.Test plan
AD_Preferencecontains rows with a display-name value forAttribute = '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 matchingAD_Languagecode (es_MX).AD_Preferencerow programmatically with the language attribute set to a 2-char ISO (es), save, and confirmValueis stored as the canonical AD_Language code.xyz), save, and confirm the value is left in place (no exception, no overwrite tonull).PreferenceUtil.getLanguagePreferenceon a row known to hold a legacy display name and confirm it returns the canonical code.PreferenceUtil.saveSessionPreferenceswithlanguage = "Español (MX)"for a user who already has a valid stored language; confirm the stored value is NOT overwritten (skip-on-junk guarantee).adempiere-core(e.g. the gRPC server and report-engine) against this version and confirm no compilation regressions.