Skip to content

Latest commit

 

History

History
622 lines (503 loc) · 28.3 KB

README.md

File metadata and controls

622 lines (503 loc) · 28.3 KB

nrich-registry

Maven Central

Overview

nrich-registry is a module whose purpose is to make administration of registry entities on client side easier. It transforms JPA entities in a format that client can interpret to create dynamic forms and tables for administration of entities without additional implementation on server side. Library provides REST API for searching, creating, updating and deleting entities. Users are only required to provide list of regular expressions for including entities they want to administrate. Optionally, users can provide display labels and headers for forms and tables in messages.properties files. For searching, it relies on nrich-search module and provides capability of overriding SearchConfiguration for each entity (default configuration performs a join fetch on each association attribute).

Setting up Spring beans

To be able to use this module following configuration is required. If Jackson's ObjectMapper is not available it should also be defined and if nrich-form-configuration (provides client side validation of registry entities) module is not on classpath then RegistryDataFormConfigurationMappingCustomizer bean is not needed. History requires hibernate-envers dependency and if history is not needed then all beans with history suffix can be omitted.

@Configuration(proxyBeanMethods = false)
public class ApplicationConfiguration {

    @Bean
    public RegistryConfiguration registryConfiguration() {
        RegistryConfiguration registryConfiguration = new RegistryConfiguration();

        RegistryGroupDefinitionConfiguration registryGroupDefinitionConfiguration = new RegistryGroupDefinitionConfiguration();

        registryGroupDefinitionConfiguration.setGroupId("DEFAULT");
        registryGroupDefinitionConfiguration.setIncludeEntityPatternList(Collections.singletonList("^.*\\.demoregistry\\..*$"));

        registryConfiguration.setGroupDefinitionConfigurationList(Collections.singletonList(registryGroupDefinitionConfiguration));

        return registryConfiguration;
    }

    @Bean
    public ModelMapper registryDataModelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        Condition<Object, Object> skipIds = context -> !context.getMapping().getLastDestinationProperty().getName().equals("id");

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

        return modelMapper;
    }

    @Bean
    public ModelMapper registryBaseModelMapper() {
        ModelMapper modelMapper = new ModelMapper();

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

        return modelMapper;
    }

    @Bean
    public StringToTypeConverter<Object> defaultStringToTypeConverter() {
        List<String> dateFormatList = Arrays.asList("dd.MM.yyyy.", "dd.MM.yyyy.'T'HH:mm", "dd.MM.yyyy.'T'HH:mm'Z'");
        List<String> decimalFormatList = Arrays.asList("#0.00", "#0,00");
        String booleanTrueRegexPattern = "^(?i)\\s*(true|yes)\\s*$";
        String booleanFalseRegexPattern = "^(?i)\\s*(false|no)\\s*$";

        return new DefaultStringToTypeConverter(dateFormatList, decimalFormatList, booleanTrueRegexPattern, booleanFalseRegexPattern);
    }

    @Bean
    public StringToEntityPropertyMapConverter registryStringToEntityPropertyMapConverter(List<StringToTypeConverter<?>> stringToTypeConverterList) {
        return new DefaultStringToEntityPropertyMapConverter(stringToTypeConverterList);
    }

    @Bean
    public RegistryConfigurationResolverService registryConfigurationResolverService(RegistryConfiguration registryConfiguration) {
        return new DefaultRegistryConfigurationResolverService(entityManager, registryConfiguration);
    }

    @Bean
    public RegistryConfigurationUpdateInterceptor registryConfigurationUpdateInterceptor(RegistryConfigurationResolverService registryConfigurationResolverService) {
        return new RegistryConfigurationUpdateInterceptor(registryConfigurationResolverService.resolveRegistryOverrideConfigurationMap());
    }

    @Bean
    public RegistryConfigurationService registryConfigurationService(MessageSource messageSource, RegistryConfigurationResolverService registryConfigurationResolverService) {
        List<String> defaultReadOnlyPropertyList = Arrays.asList("id", "version");
        RegistryGroupDefinitionHolder registryGroupDefinitionHolder = registryConfigurationResolverService.resolveRegistryGroupDefinition();
        RegistryHistoryConfigurationHolder registryHistoryConfigurationHolder = registryConfigurationResolverService.resolveRegistryHistoryConfiguration();
        Map<Class<?>, RegistryOverrideConfiguration> registryOverrideConfigurationMap = registryConfigurationResolverService.resolveRegistryOverrideConfigurationMap();

        return new DefaultRegistryConfigurationService(messageSource, defaultReadOnlyPropertyList, registryGroupDefinitionHolder, registryHistoryConfigurationHolder, registryOverrideConfigurationMap);
    }

    @Bean
    public RegistryConfigurationController registryConfigurationController(RegistryConfigurationService registryConfigurationService) {
        return new RegistryConfigurationController(registryConfigurationService);
    }

    @Bean
    public RegistryEntityFinderService registryEntityFinderService(EntityManager entityManager, ModelMapper registryBaseModelMapper,
                                                                   RegistryConfigurationResolverService registryConfigurationResolverService) {
        Map<String, ManagedTypeWrapper> managedTypeWrapperMap = registryConfigurationResolverService.resolveRegistryDataConfiguration().getClassNameManagedTypeWrapperMap();

        return new EntityManagerRegistryEntityFinderService(entityManager, registryBaseModelMapper, managedTypeWrapperMap);
    }

    @Bean
    public RegistryClassResolvingService registryClassResolvingService(RegistryConfigurationResolverService registryConfigurationResolverService) {
        return new DefaultRegistryClassResolvingService(registryConfigurationResolverService.resolveRegistryDataConfiguration(), Collections.emptyMap(), Collections.emptyMap());
    }

    @Bean
    public RegistryDataRequestConversionService registryDataRequestConversionService(ObjectMapper objectMapper, RegistryClassResolvingService registryClassResolvingService) {
        return new DefaultRegistryDataRequestConversionService(objectMapper, registryClassResolvingService);
    }

    @Bean
    public RegistryDataService registryDataService(EntityManager entityManager, ModelMapper registryDataModelMapper, StringToEntityPropertyMapConverter stringToEntityPropertyMapConverter,
                                                   RegistryConfigurationResolverService registryConfigurationResolverService,
                                                   @Autowired(required = false) List<RegistryDataInterceptor> interceptorList, RegistryEntityFinderService registryEntityFinderService) {
        List<RegistryDataInterceptor> interceptors = Optional.ofNullable(interceptorList).orElse(Collections.emptyList());
        RegistryDataConfigurationHolder registryDataConfigurationHolder = registryConfigurationResolverService.resolveRegistryDataConfiguration();

        return new DefaultRegistryDataService(entityManager, registryDataModelMapper, stringToEntityPropertyMapConverter, registryDataConfigurationHolder, interceptors, registryEntityFinderService);
    }

    @Bean
    public RegistryDataController registryDataController(RegistryDataService registryDataService, RegistryDataRequestConversionService registryDataRequestConversionService, Validator validator) {
        return new RegistryDataController(registryDataService, registryDataRequestConversionService, validator);
    }

    @Bean
    public RegistryHistoryService registryHistoryService(EntityManager entityManager, RegistryConfigurationResolverService registryConfigurationResolverService, ModelMapper registryBaseModelMapper,
                                                         RegistryEntityFinderService registryEntityFinderService) {
        RegistryDataConfigurationHolder registryDataConfigurationHolder = registryConfigurationResolverService.resolveRegistryDataConfiguration();
        RegistryHistoryConfigurationHolder historyConfigurationHolder = registryConfigurationResolverService.resolveRegistryHistoryConfiguration();

        return new DefaultRegistryHistoryService(entityManager, registryDataConfigurationHolder, historyConfigurationHolder, registryBaseModelMapper, registryEntityFinderService);
    }

    @Bean
    public RegistryHistoryController registryHistoryController(RegistryHistoryService registryHistoryService) {
        return new RegistryHistoryController(registryHistoryService);
    }

    @Bean
    public FormConfigurationMappingCustomizer registryDataFormConfigurationMappingCustomizer(RegistryConfigurationResolverService registryConfigurationResolverService,
                                                                                             RegistryClassResolvingService registryClassResolvingService) {
        List<Class<?>> registryClassList = registryConfigurationResolverService.resolveRegistryDataConfiguration().getRegistryDataConfigurationList().stream()
            .map(RegistryDataConfiguration::getRegistryType)
            .collect(Collectors.toList());

        return new RegistryDataFormConfigurationMappingCustomizer(registryClassResolvingService, registryClassList);
    }

    @Bean
    public DefaultRegistryEnumService registryEnumService(MessageSource messageSource) {
        return new DefaultRegistryEnumService(messageSource);
    }

    @Bean
    public RegistryEnumController registryEnumController(RegistryEnumService registryEnumService) {
        return new RegistryEnumController(registryEnumService);
    }
}

RegistryConfiguration defines for the what entities will the client side configuration be generated for and searching, creating and updating enabled.

ModelMapper registryDataModelMapper is used to map request data to entity instances.

ModelMapper registryBaseModelMapper is used for other mappings in module.

StringToTypeConverter<?> is an interface from nrich-search module that performs conversion from string to typed instances and is used when querying registry entities. Default implementation (DefaultStringToTypeConverter) accepts a list of data formats and regexes that are used to convert string to types found in properties of entity classes.

StringToEntityPropertyMapConverter is also an interface from nrich-search module that is used for querying registry entities, it is responsible for assembling conditions Map from query string and a list of properties to search ( conversion to typed instances is delegated to StringToTypeConverter<?>). When querying registry entities client API accepts a query (string), and a list of properties to be searched.

RegistryConfigurationResolverService is a service that parses RegistryConfiguration and returns data in format required by other registry services.

RegistryConfigurationUpdateInterceptor is an interface that is invoked before registry entity is created, updated or deleted. RegistryConfigurationUpdateInterceptor is a implementation that checks if these operations are permitted according to defined RegistryConfiguration. Users can define their own interceptors since RegistryDataService accepts a list of interceptors.

RegistryConfigurationService transforms RegistryConfiguration in a format that clients can use for creating dynamic forms and tables.

RegistryConfigurationController is a REST endpoint with single url nrich/registry/configuration/fetch that returns transformed RegistryConfiguration (a list of RegistryGroupConfiguration instances) to client.

RegistryEntityFinderService is used by RegistryDataService and RegistryHistoryService for resolving primary key of entity instances and/or finding them.

RegistryDataService is used for searching, creating, updating and deleting entity instances.

RegistryDataRequestConversionService is used to convert raw json data received from client to typed instances. It delegates resolving of classes to bind to RegistryClassResolvingService.

RegistryClassResolvingService resolves class instances used for conversion of json data to typed instances. It binds either to registry entity or (when defined) it can bind to other class instances (for example when wanting to update only part of properties). If maps with mapping are not empty it resolves from those maps otherwise it searches for classes in same package as registry entity with same name and following suffixes CreateRequest , UpdateRequest and Request it also searches in request package. Users can provide their own implementation of RegistryClassResolvingService interface if additional customization is needed.

RegistryDataController is REST API for RegistryDataService it has five POST methods:

  • nrich/registry/data/list-bulk - loads multiple registry entities

  • nrich/registry/data/list - searches single registry entity

  • nrich/registry/data/create - creates registry entity

  • nrich/registry/data/update - update registry entity

  • nrich/registry/data/delete - deletes registry entity

RegistryHistoryService is service that is reponsible for listing changes on an entity, default implementation uses hibernate-envers to find changes.

RegistryHistoryController is a REST endpoint with single url nrich/registry/history/fetch that returns history of changes on a registry entity (a list of EntityWithRevision) to client.

RegistryDataFormConfigurationMappingCustomizer is used to register registry entities with nrich-form-configuration module so clients can fetch validations for specific entities.

RegistryEnumService is used for fetching and searching enums by description (user provided message). It provides an easy way of keeping all enum description in a single place. In cases where each enum entry implements some methods and information about that is needed in response that can be achieved by defining a static property with name ADDITIONAL_METHODS_FOR_SERIALIZATION. That property should return a list of method names whose results will be added to EnumResult.additionalData map where key will be method name and value will be method result.

RegistryEnumController is REST API for RegistryEnumService it has two POST methods:

  • nrich/registry/enum/list-bulk - loads multiple enums

  • nrich/registry/enum/list - loads/searches single enum

Usage

Central configuration clients should define is:

@Configuration(proxyBeanMethods = false)
public class ApplicationConfiguration {

    @Bean
    public RegistryConfiguration registryConfiguration() {
        RegistryConfiguration registryConfiguration = new RegistryConfiguration();

        RegistryGroupDefinitionConfiguration registryGroupDefinitionConfiguration = new RegistryGroupDefinitionConfiguration();

        registryGroupDefinitionConfiguration.setGroupId("DEFAULT");
        registryGroupDefinitionConfiguration.setIncludeEntityPatternList(Collections.singletonList("^.*\\.demoregistry\\..*$"));

        registryConfiguration.setGroupDefinitionConfigurationList(Collections.singletonList(registryGroupDefinitionConfiguration));

        return registryConfiguration;
    }
}

this defines configuration that will scan JPA entities in package containing demoregistry string. It will make available all entities in that package for resolving configuration, searching, creating, updating and deleting. For each registry entity override configuration RegistryOverrideConfiguration can be defined deciding if a property is editable, sortable, property display order etc. Similarly, custom SearchConfiguration can be defined for each entity overriding default SearchConfiguration for that entity.

After that if localization is required for registry name, form labels and column headers etc. (by default class names and property names are used) they should be defined in appropriate messages.properties files.

DefaultRegistryConfigurationService resolves messages from following key:

groupId.registryGroupIdDisplayName - text displayed for registry group (a list of registry entities)

net.croz.nrich.registry.configuration.stub.RegistryConfigurationTestEntity.registryEntityDisplayName - text displayed as RegistryConfigurationTestEntity name

net.croz.nrich.registry.configuration.stub.RegistryConfigurationTestEntity.name.label - form label for name property of RegistryConfigurationTestEntity entity

net.croz.nrich.registry.configuration.stub.RegistryConfigurationTestEntity.name.header - column header for name property of RegistryConfigurationTestEntity entity

Client then invokes REST API POST method nrich/registry/configuration/fetch and receives configuration in following form (client is then responsible for building forms and tables):

[
    {
        "groupId": "DEFAULT",
        "groupIdDisplayName": "Registry default group",
        "entityConfigurationList": [
            {
                "classFullName": "net.croz.demoregistry.model.Author",
                "name": "Author",
                "displayName": "Author",
                "category": "DEFAULT",
                "readOnly": false,
                "creatable": true,
                "updateable": true,
                "deletable": true,
                "idClassPropertyNameList": [],
                "propertyConfigurationList": [
                    {
                        "name": "id",
                        "javascriptType": "number",
                        "originalType": "java.lang.Long",
                        "formLabel": "Id",
                        "columnHeader": "Id",
                        "editable": false,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": true
                    },
                    {
                        "name": "firstName",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "First name",
                        "columnHeader": "First name",
                        "editable": true,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "lastName",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "Last name",
                        "columnHeader": "Last name",
                        "editable": true,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "similarAuthor",
                        "javascriptType": "OBJECT",
                        "originalType": "net.croz.demoregistry.model.Author",
                        "singularAssociationReferencedClass": "net.croz.demoregistry.model.Author",
                        "formLabel": "Similar author",
                        "columnHeader": "Similar author",
                        "editable": true,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": true,
                        "id": false
                    }
                ],
                "embeddedIdPropertyConfigurationList": [],
                "historyPropertyConfigurationList": [
                    {
                        "name": "revisionNumber",
                        "javascriptType": "number",
                        "originalType": "java.lang.Integer",
                        "formLabel": "revisionNumber",
                        "columnHeader": "revisionNumber",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "revisionTimestamp",
                        "javascriptType": "date",
                        "originalType": "java.util.Date",
                        "formLabel": "revisionTimestamp",
                        "columnHeader": "revisionTimestamp",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "revisionType",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "revisionType",
                        "columnHeader": "revisionType",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    }
                ],
                "historyAvailable": false,
                "idClassIdentity": false,
                "embeddedIdentity": false,
                "identifierAssigned": false
            },
            {
                "classFullName": "net.croz.demoregistry.model.Book",
                "name": "Book",
                "displayName": "Book",
                "category": "DEFAULT",
                "readOnly": false,
                "creatable": true,
                "updateable": true,
                "deletable": true,
                "idClassPropertyNameList": [],
                "propertyConfigurationList": [
                    {
                        "name": "id",
                        "javascriptType": "number",
                        "originalType": "java.lang.Long",
                        "formLabel": "Id",
                        "columnHeader": "Id",
                        "editable": false,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": true
                    },
                    {
                        "name": "isbn",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "Isbn",
                        "columnHeader": "Isbn",
                        "editable": true,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "title",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "Title",
                        "columnHeader": "Title",
                        "editable": true,
                        "sortable": true,
                        "searchable": true,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    }
                ],
                "embeddedIdPropertyConfigurationList": [],
                "historyPropertyConfigurationList": [
                    {
                        "name": "revisionNumber",
                        "javascriptType": "number",
                        "originalType": "java.lang.Integer",
                        "formLabel": "revisionNumber",
                        "columnHeader": "revisionNumber",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "revisionTimestamp",
                        "javascriptType": "date",
                        "originalType": "java.util.Date",
                        "formLabel": "revisionTimestamp",
                        "columnHeader": "revisionTimestamp",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    },
                    {
                        "name": "revisionType",
                        "javascriptType": "string",
                        "originalType": "java.lang.String",
                        "formLabel": "revisionType",
                        "columnHeader": "revisionType",
                        "editable": false,
                        "sortable": true,
                        "searchable": false,
                        "decimal": false,
                        "singularAssociation": false,
                        "id": false
                    }
                ],
                "historyAvailable": false,
                "idClassIdentity": false,
                "embeddedIdentity": false,
                "identifierAssigned": false
            }
        ]
    }
]

After creating form and tables for each of these entities client can then search them by using method POST for url:

nrich/registry/data/list

requests for searching Author:

{
    "classFullName": "net.croz.demoregistry.model.Author",
    "searchParameter": {
        "propertyNameList": [
            "firstName",
            "lastName"
        ],
        "query": "last 1"
    },
    "pageNumber": 0,
    "pageSize": 25,
    "sortPropertyList": [
        {
            "property": "id",
            "direction": "ASC"
        }
    ]
}

classFullName is class name to search, propertyNameList is a list of properties to search on that class. searchParameter and sortPropertyList are not required but paging parameters are.

Response sent from server returns Spring's pageable with list of entity instances:

{
    "content": [
        {
            "id": 32,
            "firstName": "first 1",
            "lastName": "last 1",
            "class": "net.croz.nrichdemowebboot.demoregistry.model.Author"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "pageNumber": 0,
        "pageSize": 25,
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "totalPages": 1,
    "totalElements": 1,
    "last": true,
    "first": true,
    "numberOfElements": 1,
    "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
    },
    "size": 25,
    "number": 0,
    "empty": false
}

For creating entity instance following POST url is used:

nrich/registry/data/create

Request for creating Author is:

{
    "classFullName": "net.croz.nrichdemowebboot.demoregistry.model.Author",
    "jsonEntityData": "{\"firstName\":\"first name\",\"lastName\":\"last name\"}"
}

Response is saved entity instance.

For update entity instance following POST url is used:

nrich/registry/data/update

Request for updating Author is:

{
    "id": 1,
    "classFullName": "net.croz.nrichdemowebboot.demoregistry.model.Author",
    "jsonEntityData": "{\"firstName\":\"updated name\",\"lastName\":\"updated last name\"}"
}

For deleting entity instance following POST url is used:

nrich/registry/data/delete

Request for deleting Author is:

{
    "id": 1,
    "classFullName": "net.croz.nrichdemowebboot.demoregistry.model.Author"
}