Skip to content

Commit

Permalink
Add support for ignoring properties when updating registry entites
Browse files Browse the repository at this point in the history
  • Loading branch information
agrancaric committed Nov 15, 2024
1 parent e2dd23a commit 569c5cc
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 35 deletions.
1 change: 1 addition & 0 deletions nrich-registry-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ dependencies {
annotationProcessor "org.projectlombok:lombok"
compileOnly "org.projectlombok:lombok"

implementation "org.modelmapper:modelmapper"
implementation "org.springframework.data:spring-data-commons"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.croz.nrich.registry.api.core.customizer;

import org.modelmapper.ModelMapper;

public interface ModelMapperCustomizer {

void customize(ModelMapperType type, ModelMapper modelMapper);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.croz.nrich.registry.api.core.customizer;

public enum ModelMapperType {
BASE, DATA
}
57 changes: 29 additions & 28 deletions nrich-registry-spring-boot-starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,18 @@ Note if using `nrich-bom` dependency versions should be omitted.

Configuration is done through a property file, available properties and descriptions are given bellow (all properties are prefixed with nrich.registry which is omitted for readability):

| property | description | default value |
|--------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| default-read-only-property-list | List of property names that should always be marked as readonly | |
| default-converter-enabled | Whether default string to type converter used for converting strings to property values when searching registry is enabled | true |
| default-java-to-javascript-converter-enabled | Whether default Java to Javascript type converter is enabled | true |
| registry-search.date-format-list | List of date formats used to convert string to date value | dd.MM.yyyy., dd.MM.yyyy.'T'HH:mm, dd.MM.yyyy.'T'HH:mm'Z' |
| registry-search.decimal-number-format-list | List of decimal formats used to convert string to decimal value | #0.00, #0,00 |
| registry-search.boolean-true-regex-pattern | Regexp pattern that is used to match boolean true values | ^(?i)\s*(true|yes|da)\s*$ |
| registry-search.boolean-false-regex-pattern | Regexp pattern that is used to match boolean false values | ^(?i)\s*(false|no|ne)\s*$ |
| registry-configuration.create-registry-class-mapping | Optional mapping between registry classname and the class holding validations for registry create method | |
| registry-configuration.update-registry-class-mapping | Optional mapping between registry classname and the class holding validations for registry update method | |
| property | description | default value |
|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| default-read-only-property-list | List of property names that should always be marked as readonly | |
| default-converter-enabled | Whether default string to type converter used for converting strings to property values when searching registry is enabled | true |
| default-java-to-javascript-converter-enabled | Whether default Java to Javascript type converter is enabled | true |
| registry-model-mapper-customizer-enabled | Whether default ModelMapper customizer used for skipping ignored properties when updating registry entities is enabled. | true |
| registry-search.date-format-list | List of date formats used to convert string to date value | dd.MM.yyyy., dd.MM.yyyy.'T'HH:mm, dd.MM.yyyy.'T'HH:mm'Z' |
| registry-search.decimal-number-format-list | List of decimal formats used to convert string to decimal value | #0.00, #0,00 |
| registry-search.boolean-true-regex-pattern | Regexp pattern that is used to match boolean true values | ^(?i)\s*(true|yes|da)\s*$ |
| registry-search.boolean-false-regex-pattern | Regexp pattern that is used to match boolean false values | ^(?i)\s*(false|no|ne)\s*$ |
| registry-configuration.create-registry-class-mapping | Optional mapping between registry classname and the class holding validations for registry create method | |
| registry-configuration.update-registry-class-mapping | Optional mapping between registry classname and the class holding validations for registry update method | |

The properties under registry-search are used when converting string received from client to property values that will be used to search registry. For example if a string is sent
and the property searched by is of type Date, nrich will try to parse string to Date and if parsing succeeds restriction will be added to query and if parsing fails the property will be skipped
Expand All @@ -63,14 +64,14 @@ The default configuration values in yaml format for easier modification are give
```yaml

nrich.registry:
default-read-only-property-list:
default-converter-enabled: true
default-java-to-javascript-converter-enabled: true
registry-search:
date-format-list: dd.MM.yyyy., dd.MM.yyyy.'T'HH:mm, dd.MM.yyyy.'T'HH:mm'Z'
decimal-number-format-list: #0.00, #0,00
boolean-true-regex-pattern: ^(?i)\s*(true|yes|da)\s*$
boolean-false-regex-pattern: ^(?i)\s*(false|no|ne)\s*$
default-read-only-property-list:
default-converter-enabled: true
default-java-to-javascript-converter-enabled: true
registry-search:
date-format-list: dd.MM.yyyy., dd.MM.yyyy.'T'HH:mm, dd.MM.yyyy.'T'HH:mm'Z'
decimal-number-format-list: #0.00, #0,00
boolean-true-regex-pattern: ^(?i)\s*(true|yes|da)\s*$
boolean-false-regex-pattern: ^(?i)\s*(false|no|ne)\s*$

```

Expand All @@ -85,12 +86,12 @@ of type [RegistryClassResolvingService](../nrich-registry-api/src/main/java/net/
This module is meant to be used through REST API and as such exposes multiple endpoints. For a detailed description of each endpoint see `nrich-registry` [README.MD](../nrich-registry/README.md).
Bellow is just a short overview of available endpoints (all methods use HTTP POST method):

| request path | description |
|---------------------------------------|------------------------------------------------------------------------------------------------|
| `nrich/registry/configuration/fetch` | Fetches configuration of all entities (used on client for generating dynamic forms and tables) |
| `nrich/registry/data/list-bulk` | Lists multiple registry entities |
| `nrich/registry/data/list` | List a single registry entity (also supports searching if query parameter is specified) |
| `nrich/registry/data/create` | Creates registry entity |
| `nrich/registry/data/update` | Update registry entity |
| `nrich/registry/data/delete` | Deletes registry entity |
| `nrich/registry/history` | List all the revisions of the entity (available only if envers is on classpath) |
| request path | description |
|--------------------------------------|------------------------------------------------------------------------------------------------|
| `nrich/registry/configuration/fetch` | Fetches configuration of all entities (used on client for generating dynamic forms and tables) |
| `nrich/registry/data/list-bulk` | Lists multiple registry entities |
| `nrich/registry/data/list` | List a single registry entity (also supports searching if query parameter is specified) |
| `nrich/registry/data/create` | Creates registry entity |
| `nrich/registry/data/update` | Update registry entity |
| `nrich/registry/data/delete` | Deletes registry entity |
| `nrich/registry/history` | List all the revisions of the entity (available only if envers is on classpath) |
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import net.croz.nrich.javascript.converter.DefaultJavaToJavascriptTypeConverter;
import net.croz.nrich.javascript.service.DefaultJavaToJavascriptTypeConversionService;
import net.croz.nrich.registry.api.configuration.service.RegistryConfigurationService;
import net.croz.nrich.registry.api.core.customizer.ModelMapperCustomizer;
import net.croz.nrich.registry.api.core.customizer.ModelMapperType;
import net.croz.nrich.registry.api.core.model.RegistryOverrideConfiguration;
import net.croz.nrich.registry.api.core.model.RegistryOverrideConfigurationHolder;
import net.croz.nrich.registry.api.core.service.RegistryClassResolvingService;
Expand All @@ -49,6 +51,7 @@
import net.croz.nrich.registry.core.support.ManagedTypeWrapper;
import net.croz.nrich.registry.data.controller.RegistryDataController;
import net.croz.nrich.registry.data.customizer.RegistryDataFormConfigurationMappingCustomizer;
import net.croz.nrich.registry.data.customizer.RegistryDataModelMapperCustomizer;
import net.croz.nrich.registry.data.service.DefaultRegistryDataRequestConversionService;
import net.croz.nrich.registry.data.service.DefaultRegistryDataService;
import net.croz.nrich.registry.data.service.RegistryDataRequestConversionService;
Expand Down Expand Up @@ -79,6 +82,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

Expand Down Expand Up @@ -113,16 +117,22 @@ public Validator validator() {
return new LocalValidatorFactoryBean();
}

@ConditionalOnProperty(name = "nrich.registry.registry-model-mapper-customizer-enabled", havingValue = "true", matchIfMissing = true)
@Bean
public ModelMapperCustomizer registryDataModelMapperCustomizer(@Autowired(required = false) List<RegistryOverrideConfigurationHolder> registryOverrideConfigurationHolderList) {
return new RegistryDataModelMapperCustomizer(registryOverrideConfigurationHolderList);
}

@ConditionalOnMissingBean(name = "registryDataModelMapper")
@Bean
public ModelMapper registryDataModelMapper() {
return strictModelMapper();
public ModelMapper registryDataModelMapper(@Autowired(required = false) List<ModelMapperCustomizer> modelMapperCustomizerList) {
return strictModelMapper(ModelMapperType.DATA, modelMapperCustomizerList);
}

@ConditionalOnMissingBean(name = "registryBaseModelMapper")
@Bean
public ModelMapper registryBaseModelMapper() {
return strictModelMapper();
public ModelMapper registryBaseModelMapper(@Autowired(required = false) List<ModelMapperCustomizer> modelMapperCustomizerList) {
return strictModelMapper(ModelMapperType.BASE, modelMapperCustomizerList);
}

@ConditionalOnMissingBean
Expand Down Expand Up @@ -313,11 +323,15 @@ public RegistryEnumController registryEnumController(RegistryEnumService registr
return new RegistryEnumController(registryEnumService);
}

private ModelMapper strictModelMapper() {
protected ModelMapper strictModelMapper(ModelMapperType type, List<ModelMapperCustomizer> modelMapperCustomizerList) {
ModelMapper modelMapper = new ModelMapper();

modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

if (!CollectionUtils.isEmpty(modelMapperCustomizerList)) {
modelMapperCustomizerList.forEach(modelMapperCustomizer -> modelMapperCustomizer.customize(type, modelMapper));
}

return modelMapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
* @param registrySearch Registry search configuration used by {@link net.croz.nrich.search.converter.DefaultStringToTypeConverter}.
* @param defaultConverterEnabled Whether default string to type converter ({@link net.croz.nrich.search.converter.DefaultStringToTypeConverter}) used for converting strings to property values when searching registry is enabled.
* @param defaultJavaToJavascriptConverterEnabled Whether default Java to Javascript type converter ({@link net.croz.nrich.javascript.converter.DefaultJavaToJavascriptTypeConverter}) used for converting Java to Javascript types is enabled.
* @param registryModelMapperCustomizerEnabled Whether default ModelMapper customizer ({@link net.croz.nrich.registry.data.customizer.RegistryDataModelMapperCustomizer}) used for skipping ignored properties when updating registry entities is enabled.
* @param registryConfiguration Registry configuration used for defining entities and groups which will be managed.
*/
@ConfigurationProperties("nrich.registry")
public record NrichRegistryProperties(List<String> defaultReadOnlyPropertyList, @DefaultValue @NestedConfigurationProperty RegistrySearchProperties registrySearch,
@DefaultValue("true") boolean defaultConverterEnabled,
@DefaultValue("true") boolean defaultJavaToJavascriptConverterEnabled,
@DefaultValue("true") boolean registryModelMapperCustomizerEnabled,
RegistryConfiguration registryConfiguration) {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import net.croz.nrich.javascript.api.converter.JavaToJavascriptTypeConverter;
import net.croz.nrich.javascript.api.service.JavaToJavascriptTypeConversionService;
import net.croz.nrich.registry.api.configuration.service.RegistryConfigurationService;
import net.croz.nrich.registry.api.core.customizer.ModelMapperCustomizer;
import net.croz.nrich.registry.api.core.model.RegistryConfiguration;
import net.croz.nrich.registry.api.core.model.RegistryGroupDefinitionConfiguration;
import net.croz.nrich.registry.api.core.model.RegistryOverrideConfigurationHolder;
Expand Down Expand Up @@ -79,6 +80,7 @@ void shouldNotRegisterConfigurationWhenNoPropertyValuesHaveBeenConfigured() {
assertThat(context).doesNotHaveBean(RegistryHistoryService.class);
assertThat(context).doesNotHaveBean(RegistryEnumService.class);
assertThat(context).doesNotHaveBean(FormConfigurationMappingCustomizer.class);
assertThat(context).doesNotHaveBean(ModelMapperCustomizer.class);
});
}

Expand Down Expand Up @@ -154,6 +156,7 @@ void shouldConfigureDefaultConfiguration() {
assertThat(context).hasSingleBean(JavaToJavascriptTypeConversionService.class);
assertThat(context).hasSingleBean(RegistryEnumService.class);
assertThat(context).hasSingleBean(NrichRegistryProperties.class);
assertThat(context).hasSingleBean(ModelMapperCustomizer.class);
assertThat(context.getBean(NrichRegistryProperties.class).registrySearch()).isNotNull();

assertThat(context).doesNotHaveBean(RegistryConfigurationController.class);
Expand Down Expand Up @@ -202,4 +205,13 @@ void shouldRegisterRegistryOverrideConfiguration() {
assertThat(registryProperties.registryConfiguration().getOverrideConfigurationHolderList()).containsExactly(holder);
});
}

@Test
void shouldNotCreateRegistryModelMapperCustomizerWhenCreationIsDisabled() {
// expect
contextRunner.withPropertyValues(REGISTRY_CONFIGURATION).withBean(LocalValidatorFactoryBean.class)
.withPropertyValues("nrich.registry.registry-model-mapper-customizer-enabled=false").run(context ->
assertThat(context).doesNotHaveBean(ModelMapperCustomizer.class)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package net.croz.nrich.registry.data.customizer;

import lombok.RequiredArgsConstructor;
import net.croz.nrich.registry.api.core.customizer.ModelMapperCustomizer;
import net.croz.nrich.registry.api.core.customizer.ModelMapperType;
import net.croz.nrich.registry.api.core.model.RegistryOverrideConfiguration;
import net.croz.nrich.registry.api.core.model.RegistryOverrideConfigurationHolder;
import org.modelmapper.ModelMapper;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
public class RegistryDataModelMapperCustomizer implements ModelMapperCustomizer {

private final List<RegistryOverrideConfigurationHolder> registryOverrideConfigurationHolderList;

@Override
public void customize(ModelMapperType type, ModelMapper modelMapper) {
if (type != ModelMapperType.DATA || CollectionUtils.isEmpty(registryOverrideConfigurationHolderList)) {
return;
}

registryOverrideConfigurationHolderList.forEach(registryOverrideConfigurationHolder -> {
List<String> ignoredPropertyList = Optional.ofNullable(registryOverrideConfigurationHolder.getOverrideConfiguration())
.map(RegistryOverrideConfiguration::getIgnoredPropertyList)
.orElse(Collections.emptyList());

if (ignoredPropertyList.isEmpty()) {
return;
}

modelMapper.getConfiguration().setPropertyCondition(context -> {
if (context.getParent() != null && registryOverrideConfigurationHolder.getType().equals(context.getParent().getDestinationType())) {
return context.getMapping().getDestinationProperties().stream().noneMatch(property -> ignoredPropertyList.contains(property.getName()));
}

return true;
});
});
}
}
Loading

0 comments on commit 569c5cc

Please sign in to comment.