-
Notifications
You must be signed in to change notification settings - Fork 355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: system and user settings as value objects [DHIS2-18061] #18649
Conversation
dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/LazySettings.java
Dismissed
Show dismissed
Hide dismissed
dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConfigurationController.java
Dismissed
Show dismissed
Hide dismissed
dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConfigurationController.java
Dismissed
Show dismissed
Hide dismissed
dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/StaticContentController.java
Fixed
Show fixed
Hide fixed
dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SystemSettingController.java
Fixed
Show fixed
Hide fixed
.../dhis-service-core/src/main/java/org/hisp/dhis/user/hibernate/HibernateUserSettingStore.java
Fixed
Show fixed
Hide fixed
* the store entity type. Use only when the result is a of the store entity type or a list of it. | ||
*/ | ||
protected NativeQuery<T> nativeSynchronizedTypedQuery(@Language("SQL") String sql) { | ||
return getSession().createNativeQuery(sql, clazz).addSynchronizedEntityClass(getClazz()); |
Check failure
Code scanning / CodeQL
Query built from user-controlled sources High
user-provided value
This query depends on a
user-provided value
dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/StaticContentController.java
Fixed
Show fixed
Hide fixed
dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/LazySettings.java
Dismissed
Show dismissed
Hide dismissed
String key = e.getKey(); | ||
String value = e.getValue(); | ||
keys[i] = key; | ||
values[i++] = isConfidential(key) ? decoder.apply(key, value) : value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First time seeing an index used like this. This might not be clear to readers who don't know how pre/post increment works. Just an observation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, then it is good you haven't looked at some of the parser code I wrote :) But for clarity we can move the increment out to its own statement.
} | ||
|
||
@Nonnull | ||
private JsonValue asJson(String key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did we always allow storing json as a system setting? This seems like something that would be more suited in the datastore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The toJson
(and asJson
) part is purely to not break our REST API where settings were exposed as JSON in some cases and so I tried to have them stay in the original JSON type. I would hope that we can simplify this at some point and just treat all of them as JSON string as well. As you say JSON values better fit the datastore and as of now there are no complex values any more in settings.
public class DefaultSystemSettingsTranslationService implements SystemSettingsTranslationService { | ||
|
||
/** The namespace used to store settings translations in the datastore. */ | ||
private static final String NS = "settings-translations"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder would it be useful to have all system namespaces declared somewhere so we know what namespaces are used? Kind of like authorities. So they're not scattered around the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. Something that needs some consideration and time to arrive at a conclusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks nice Jan 👍
I have a question about translations but maybe we can chat at the next meeting. When I hear translations, I think there's 1 service that looks after that, but I see we have a settings translation service here.
Yes, lets do that. You are fully correct that ideally we have a universal translation service not one scoped to settings. But that is a another very big step. |
Quality Gate passedIssues Measures |
Summary
Implements
SystemSettings
andUserSettings
as immutable value objects.Qualities of settings...
String
to typed values on accessUsage
UserSettings
: Access can (and must) be static (since we need that inBaseIdentifiableObject.getTranslation
therefore the staticSessionUserSettings
solution)SystemSettings
: Access must useSystemSettingsProvider
(orSystemSettingsService
) as the initialisation requires beansSettings are created from a
Map<String, String>
usingUserSettings.of
andSystemSettings.of
. This is used when instances are created from DB but can also be used to manually create them in tests. As settings use interfaces they can also be mocked (not that I recommend it).API Nuances
Settings and translations for settings are not a single feature but two features.
To request a translation a
locale
must be specified to indicate the translation endpoint.To access the translation in the current user's locale the locale is provided as empty, like so
&locale=
.This means
/api/systemSettings/{key}
without a locale will not return a translation in the current user's locale, whereas/api/systemSettings/{key}?locale=
will.Representation
Settings are key-value pairs. Both keys and values are
String
s in DB. In code the rawString
value is converted (parsed) into different types on access.LazySettings
are used for bothSystemSettings
andUserSettings
as the class implements a general set of settings using aString[]
for both keys and another for raw values. The arrays are sorted by keys in alphabetical order to allow O(log n) access while having a minimal memory footprint. ASerializable[]
holds the converted typed values. The conversion is encoded in the specific accessor methods ofSystemSettings
andUserSettings
interfaces which implement these asdefault
methods based on the abstract methods defined bySettings
whichLazySettings
implement.This means settings can generally hold any set of settings. This mainly targets the settings used in the backend code for which there are accessor methods. Through these their type and default is known. Settings without such method are not used in the backend and therefore always stay
String
.This means both
SettingKey
andUserSettingKey
enums have been removed. Their type and default value have become thedefault
methods inSystemSettings
andUserSettings
interfaces.Life-cycle
Settings are always used as sets of settings not individual values. The entire set is an immutable value. If a value in the set is outdated the entire set is reloaded. The re-leading differs for
SystemSettings
andUserSettings
.For
SystemSettings
the service holds the recent set in a field. Being immutable the instance can be shared freely. When it becomes outdated by a write the field is setnull
causing the next access to reload it from DB. In addition the set is kept in aThreadLocal
for each thread that is initialized implicitly on access so that during a request the settings always stay constant.For
UserSettings
theSessionUserSettings
holds the settings for the duration of the user session. The set includes the fallbacks to the values of theSystemSettings
. In addition the set it kept in aThreadLoacal
(inThreadUserSettings
) for each thread that is cleared at the start and end of each request and initialized on access from theSessionUserSettings
. This way theUserSettings.getCurrentSettings()
never changes during a request and allows to override individual settings just in the scope of a request.Confidential
Some settings are considered a secret and are therefore encoded.
This affects the value as stored in DB. When settings are in memory in form of a
LazySettings
object the values are always decoded (plain). Confidential settings are marked as such by annotating their accessor method using a new annotation@Confidential
. A confidential setting is not included in the set of settings accessible through the API. The only exception is plain single key access as superuser (ALL) allows to read any setting to preserve existing behaviour.Manipulation
Settings are handled as key-value pairs. This is equally reflected in the API to manipulate them as they ask for keys and values as a
Map
or as a single key and value.When updating a setting value the value needs to be presented as a
String
. However, as settings were converted between several classes and theirString
a conversion other thantoString()
is required. This is handled bySettings.valueOf
method. When individual settings are passed to a service this conversion is implicit and does not need to be applied by the caller.Translations
The translations feature has been extracted to a new dedicated interface
SystemSettingsTranslationService
. The implementation is now backed by the datastore (the translations are no longer saved in thesystemsetting
table).The translations use the namespace
settings-translations
which is protected with the existing authority required to use settingsF_SYSTEM_SETTING
. Internally reading the translation via the service will use theSystemUser
to make access possible without having the authority. Direct read access through the datastore however will require the authority.Set vs Effective Settings
For settings that do have a default value the effective value might not be the one set in case it fails to parse to the requested target type which will then use the default value.
Therefore the
toJson
which is used to support the REST API returns the effective values whereastoMap
returns the values as present (set).Automatic Tests
A LOT of tests need to be modified to make this work. The main reasons are API changes to set and get settings and that system settings are now stable within a request. While this is a good feature in production in tests this tends to work against the intent since a change to settings only becomes visible in the thread after calling
SystemSettingsService.clearCurrentSettings
. But this has worked in a positive way still as it made visible which tests do depend on settings without previously showing this in the code. In that sense many tests have become more descriptive.New unit tests were added for the code that is entirely new. I removed any settings store level tests and replaced it with service level tests. I also removed several tests methods in other tests that no longer apply as the scenario does not exist any longer due to semantic changes of the settings when it comes to null-ness.
Manual Tests
Settings affect most features of the server. A few obvious things to test
Next
SystemSetting
andUserSetting
are no longer used I think, we might actually not need them any more. Needs to be checked, the schema might be the last reason to have them.package-info.java
(I started)