FINERACT-2169: New command processing infrastructure
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
fd45f43f36
commit
8d83b5547c
+3
-1
@@ -28,6 +28,7 @@ buildscript {
|
||||
[
|
||||
'fineract-api',
|
||||
'fineract-core',
|
||||
'fineract-command',
|
||||
'fineract-accounting',
|
||||
'fineract-provider',
|
||||
'fineract-branch',
|
||||
@@ -103,7 +104,7 @@ plugins {
|
||||
id 'com.gorylenko.gradle-git-properties' version '2.4.2' apply false
|
||||
id 'org.asciidoctor.jvm.convert' version '3.3.2' apply false
|
||||
id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false
|
||||
id 'com.google.cloud.tools.jib' version '3.4.4' apply false
|
||||
id 'com.google.cloud.tools.jib' version '3.4.5' apply false
|
||||
id 'org.sonarqube' version '6.0.1.5171'
|
||||
id 'com.github.andygoossens.modernizer' version '1.10.0' apply false
|
||||
// TODO: upgrade to 6.0.4
|
||||
@@ -112,6 +113,7 @@ plugins {
|
||||
id "com.github.davidmc24.gradle.plugin.avro-base" version "1.9.1" apply false
|
||||
id 'org.openapi.generator' version '7.8.0' apply false
|
||||
id 'com.gradleup.shadow' version '8.3.5' apply false
|
||||
id 'me.champeau.jmh' version '0.7.1' apply false
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.release.gradle"
|
||||
|
||||
@@ -37,6 +37,7 @@ dependencyManagement {
|
||||
mavenBom 'org.mockito:mockito-bom:5.14.2'
|
||||
mavenBom 'software.amazon.awssdk:bom:2.29.9'
|
||||
mavenBom 'io.github.resilience4j:resilience4j-bom:2.2.0'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.20.4'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -235,6 +236,8 @@ dependencyManagement {
|
||||
//v42.7.5: performance issue: https://github.com/pgjdbc/pgjdbc/issues/3511#issuecomment-2637277977
|
||||
dependency 'org.postgresql:postgresql:42.7.4'
|
||||
|
||||
dependency 'com.mysql:mysql-connector-j:9.2.0'
|
||||
|
||||
dependency 'org.assertj:assertj-core:3.26.3'
|
||||
|
||||
dependency 'org.apache.commons:commons-math3:3.6.1'
|
||||
@@ -249,5 +252,18 @@ dependencyManagement {
|
||||
dependency 'io.cucumber:cucumber-spring:7.20.1'
|
||||
|
||||
dependency 'org.reflections:reflections:0.10.2'
|
||||
|
||||
dependency 'org.openjdk.jmh:jmh-core:1.37'
|
||||
dependency 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
|
||||
|
||||
dependency 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.0.3'
|
||||
dependency 'org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3'
|
||||
dependency 'org.springframework.restdocs:spring-restdocs-webtestclient:3.0.3'
|
||||
dependency 'org.springframework.restdocs:spring-restdocs-restassured:3.0.3'
|
||||
|
||||
dependency 'com.lmax:disruptor:3.4.4'
|
||||
|
||||
dependency 'com.ibm.icu:icu4j:76.1'
|
||||
dependency 'org.yakworks:spring-icu4j:0.4.2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,8 @@
|
||||
<suppress files="[/\\]build[/\\]generated[/\\]" checks=".*"/>
|
||||
<!-- TODO: fix integration test formatting -->
|
||||
<suppress files="[/\\]integration-tests[/\\]src[/\\]test[/\\]main[/\\]" checks=".*"/>
|
||||
<!-- TODO: fix generic type name constraints -->
|
||||
<suppress files="[/\\]fineract-command[/\\]src[/\\]main[/\\]java[/\\]" checks="MethodTypeParameterName"/>
|
||||
<suppress files="[/\\]fineract-command[/\\]src[/\\]main[/\\]java[/\\]" checks="ClassTypeParameterName"/>
|
||||
<suppress files="[/\\]fineract-command[/\\]src[/\\]main[/\\]java[/\\]" checks="HideUtilityClassConstructor"/>
|
||||
</suppressions>
|
||||
|
||||
@@ -76,7 +76,7 @@ jib {
|
||||
}
|
||||
// NOTE: other custom dependencies
|
||||
implementation 'org.mariadb.jdbc:mariadb-java-client'
|
||||
implementation 'mysql:mysql-connector-java:8.0.33'
|
||||
implementation 'com.mysql:mysql-connector-j'
|
||||
implementation 'org.postgresql:postgresql'
|
||||
annotationProcessor('org.springframework.boot:spring-boot-autoconfigure-processor')
|
||||
annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
Background and Motivation
|
||||
-------------------------
|
||||
|
||||
Fineract accumulated some technical debt over the years. One area that is implicated is type-safety of internal and external facing APIs, the most prominent of which is Fineract's REST API. In general the package layout of the project reflects a more or less classic layered architecture (REST API, data transfer/value objects, business logic services, storage/repositories). The project predates some of the more modern frameworks and best practices that are available today and on occasions the data structures that are exchanged offer some challenges (e.g. generic types). Fineract's code base reflects that, especially where JSON de-/serialization is involved. Nowadays, this task would be simply delegated to the Jackson framework, but when Fineract (Mifos) started the decision was made to use Google's GSON library and create handcrafted helper classes to deal with JSON parsing. While this provided a lot of flexibility this approach had some downsides:
|
||||
|
||||
- the lowest common denominator is the string type (aka JSON blob); this is where we lose the type information
|
||||
- the strings are transformed into JSONObjects; a little bit better than raw strings, but barely more than a hash map
|
||||
- a ton of "magic" strings are needed to get/set values
|
||||
- this approach makes refactoring unnecessarily more difficult
|
||||
- to be able to serve an OpenAPI descriptor (as JSON and YAML) we had to re-introduce the type information at the REST API level with dummy classes that contain only the specified attributes; those classes are only used with the Swagger annotations and no were else
|
||||
- some developers skipped the layered architecture and found it too tedious to maintain DTOs and JSON helper classes, and as a result just passed JSONObjects right to the business logic layer
|
||||
- now the business logic is unnecessarily aware of how Fineract communicates to the outside world and makes replacing/enhancing the communication protocol (e.g. with GRPC) pretty much impossible
|
||||
|
||||
The list doesn't end here, but in the end things boil down to two main points:
|
||||
|
||||
- poor developer experience: boilerplate code and missing type safety cost more time
|
||||
- bugs: the more code the more likely errors get introduced, especially when type safety is missing and we have to rely on runtime errors (vs. compile time).
|
||||
|
||||
There has been already some preparatory work done concerning type safety, but until now we avoided dealing with the real source of this issue. Fineract's architectures devises read from write requests ("CQRS", https://martinfowler.com/bliki/CQRS.html) for improved scalability.
|
||||
|
||||
The read requests are not that problematic, but all write requests pass through a component/service that is called "SynchronousCommandProcessingService. As the name suggests the execution of business logic is synchronous (mostly) due to this part of the architecture. This is not necessarily a problem (not immediately at least), but it's nevertheless a central bottleneck in the system. Even more important: this service is responsible to route incoming commands to their respective handler classes which in turn execute functions on one or more business logic services. The payload of these commands are obviously not always the same... which is the main reason why we decided to use the lowest common denominator to be able to handle these various types and rendered all payloads as strings. This compromise bubbles now up in the REST API and the business logic layers (and actually everything in between).
|
||||
|
||||
Over the years we've also added additional features (e.g. idempotency guarantees for incoming write requests) that make it now very hard to reason about the execution flow. Testing the performance impact of such additions to the critical execution path even can't be properly measured. Note: the current implementation of idempotency relies on database lookups (quite often, for each incoming request) and none of those queries are cached. If we wanted to store already processed requests (IDs) in a faster system (let's Redis) then this can't be done without major refactoring.
|
||||
|
||||
In conclusion, if we really want to fix those issues that are not only cosmetic and affect the performance and the developer experience equally then we urgently need to fix the way how we process write requests aka commands.
|
||||
|
||||
Target Personas
|
||||
---------------
|
||||
|
||||
- developers
|
||||
- integrators
|
||||
- end users
|
||||
- BaaS
|
||||
|
||||
Goals
|
||||
-----
|
||||
|
||||
- new command processing will run independently next to the legacy mechanics
|
||||
- self contained
|
||||
- fully tested
|
||||
- ensure that the REST API is 100% backward compatible
|
||||
- try to contain the migration and make it as easy as possible for the community to integrate those changes
|
||||
- introduce types where needed and migrate the (old) JAX-RS REST resource classes to Spring Web MVC (better performance and better testability)
|
||||
- introduce DTOs if not already available and make sure if they exist that they are not outdated
|
||||
- assemble one DTO as command payload from all incoming REST API parameters (headers, query/path paramters, request bodies)
|
||||
- annotate attributes in the DTOs with Jakarta Validation annotations to enforce constraints on their values
|
||||
- wired REST API to the new command processing, one service at a time/pull request
|
||||
- take a non-critical service (like document management) and migrate it to the new command processing mechanics from top (REST API) to bottom (business logic service)
|
||||
- refactor command handlers to new internal API
|
||||
- make sure that the business service logic classes/functions take only one DTO request input parameter (aka don't let a function have 12 input parameters of type string...)
|
||||
- when all integration tests run successfully then remove all legacy boilerplate code that is not used anymore
|
||||
- make an ordered list of modules/features (easiest, lowest hanging fruit first)
|
||||
- maintain at least the same performance as the current implementation
|
||||
- optional: improve performance if it can be done in a reasonable time frame
|
||||
- optional: improve resilience if it can be done in a reasonable time frame
|
||||
|
||||
Non-Goals
|
||||
---------
|
||||
|
||||
- current command processing will stay untouched, will run independently of new infrastructure
|
||||
- don't try cleaning up the storage layer; that's a separate effort for later (type safe queries, query peformance, clean entity classes)
|
||||
- doesn't need to be optimized for speed immediately
|
||||
- no changes in the integration tests
|
||||
|
||||
Proposed API Changes
|
||||
--------------------
|
||||
|
||||
1. Command Wrapper
|
||||
|
||||
TBD
|
||||
|
||||
Class contains some generic atttributes like:
|
||||
|
||||
- username
|
||||
- tenant ID
|
||||
- timestamp
|
||||
|
||||
The actual payload (aka command input parameters) are defined as a generic parameter. It is expected that the modules implement classes that introduce the payload types and inherit from the abstract command class.
|
||||
|
||||
2. Command Processing Service
|
||||
|
||||
TBD
|
||||
|
||||
- synchronously (required): this is pretty much as we do right now (use virtual threads optionally)
|
||||
- asynchronously (optional): with executor service and completable futures (use virtual threads optionally)
|
||||
- non-blocking (optional): high perfomance LMAX Disruptor non-blocking implementation
|
||||
|
||||
These different perfromance level implementations need to be absolute drop-in replacements (for each other). It is expected that more performant implementations need more testing due to increased complexity and possible unforseen side effects. In case any problems show up we can always roll back to the required default implementation (synchronous).
|
||||
|
||||
NOTE: we should consider providing a command processing implementation based on Apache Camel once this concept is approved and we migrated already a couple of services. They are specialized for exactly this kind of use cases and have more dedicated people working on it's implementation. Could give more flexibility without us needing to maintain code.
|
||||
|
||||
3. Middlewares
|
||||
|
||||
TBD
|
||||
|
||||
4. Command Handlers
|
||||
|
||||
TBD
|
||||
|
||||
5. References to users (aka AppUser)
|
||||
|
||||
Keep things lightweight and only reference users by their user names.f
|
||||
|
||||
Risks
|
||||
-----
|
||||
|
||||
TBD
|
||||
|
||||
- feature creep
|
||||
|
||||
ETA
|
||||
---
|
||||
|
||||
A first prototype of the a new command processing component is ready for evaluation. There is also an initial smoke test (JMH) available.
|
||||
|
||||
You can try it out with the following instructions (it's still in a private repository, but will be published soon as an official PR):
|
||||
|
||||
```
|
||||
git clone git@github.com:vidakovic/fineract.git
|
||||
cd fineract
|
||||
git checkout feature/FINERACT-2169
|
||||
./gradlew :fineract-command:build
|
||||
./gradlew :fineract-command:jmh
|
||||
```
|
||||
|
||||
Diagrams
|
||||
--------
|
||||
|
||||
TBD
|
||||
|
||||
Related Jira Tickets
|
||||
--------------------
|
||||
|
||||
- https://issues.apache.org/jira/browse/FINERACT-2169
|
||||
- https://issues.apache.org/jira/browse/FINERACT-2021
|
||||
- https://issues.apache.org/jira/browse/FINERACT-1744
|
||||
- https://issues.apache.org/jira/browse/FINERACT-1909
|
||||
@@ -0,0 +1,337 @@
|
||||
Refactoring Instructions
|
||||
========================
|
||||
|
||||
General
|
||||
--------
|
||||
|
||||
1. POJOs
|
||||
|
||||
Please make sure that all POJOs (for request and response types) have a similar structure. They should:
|
||||
|
||||
- use Lombok to reduce boilerplate code
|
||||
- make sure that all annotations are always in the same order (see example code)
|
||||
- avoid `record`s for now (we might or might not migrate later from Lombok to `record`)
|
||||
- each class should implement `java.io.Serializable`
|
||||
- each class should contain a serialization version set to `1L`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
...
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
|
||||
private String content;
|
||||
}
|
||||
```
|
||||
|
||||
2. Transformation between types
|
||||
|
||||
If you need to transform between two POJOs then use MapStruct wherever possible. Place the MapStruct interfaces under a sibling package `mapping` next to the `data` package.
|
||||
|
||||
3. Package structure (`org.apache.fineract.xxx.*`)
|
||||
|
||||
Please make sure to always follow this package structure pattern:
|
||||
|
||||
- `api`: contains all REST JAX-RS resource classes (later Spring Web MVC controllers)
|
||||
- `command`: primarily used for command specific child class implementations of `org.apache.fineract.command.core.Command` (see section "Command Pipeline preserving Type Information")
|
||||
- `data`: contains all DTOs (request and response types)
|
||||
- `domain`: contains all entity/table mapping classes
|
||||
- `handler`: contains all command handlers
|
||||
- `service`: contains business logic services
|
||||
- `mapping`: contains MapStruct interfaces
|
||||
- `middleware`: contains middleware related to command processing; I am not expecting that we will need any of this during the refactoring process
|
||||
- `security`: might contain later so called Spring Security "authorization managers" for more complex use cases
|
||||
- `serialization`: technically we should not need this package anymore after we are done with the refactorings; in theory there could be very complex data structures that are not easily digestable by Jackson; for those case we could still use this package to add de-/serialization helpers (Jackson provides a proper API for this)
|
||||
- `starter`: Spring Java configuration that allows us to make Fineract customizable (make parts of the system replaceable)
|
||||
- `validation`: contains custom Jakarta Validation components/annotations
|
||||
|
||||
In general avoid too many nesting levels. Ideally we would have only one additional underneath the base package (`org.apache.fineract.*`) where in turn only the above package patterns are used.
|
||||
|
||||
REST API
|
||||
--------
|
||||
|
||||
1. Read Requests
|
||||
|
||||
The **read requests** (HTTP **GET**) usually only require to refactor (if at all) the business logic services in case they don't return proper Java POJOs. Please name all return types for these read requests consistently. We propose to add always a suffix `Response`; this makes it immediately clear that we are dealing with a data transfer object (vs database mapping entities) and that it's something that we return to the clients (vs incoming requests). Historically we've used `Data` as a suffix, but this doesn't make it clear if it's used as input or output. Let's see an example.
|
||||
|
||||
This is how the legacy code looks like:
|
||||
|
||||
```
|
||||
public class BusinessDateApiResource {
|
||||
...
|
||||
@GET
|
||||
public String getBusinessDates(@Context final UriInfo uriInfo) {
|
||||
securityContext.authenticatedUser().validateHasReadPermission("BUSINESS_DATE");
|
||||
final List<BusinessDateData> foundBusinessDates = this.readPlatformService.findAll();
|
||||
ApiRequestJsonSerializationSettings settings = parameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.jsonSerializer.serialize(settings, foundBusinessDates);
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This is how the refactored code should look like; there is absolutely no need to manually serialize:
|
||||
|
||||
```
|
||||
public class BusinessDateApiResource {
|
||||
...
|
||||
@GET
|
||||
public List<BusinessDateResponse> getBusinessDates() {
|
||||
securityContext.authenticatedUser().validateHasReadPermission(BUSINESS_DATE);
|
||||
return this.readPlatformService.findAll();
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
[!IMPORTANT] We should make sure that all response DTOs should reside in a package `org.apache.fineract.xxx.data`; for above example `org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse`!
|
||||
|
||||
Bonus: very often you'll see that we are using our homegrown `PlatformSecurityContext` to validate read permissions. This is the **WRONG** way to do it. Fortunately, fixing this is a low hanging fruit. We just need to introduce an "Ant matcher" configuration in the web configuration. For above example we should add an entry in `org.apache.fineract.infrastructure.core.config.SecurityConfig`. The change should look something like this:
|
||||
|
||||
```
|
||||
@Configuration
|
||||
@ConditionalOnProperty("fineract.security.basicauth.enabled")
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
...
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.securityMatcher(antMatcher("/api/**")).authorizeHttpRequests((auth) -> {
|
||||
auth.requestMatchers(antMatcher(HttpMethod.OPTIONS, "/api/**")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/echo")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/authentication")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/authentication")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration/user")).permitAll()
|
||||
// NOTE: enforce read permission for get business date by type
|
||||
.requestMatchers(antMatcher(HttpMethod.GET, "/api/v1/businessdate/*")).hasPermission("ALL_FUNCTIONS", "ALL_FUNCTIONS_READ", "READ_BUSINESS_DATE")
|
||||
// ... insert more like above...
|
||||
.requestMatchers(antMatcher(HttpMethod.PUT, "/api/*/instance-mode")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
|
||||
.requestMatchers(antMatcher("/api/*/twofactor")).fullyAuthenticated()
|
||||
.requestMatchers(antMatcher("/api/**"))
|
||||
.access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED")));
|
||||
}).httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint()))
|
||||
.cors(Customizer.withDefaults()).csrf((csrf) -> csrf.disable())
|
||||
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class)
|
||||
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class)
|
||||
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class)
|
||||
.addFilterAfter(fineractInstanceModeApiFilter(), CorrelationHeaderFilter.class);
|
||||
if (!Objects.isNull(loanCOBFilterHelper)) {
|
||||
http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class)
|
||||
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class);
|
||||
} else {
|
||||
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class);
|
||||
}
|
||||
|
||||
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
||||
http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class);
|
||||
} else {
|
||||
http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), CorrelationHeaderFilter.class);
|
||||
}
|
||||
|
||||
if (serverProperties.getSsl().isEnabled()) {
|
||||
http.requiresChannel(channel -> channel.requestMatchers(antMatcher("/api/**")).requiresSecure());
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The nice side effect is that we'll have all security rules that we are enforcing in one place. This will enable more flexible customizations around security (will be handled in the modular security proposal).
|
||||
|
||||
The final refactored function should look like this:
|
||||
|
||||
```
|
||||
public class BusinessDateApiResource {
|
||||
...
|
||||
@GET
|
||||
public List<BusinessDateResponse> getBusinessDates() {
|
||||
return this.readPlatformService.findAll();
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
That way we lose immediately dependencies on the security context, the GSON configuration and the handcrafted serialization helper for GSON.
|
||||
|
||||
[!IMPORTANT] We have to make sure that the Jackson parser configuration matches the one for GSON (especially dates etc.)!
|
||||
|
||||
2. Write Requests
|
||||
|
||||
The **write requests** (HTTP **PUT** and **POST**) are the ones that affect the command processing infrastructure. The JSON body in pretty much all of the legacy cases is probably represented as a simple string variable that gets passed to a command wrapper class. All these variables need to be replaced by proper POJO classes that represent the request body. You can get hints how these classes should look like by checking the OpenAPI dummy classes we created to re-introduce the type information for the OpenAPI descriptor (see Swagger annotations).
|
||||
|
||||
Usually you'll have 2 classes to take care of: **requests** (incoming input parameters) and **responses** (outgoing results). These classes are technically **DTO**s (data transfer objects) or **VO**s (value objects) and to make them more recognizable as such I would start standardizing the naming. E.g. if I have a use case aka REST endpoint that creates client data we would name that DTO class `CreateClientRequest`; and if any data needs to be sent back to the client then that class should be called `CreateClientResponse`. This is a very simple mechanic that helps identifying immediately what is what and doesn't require the developers to come up with any naming stunts. Do not try to re-use these DTOs on multiple endpoints, it's not worth it. Create a separate DTO for each endpoint. Nice side effect: all this becomes then nicer to read (both in Java code and in the OpenAPI descriptor and the resulting Asciidoc documentation) and this will make it less likely that names are clashing in the OpenAPI descriptor (and during code generation for the Fineract Java Client).
|
||||
|
||||
Only new thing that needs to be injected in those REST API resource classes (JAX-RS) or controllers (Spring Web MVC) is the `CommandPipeline` component that allows you to send requests to the **command pipeline** which in turn will be processed by **handlers** that eventually call one or more **business logic services**. Results are sent back to the pipeline and are received in the REST API aka returned by `CommandPipeline` as so called **supplier objects**. (Java) Supplier is a functional interface, i.e. there is only one function to be implemented (`get()`). This small abstraction helps us to standardize how the results are delivered (**synchronous**, **asynchronous**, **non-blocking**) and maintain the same internal API.
|
||||
|
||||
3. Jakarta Validation
|
||||
|
||||
TBD
|
||||
|
||||
Example POJO:
|
||||
|
||||
```
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
...
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
...
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotEmpty
|
||||
private String content;
|
||||
}
|
||||
```
|
||||
|
||||
Create message resource bundles for the validation errors.
|
||||
|
||||
TBD
|
||||
|
||||
4. **Skip**: Spring Web MVC
|
||||
|
||||
We will not do this right now, but later we might move away from JAX-RS and introduce **Spring Web MVC** which is mostly just changing the annotations (e.g. `@POST` vs `@PostRequest`). This process should be pretty straight forward and could be even (semi-) automated by using **OpenRewrite** recipes. Once we have proper POJOs for requests and response we can also use **Jakarta Validation** annotations to validate the request content. You'll see that currently we have an explicit service injected everywhere that does those checks manually (that includes some JSON parsing); very tedious and work intensive and hard to refactor. The first setup with Jakarta Validation take a little longer, but once that is working for one use case it should be pretty much rinse and repeat for the rest. Occasionally for very exotic validations there might be a need to implement **custom validations**. Another advantage of this approach: we can finally add the proper **internationalized error messages** on the server side, not only the translation keys. This removes a ton of code on the client side and ensures that the clients always have the correct messages. Read the Jakarta Validation documentation on how that works and use **Hibernate Validation** to implement this (pretty much the defacto standard library).
|
||||
|
||||
5. Security Enforcement
|
||||
|
||||
very often you'll see that we are using our homegrown to validate read permissions. This is the **WRONG** way to do it. Fortunately, fixing this is a low hanging fruit. We just need to introduce an "Ant matcher" configuration in the web configuration. For above example we should add an entry in `org.apache.fineract.infrastructure.core.config.SecurityConfig`. The change should look something like this:
|
||||
|
||||
Another relatively low hanging fruit that can be tackled here is the way we enforce security aka authorization with `org.apache.fineract.infrastructure.security.service.PlatformSecurityContext`. At the moment we use an explicitly injected security service that we call pretty much everywhere in the REST API layer and then decide in numerous if-then-else constructs if we should execute the commands (or read requests for that matter). This is the **WRONG** way to do it. What we should do is to use Spring Security's APIs and define the authorization requirements in the web configuration where you have then all the security related definitions in one place (instead of chasing them down in all the controller classes); in some more complex cases we might need to implement so called `AuthorizationManager` components to enforce a certain authorization; in any case this will all be visible in one place, the web configuration which makes adapting to specific custom situations also a **LOT** easier.
|
||||
|
||||
Because all JSON parsing was done up until now manually some developers got tired and decided to pass the JSON data structures (`CommandWrapper`, `JsonCommand`) directly to the business logic services. This is **WRONG**. The only parameter those business logic functions should receive is exactly ONE Java Pojo that contains all necessary input parameter for that function to execute. **DO NOT** define functions with primitive/base types. It is impossible to understand the order of those parameters once you have e.g. 17 (example to make a point) string variables. Ideally, you'd just pass the request POJOs to those functions. If you need to transform something then use **MapStruct** instead of manual code. The refactoring should not be too bad. Once proper Java POJOs are defined for the functions we just need to replace the manual JSON parsing boilerplate in the service classes and just call the getters/setters of the request POJOs (instead the manual JSON parsing). The results should also be sent back in proper POJOs (responses). I would go as far that even if you have a single string value (example) that you **should** wrap it in a response class (same for the incoming input parameters aka requests). This will make the service interfaces more stable if we add/remove parameters over time; in other words: this will avoid all those refactoring fests we have upstream when we have functions with 12 base type (example) parameters and need to add/remove something. Obviously you need to check where else in Fineract's code base those service functions are called and adapt accordingly, but usually those functions are called in just one place (handlers).
|
||||
|
||||
Command Pipeline preserving Type Information
|
||||
--------------------------------------------
|
||||
|
||||
`CommandPipeline` needs to be injected where needed (usually only REST API controllers). By default everything is configured for `sync`(-hronous) processing. Other modes (`async`, `disruptor`) can be easily configured via application.properties, but need more testing and are out of scope for now. As you can see the command object just contains some of metadata (`createdAt`, `username` etc.) and the payload aka request object. Obviously we have different use cases so that **payload** attribute is defined as a generic **type**. Please check the unit test code how to create a command object with payload/request properly.
|
||||
|
||||
```
|
||||
public class DummyApiResource {
|
||||
...
|
||||
@GET
|
||||
DummyResponse dummy(@HeaderParam("x-fineract-request-id") UUID requestId, @HeaderParam("x-fineract-tenant-id") String tenantId, DummyRequest request) {
|
||||
var command = new DummyCommand();
|
||||
command.setId(requestId);
|
||||
command.setPayload(request);
|
||||
|
||||
tenantService.set(tenantId);
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
return result.get();
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to create command specific child class of the generic (and abstract) `org.apache.fineract.command.core.Command` class. Example:
|
||||
|
||||
```
|
||||
...
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DummyCommand extends Command<DummyRequest> {}
|
||||
```
|
||||
|
||||
If everything is done correctly then no type information will be lost and everything can be parsed without further help by the Jackson parser. Eventually all handcrafted boilerplate JSON parsing code can be dumped. In rare cases we might need to add de-/serialization helper classes (a concept provided by Jackson) to help the parser identify the types properly.
|
||||
|
||||
When we use **Spring Web MVC unit testing** (actually integration testing) gets very easy. Just a couple of annotations and you can execute them pretty much like simple unit tests in your IDE (because Spring Web MVC is a first class citizen obviously in the Spring Framework). NONE of that handcrafted client code like in our current integration tests is required; the tests should be easier to refactor and easier to understand. Writing those tests is **optional** for now, because we have already over **1000 integration tests**. After all those refactorings the REST API should be 100% compatible to upstream even if it uses a different technology stack. If everything passes those ""old"" integration tests then we can be pretty confident that we didn't mess something up. Migrating the integration tests to simpler Spring Web tests can be done later."
|
||||
|
||||
Command Handlers
|
||||
----------------
|
||||
|
||||
In the current CQRS implementation we have already a concept that is called **handlers**. Those handlers are responsible to receive the command objects with their (request) payloads and transform the requests as needed an pass them to one or more business logic services. The refactoring of those handlers should not be too complicated, they just need to implement the Java interface `CommandHandler`. Look at my test samples to see how the implementation details look like.
|
||||
|
||||
The old handlers look somewhat like this:
|
||||
|
||||
```
|
||||
@Service
|
||||
@CommandType(entity = "PAYMENTTYPE", action = "CREATE")
|
||||
public class CreatePaymentTypeCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||
|
||||
@Autowired
|
||||
public CreatePaymentTypeCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
||||
this.paymentTypeWriteService = paymentTypeWriteService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult processCommand(JsonCommand command) {
|
||||
return this.paymentTypeWriteService.createPaymentType(command);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
... and this is how the refactored handler could look like:
|
||||
|
||||
```
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class CreatePaymentTypeCommandHandler implements CommandHandler<CreatePaymentTypeRequest, CreatePaymentTypeResponse> {
|
||||
|
||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||
|
||||
@Override
|
||||
public CreatePaymentTypeResponse handle(Command<CreatePaymentTypeRequest> command) {
|
||||
// TODO: refactor business logic service to accept properly typed request objects as input
|
||||
return paymentTypeWriteService.createPaymentType(command.getPayload());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Execution Mode
|
||||
--------------
|
||||
|
||||
1. Sync Execution
|
||||
|
||||
This is the default execution mode. Performance is to be expected on par with the current legacy implementation all tests need to work with this mode.
|
||||
|
||||
2. **Skip**: Async Execution
|
||||
|
||||
Already included in the current implementation. Just needs a proper **configuration** in **application.properties** (see unit tests). One thing that might need some additional coding: the use of **thread local variables in multi threaded environments** needs some special care to properly work (we use this to identify the current tenant). Also: we should upgrade to JDK 21 and make use of virtual threads (very easy in Spring Boot, simple configuration property). This allows for massive parallel execution that is not bound by physical CPU cores without (take this with a pinch of salt) performance penalties (read: use millions of threads).
|
||||
|
||||
3. **Skip**: Non-blocking Execution
|
||||
|
||||
Already included in the current implementation and configurable. I've tried a couple of combinations with LMAX disruptor, but this needs more testing to figure out optimal an configuration (see also https://lmax-exchange.github.io/disruptor/user-guide/). Would be worth to create more realistic JMH benchmarks. I have added a simple one to get a first idea how the mechanics are working.
|
||||
|
||||
Maker-Checker
|
||||
-------------
|
||||
|
||||
This will be part of a separate proposal. The only related feature I've added here was command persistence so that you can save commands for deferred execution. Other than that I want to keep this concept (command processing) clean and avoid mixing to many concepts/concerns in one place. This ensures better maintainability. Maker-checker is actually a security related concept and should probably be handled with the proper Spring Security APIs (`AuthorizationManager` interface comes to mind). But again, different proposal and can be ignored here for now.
|
||||
|
||||
When we encounter the first need to take care of Maker-Checker then let's figure out a solution that has minimal impact and do a proper cleanup when the Maker-Checker proposal is available.
|
||||
|
||||
[!NOTE] The best guess is that Maker-Checker will probably implemented
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
description = 'Fineract Command'
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'me.champeau.jmh'
|
||||
|
||||
configurations {
|
||||
asciidoctorExtensions
|
||||
providedRuntime // needed for Spring Boot executable WAR
|
||||
providedCompile
|
||||
compile() {
|
||||
exclude module: 'hibernate-entitymanager'
|
||||
exclude module: 'hibernate-validator'
|
||||
exclude module: 'activation'
|
||||
exclude module: 'bcmail-jdk14'
|
||||
exclude module: 'bcprov-jdk14'
|
||||
exclude module: 'bctsp-jdk14'
|
||||
exclude module: 'c3p0'
|
||||
exclude module: 'stax-api'
|
||||
exclude module: 'jaxb-api'
|
||||
exclude module: 'jaxb-impl'
|
||||
exclude module: 'jboss-logging'
|
||||
exclude module: 'itext-rtf'
|
||||
exclude module: 'classworlds'
|
||||
}
|
||||
runtime
|
||||
}
|
||||
|
||||
apply from: 'dependencies.gradle'
|
||||
|
||||
// Configuration for the modernizer plugin
|
||||
// https://github.com/andygoossens/gradle-modernizer-plugin
|
||||
modernizer {
|
||||
ignoreClassNamePatterns = [
|
||||
'.*AbstractPersistableCustom',
|
||||
'.*EntityTables',
|
||||
'.*domain.*'
|
||||
]
|
||||
}
|
||||
|
||||
// If we are running Gradle within Eclipse to enhance classes with OpenJPA,
|
||||
// set the classes directory to point to Eclipse's default build directory
|
||||
if (project.hasProperty('env') && project.getProperty('env') == 'eclipse') {
|
||||
sourceSets.main.java.outputDir = new File(rootProject.projectDir, "fineract-command/bin/main")
|
||||
}
|
||||
|
||||
if (!(project.hasProperty('env') && project.getProperty('env') == 'dev')) {
|
||||
sourceSets {
|
||||
test {
|
||||
java {
|
||||
exclude '**/core/boot/tests/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jmh {
|
||||
// include = ['.*'] // Include all benchmarks (regex-based filter)
|
||||
warmupIterations = 2 // Number of warm-up iterations
|
||||
iterations = 3 // Number of measurement iterations
|
||||
fork = 1 // Number of forks
|
||||
timeOnIteration = '2s' // Time per iteration
|
||||
benchmarkMode = ['thrpt'] // Default benchmark mode
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
implementation(
|
||||
'org.springframework.boot:spring-boot-starter',
|
||||
'org.springframework.boot:spring-boot-starter-validation',
|
||||
'org.springframework.boot:spring-boot-starter-data-jpa',
|
||||
'io.github.resilience4j:resilience4j-spring-boot3',
|
||||
|
||||
'org.liquibase:liquibase-core',
|
||||
|
||||
'com.mysql:mysql-connector-j',
|
||||
'org.postgresql:postgresql',
|
||||
|
||||
'com.fasterxml.jackson.core:jackson-databind',
|
||||
|
||||
'com.google.guava:guava',
|
||||
|
||||
'org.apache.commons:commons-lang3',
|
||||
|
||||
'com.github.spotbugs:spotbugs-annotations',
|
||||
'org.mapstruct:mapstruct',
|
||||
'com.lmax:disruptor',
|
||||
'com.ibm.icu:icu4j',
|
||||
'org.yakworks:spring-icu4j',
|
||||
)
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.mapstruct:mapstruct-processor'
|
||||
jmh 'org.openjdk.jmh:jmh-core'
|
||||
jmh 'org.openjdk.jmh:jmh-generator-annprocess'
|
||||
annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
|
||||
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
|
||||
|
||||
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
|
||||
|
||||
testImplementation ('org.springframework.boot:spring-boot-starter-test') {
|
||||
exclude group: 'com.jayway.jsonpath', module: 'json-path'
|
||||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||
exclude group: 'jakarta.activation'
|
||||
exclude group: 'javax.activation'
|
||||
exclude group: 'org.skyscreamer'
|
||||
}
|
||||
testImplementation (
|
||||
'org.springframework.boot:spring-boot-starter-web',
|
||||
'org.mockito:mockito-inline',
|
||||
'org.openjdk.jmh:jmh-core',
|
||||
'org.testcontainers:junit-jupiter',
|
||||
'org.testcontainers:postgresql',
|
||||
'org.testcontainers:mysql',
|
||||
'org.springframework.restdocs:spring-restdocs-mockmvc',
|
||||
'org.springframework.restdocs:spring-restdocs-webtestclient',
|
||||
'org.springframework.restdocs:spring-restdocs-restassured',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Data
|
||||
@FieldNameConstants
|
||||
public class Command<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String username;
|
||||
|
||||
private T payload;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public final class CommandConstants {
|
||||
|
||||
public static final String COMMAND_REQUEST_ID = "x-fineract-request-id";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface CommandExecutor {
|
||||
|
||||
<REQ, RES> Supplier<RES> execute(Command<REQ> command);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
|
||||
public interface CommandHandler<REQ, RES> {
|
||||
|
||||
RES handle(Command<REQ> command);
|
||||
|
||||
default boolean matches(Command<REQ> command) {
|
||||
TypeToken<REQ> handlerType = new TypeToken<>(getClass()) {};
|
||||
|
||||
return handlerType.getRawType().isAssignableFrom(command.getPayload().getClass());
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CommandMiddleware {
|
||||
|
||||
void invoke(Command<?> command);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface CommandPipeline {
|
||||
|
||||
<REQ, RES> Supplier<RES> send(Command<REQ> command);
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
import com.lmax.disruptor.dsl.ProducerType;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@ConfigurationProperties(prefix = "fineract.command")
|
||||
public final class CommandProperties implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean enabled = true;
|
||||
|
||||
@Builder.Default
|
||||
private CommandExecutorType executor = CommandExecutorType.sync;
|
||||
|
||||
@Builder.Default
|
||||
private Integer ringBufferSize = 1024;
|
||||
|
||||
@Builder.Default
|
||||
private ProducerType producerType = ProducerType.SINGLE;
|
||||
|
||||
public enum CommandExecutorType {
|
||||
sync, async, disruptor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CommandRouter {
|
||||
|
||||
<REQ, RES> CommandHandler<REQ, RES> route(Command<REQ> command);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core.exception;
|
||||
|
||||
import org.apache.fineract.command.core.Command;
|
||||
|
||||
public class CommandHandlerNotFoundException extends RuntimeException {
|
||||
|
||||
public CommandHandlerNotFoundException(Command<?> command) {
|
||||
super("Cannot find a matching handler for " + (command != null ? command.getClass().getSimpleName() : "UKNOWN") + " command");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.core.exception;
|
||||
|
||||
import org.apache.fineract.command.core.Command;
|
||||
|
||||
public class CommandIllegalArgumentException extends RuntimeException {
|
||||
|
||||
public CommandIllegalArgumentException(Command<?> command, String message) {
|
||||
super("Illegal argument for " + command.getClass().getSimpleName() + " command: " + message);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.implementation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandExecutor;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "async")
|
||||
public class AsynchronousCommandExecutor implements CommandExecutor {
|
||||
|
||||
private final List<CommandMiddleware> middlewares;
|
||||
|
||||
private final CommandRouter router;
|
||||
|
||||
@Override
|
||||
public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
|
||||
CompletableFuture<RES> future = CompletableFuture.supplyAsync(() -> {
|
||||
for (CommandMiddleware middleware : middlewares) {
|
||||
middleware.invoke(command);
|
||||
}
|
||||
|
||||
CommandHandler<REQ, RES> handler = router.route(command);
|
||||
|
||||
return handler.handle(command);
|
||||
});
|
||||
|
||||
return future::join;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.implementation;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandExecutor;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@ConditionalOnBean(CommandPipeline.class)
|
||||
public class DefaultCommandPipeline implements CommandPipeline {
|
||||
|
||||
private final CommandExecutor executor;
|
||||
|
||||
@Override
|
||||
public <REQ, RES> Supplier<RES> send(final Command<REQ> command) {
|
||||
requireNonNull(command, "Command must not be null");
|
||||
|
||||
return executor.execute(command);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.implementation;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.apache.fineract.command.core.exception.CommandHandlerNotFoundException;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@ConditionalOnBean(CommandRouter.class)
|
||||
@SuppressWarnings({ "unchecked", "raw" })
|
||||
public class DefaultCommandRouter implements CommandRouter {
|
||||
|
||||
private final List<CommandHandler> commandHandlers;
|
||||
|
||||
@Override
|
||||
public <REQ, RES> CommandHandler<REQ, RES> route(Command<REQ> command) {
|
||||
// TODO: make sure there are no duplicate handlers
|
||||
if (command == null) {
|
||||
throw new CommandHandlerNotFoundException(command);
|
||||
}
|
||||
|
||||
return commandHandlers.stream().filter(handler -> handler.matches(command)).findFirst()
|
||||
.orElseThrow(() -> new CommandHandlerNotFoundException(command));
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.implementation;
|
||||
|
||||
import com.lmax.disruptor.EventHandler;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandExecutor;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "disruptor")
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public class DisruptorCommandExecutor implements CommandExecutor, Closeable {
|
||||
|
||||
private final Disruptor<CommandEvent> disruptor;
|
||||
|
||||
@Override
|
||||
public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
|
||||
CommandEvent<REQ, RES> processedEvent = next(command);
|
||||
|
||||
return processedEvent.getFuture()::join;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
disruptor.shutdown();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
private <REQ, RES> CommandEvent<REQ, RES> next(Command<REQ> command) {
|
||||
var ringBuffer = disruptor.getRingBuffer();
|
||||
|
||||
var sequenceId = ringBuffer.next();
|
||||
|
||||
CommandEvent<REQ, RES> event = ringBuffer.get(sequenceId);
|
||||
event.setCommand(command);
|
||||
ringBuffer.publish(sequenceId);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class CommandEvent<REQ, RES> {
|
||||
|
||||
private Command<REQ> command;
|
||||
private CompletableFuture<RES> future = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static class CompleteableCommandEventHandler implements EventHandler<CommandEvent> {
|
||||
|
||||
private final List<CommandMiddleware> middlewares;
|
||||
|
||||
private final CommandRouter router;
|
||||
|
||||
@Override
|
||||
public void onEvent(CommandEvent event, long sequence, boolean endOfBatch) throws Exception {
|
||||
try {
|
||||
for (CommandMiddleware middleware : middlewares) {
|
||||
middleware.invoke(event.getCommand());
|
||||
}
|
||||
|
||||
var handler = router.route(event.getCommand());
|
||||
|
||||
event.getFuture().complete(handler.handle(event.getCommand()));
|
||||
} catch (Exception e) {
|
||||
event.getFuture().completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.implementation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandExecutor;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "sync")
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public class SynchronousCommandExecutor implements CommandExecutor {
|
||||
|
||||
private final List<CommandMiddleware> middlewares;
|
||||
|
||||
private final CommandRouter router;
|
||||
|
||||
@Override
|
||||
public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
|
||||
for (CommandMiddleware middleware : middlewares) {
|
||||
middleware.invoke(command);
|
||||
}
|
||||
|
||||
CommandHandler<REQ, RES> handler = router.route(command);
|
||||
|
||||
return () -> handler.handle(command);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.persistence;
|
||||
|
||||
import org.apache.fineract.command.persistence.model.CommandEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface CommandRepository extends JpaRepository<CommandEntity, Long>, JpaSpecificationExecutor<CommandEntity> {}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.persistence.converter;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Converter
|
||||
public class JsonAttributeConverter implements AttributeConverter<JsonNode, String> {
|
||||
// TODO: it would be nicer to use a native JSON type on the database side, but not every system supports this;
|
||||
// string/text are the lowest common denominator that should work on every database
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String convertToDatabaseColumn(JsonNode source) {
|
||||
if (source != null) {
|
||||
return mapper.writeValueAsString(source);
|
||||
}
|
||||
|
||||
// TODO: throw exception?
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public JsonNode convertToEntityAttribute(String source) {
|
||||
if (source != null) {
|
||||
return mapper.readTree(source);
|
||||
}
|
||||
|
||||
// TODO: throw exception?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.persistence.mapping;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public final class CommandJsonMapper {
|
||||
|
||||
private static final String CLASS_ATTRIBUTE = "@class";
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public <T> T map(JsonNode source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var canonicalName = source.get(CLASS_ATTRIBUTE).asText();
|
||||
|
||||
try {
|
||||
return (T) mapper.convertValue(source, Class.forName(canonicalName));
|
||||
} catch (Exception e) {
|
||||
log.error("Error while mapping json node", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public JsonNode map(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = mapper.convertValue(source, ObjectNode.class);
|
||||
|
||||
json.set(CLASS_ATTRIBUTE, new TextNode(source.getClass().getCanonicalName()));
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.persistence.mapping;
|
||||
|
||||
import static org.mapstruct.InjectionStrategy.CONSTRUCTOR;
|
||||
import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
|
||||
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.persistence.model.CommandEntity;
|
||||
import org.mapstruct.InheritInverseConfiguration;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@Mapper(componentModel = SPRING, injectionStrategy = CONSTRUCTOR, uses = { CommandJsonMapper.class })
|
||||
public interface CommandMapper {
|
||||
|
||||
@Mapping(ignore = true, target = "id")
|
||||
@Mapping(source = "id", target = "commandId")
|
||||
@Mapping(source = "createdAt", target = "createdAt")
|
||||
@Mapping(source = "tenantId", target = "tenantId")
|
||||
@Mapping(source = "username", target = "username")
|
||||
@Mapping(source = "payload", target = "payload")
|
||||
CommandEntity map(Command source);
|
||||
|
||||
@InheritInverseConfiguration
|
||||
Command map(CommandEntity source);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.persistence.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.PostLoad;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Transient;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import org.apache.fineract.command.persistence.converter.JsonAttributeConverter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@FieldNameConstants
|
||||
@Entity
|
||||
@Table(name = "m_command")
|
||||
public class CommandEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@jakarta.persistence.Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Transient
|
||||
@Setter(value = AccessLevel.NONE)
|
||||
private boolean isNew = true;
|
||||
|
||||
@Column(name = "command_id")
|
||||
private UUID commandId;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@Column(name = "tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Column(name = "username")
|
||||
private String username;
|
||||
|
||||
@Column(name = "payload")
|
||||
@Convert(converter = JsonAttributeConverter.class)
|
||||
private JsonNode payload;
|
||||
|
||||
@PrePersist
|
||||
@PostLoad
|
||||
void markNotNew() {
|
||||
this.isNew = false;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.starter;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@AutoConfiguration
|
||||
@Import({ CommandConfiguration.class, CommandPersistenceConfiguration.class })
|
||||
public class CommandAutoConfiguration {}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.starter;
|
||||
|
||||
import com.lmax.disruptor.IgnoreExceptionHandler;
|
||||
import com.lmax.disruptor.WaitStrategy;
|
||||
import com.lmax.disruptor.YieldingWaitStrategy;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import com.lmax.disruptor.util.DaemonThreadFactory;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.CommandProperties;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.apache.fineract.command.implementation.DisruptorCommandExecutor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(CommandProperties.class)
|
||||
@ComponentScan("org.apache.fineract.command.core")
|
||||
@ComponentScan("org.apache.fineract.command.implementation")
|
||||
class CommandConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
WaitStrategy waitStrategy() {
|
||||
return new YieldingWaitStrategy();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Disruptor<?> disruptor(CommandProperties properties, WaitStrategy waitStrategy, List<CommandMiddleware> middlewares,
|
||||
CommandRouter router) {
|
||||
// TODO: make this more configurable
|
||||
|
||||
// Create the disruptor
|
||||
Disruptor<DisruptorCommandExecutor.CommandEvent> disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new,
|
||||
properties.getRingBufferSize(), DaemonThreadFactory.INSTANCE, properties.getProducerType(), waitStrategy);
|
||||
|
||||
disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(middlewares, router));
|
||||
disruptor.setDefaultExceptionHandler(new IgnoreExceptionHandler());
|
||||
|
||||
// Start the disruptor
|
||||
disruptor.start();
|
||||
|
||||
return disruptor;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.starter;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("org.apache.fineract.command.persistence")
|
||||
class CommandPersistenceConfiguration {}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.apache.fineract.command.starter.CommandAutoConfiguration
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="command-00001" context="postgresql">
|
||||
<createTable tableName="m_command">
|
||||
<column autoIncrement="true" name="id" type="bigint" remarks="Internal ID">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="created_at" type="timestamp with time zone" />
|
||||
<column name="command_id" type="uuid" />
|
||||
<column name="tenant_id" type="varchar(255)" />
|
||||
<column name="username" type="varchar(255)" />
|
||||
<column name="payload" type="text" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="command-00002" context="mysql">
|
||||
<createTable tableName="command">
|
||||
<column autoIncrement="true" name="id" type="bigint" remarks="Internal ID">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="created_at" type="datetime" />
|
||||
<column name="command_id" type="uuid" />
|
||||
<column name="tenant_id" type="varchar(255)" />
|
||||
<column name="username" type="varchar(255)" />
|
||||
<column name="payload" type="text" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.persistence.CommandRepository;
|
||||
import org.apache.fineract.command.persistence.mapping.CommandMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@ContextConfiguration(classes = TestConfiguration.class)
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
abstract class CommandBaseTest {
|
||||
|
||||
protected static Network network = Network.newNetwork();
|
||||
|
||||
@Container
|
||||
protected static final GenericContainer POSTGRES_CONTAINER = new GenericContainer("postgres:16").withNetwork(network)
|
||||
.withNetworkAliases("postgres").withExposedPorts(5432)
|
||||
.withEnv(Map.of("POSTGRES_DB", "fineract-test", "POSTGRES_USER", "root", "POSTGRES_PASSWORD", "mifos"));
|
||||
|
||||
@Autowired
|
||||
protected CommandRepository commandRepository;
|
||||
|
||||
@Autowired
|
||||
protected CommandMapper commandMapper;
|
||||
|
||||
@DynamicPropertySource
|
||||
protected static void configure(DynamicPropertyRegistry registry) {
|
||||
POSTGRES_CONTAINER.start();
|
||||
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
registry.add("spring.datasource.username", () -> "root");
|
||||
registry.add("spring.datasource.password", () -> "mifos");
|
||||
registry.add("spring.datasource.url", () -> "jdbc:postgresql://" + POSTGRES_CONTAINER.getHost() + ":"
|
||||
+ POSTGRES_CONTAINER.getMappedPort(5432) + "/fineract-test");
|
||||
registry.add("spring.datasource.platform", () -> "postgresql");
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@ContextConfiguration(classes = TestConfiguration.class)
|
||||
class CommandPersistenceTest extends CommandBaseTest {
|
||||
|
||||
@Test
|
||||
void save() {
|
||||
var content = "hello";
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
|
||||
var commandEntity = commandMapper.map(command);
|
||||
|
||||
var result = commandRepository.save(commandEntity);
|
||||
|
||||
log.info("Saved command: {}", result);
|
||||
|
||||
var found = commandRepository.findById(result.getId());
|
||||
|
||||
log.info("Found command: {}", found.orElse(null));
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import com.lmax.disruptor.YieldingWaitStrategy;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import com.lmax.disruptor.dsl.ProducerType;
|
||||
import com.lmax.disruptor.util.DaemonThreadFactory;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.apache.fineract.command.implementation.DefaultCommandPipeline;
|
||||
import org.apache.fineract.command.implementation.DefaultCommandRouter;
|
||||
import org.apache.fineract.command.implementation.DisruptorCommandExecutor;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.handler.DummyCommandHandler;
|
||||
import org.apache.fineract.command.sample.middleware.DummyIdempotencyMiddleware;
|
||||
import org.apache.fineract.command.sample.middleware.DummyMiddleware;
|
||||
import org.apache.fineract.command.sample.service.DefaultDummyService;
|
||||
import org.apache.fineract.command.sample.service.DefaultDummyTenantService;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.TearDown;
|
||||
|
||||
@Slf4j
|
||||
@BenchmarkMode(Mode.Throughput) // Measures operations per second
|
||||
@State(Scope.Benchmark) // Benchmark state for each thread
|
||||
@OutputTimeUnit(TimeUnit.SECONDS) // Output results in seconds
|
||||
@SuppressWarnings({ "raw" })
|
||||
public class CommandPipelineBenchmark {
|
||||
|
||||
private CommandRouter router;
|
||||
private Disruptor<DisruptorCommandExecutor.CommandEvent> disruptor;
|
||||
|
||||
private CommandPipeline pipeline;
|
||||
|
||||
@Setup(Level.Iteration)
|
||||
public void setUp() {
|
||||
this.router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService(new DefaultDummyTenantService()))));
|
||||
|
||||
// Create the disruptor
|
||||
this.disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new, 2048, DaemonThreadFactory.INSTANCE, ProducerType.MULTI,
|
||||
new YieldingWaitStrategy());
|
||||
|
||||
disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(
|
||||
List.of(new DummyMiddleware(), new DummyIdempotencyMiddleware()), router));
|
||||
|
||||
// Start the disruptor
|
||||
disruptor.start();
|
||||
|
||||
pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor));
|
||||
}
|
||||
|
||||
@TearDown(Level.Iteration)
|
||||
@SneakyThrows
|
||||
public void tearDown() {
|
||||
disruptor.shutdown(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void processCommand() {
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content("hello").build());
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
// NOTE: force yield
|
||||
result.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@ContextConfiguration(classes = TestConfiguration.class)
|
||||
class CommandPipelineTest extends CommandBaseTest {
|
||||
|
||||
@Autowired
|
||||
private CommandPipeline pipeline;
|
||||
|
||||
@Test
|
||||
void processCommand() {
|
||||
var content = "hello";
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
|
||||
var result = pipeline.send(command);
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
|
||||
Object response = result.get();
|
||||
|
||||
assertNotNull(response, "Response should not be null.");
|
||||
|
||||
assertInstanceOf(DummyResponse.class, response, "Response is of wrong type.");
|
||||
|
||||
if (response instanceof DummyResponse dummyResponse) {
|
||||
log.warn("Result: {}", dummyResponse);
|
||||
|
||||
assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
|
||||
assertNotNull(dummyResponse.getRequestId(), "Request ID should not be null.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ContextConfiguration(classes = TestConfiguration.class)
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
class CommandSampleApiTest extends CommandBaseTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
private List<ClientHttpRequestInterceptor> interceptors;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.baseUrl = "http://localhost:" + port + "/test/dummy";
|
||||
this.interceptors = Collections.singletonList((request, body, execution) -> {
|
||||
var headers = request.getHeaders();
|
||||
headers.add(COMMAND_REQUEST_ID, UUID.randomUUID().toString());
|
||||
headers.add("x-fineract-tenant-id", "dummy");
|
||||
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
|
||||
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
|
||||
return execution.execute(request, body);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validation() {
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
var problemDetail = restTemplate.postForObject(baseUrl + "/sync", DummyRequest.builder().build(), ProblemDetail.class);
|
||||
|
||||
log.warn("Problem detail (sync) : {} ({})", problemDetail.getDetail(), problemDetail.getProperties());
|
||||
|
||||
assertNotNull(problemDetail, "Response should not be null.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dummyApiSync() {
|
||||
var content = "test-sync";
|
||||
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
var result = restTemplate.postForObject(baseUrl + "/sync", DummyRequest.builder().content(content).build(), DummyResponse.class);
|
||||
|
||||
log.warn("Result (sync) : {} ({})", result.getContent(), result.getRequestId());
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
assertNotNull(result.getContent(), "Response body should not be null.");
|
||||
assertNotNull(result.getRequestId(), "Request ID should not be null.");
|
||||
assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
|
||||
assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dummyApiAsync() {
|
||||
var content = "test-async";
|
||||
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
var result = restTemplate.postForObject(baseUrl + "/async", DummyRequest.builder().content(content).build(), DummyResponse.class);
|
||||
|
||||
log.warn("Result (async): {} ({})", result.getContent(), result.getRequestId());
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
assertNotNull(result.getContent(), "Response body should not be null.");
|
||||
assertNotNull(result.getRequestId(), "Request ID should not be null.");
|
||||
// TODO: make sure all ThreadLocal variables are available
|
||||
// assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
|
||||
// assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dummyApiIdempotencyAsync() {
|
||||
dummyApiIdempotency("/async");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dummyApiIdempotencySync() {
|
||||
dummyApiIdempotency("/sync");
|
||||
}
|
||||
|
||||
void dummyApiIdempotency(String path) {
|
||||
var id = UUID.randomUUID().toString();
|
||||
var content = "test-idempotency";
|
||||
var request = DummyRequest.builder().content(content).build();
|
||||
List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList((req, body, execution) -> {
|
||||
var headers = req.getHeaders();
|
||||
headers.add(COMMAND_REQUEST_ID, id);
|
||||
headers.add("x-fineract-tenant-id", "dummy");
|
||||
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
|
||||
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
|
||||
return execution.execute(req, body);
|
||||
});
|
||||
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
|
||||
// first request passes
|
||||
var response = restTemplate.postForEntity(baseUrl + path, request, Map.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(OK);
|
||||
|
||||
// second request fails, because we are using the same request ID in both cases
|
||||
response = restTemplate.postForEntity(baseUrl + path, request, Map.class);
|
||||
|
||||
log.warn("Body: {} - {}", response.getBody(), response.getStatusCode());
|
||||
|
||||
// Assert HTTP status
|
||||
assertThat(response.getStatusCode()).isEqualTo(INTERNAL_SERVER_ERROR);
|
||||
|
||||
log.info("Idempotency all good!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandProperties;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import yakworks.i18n.icu.ICUBundleMessageSource;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({ CommandProperties.class, JpaProperties.class })
|
||||
@EnableAutoConfiguration
|
||||
@EnableJpaRepositories(basePackages = { "org.apache.fineract.command.persistence" })
|
||||
@EnableAsync
|
||||
@PropertySource("classpath:application-test.properties")
|
||||
@ComponentScan("org.apache.fineract.command.sample")
|
||||
public class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
MessageSource messageSource() {
|
||||
var messageSource = new ICUBundleMessageSource();
|
||||
messageSource.setBasenames("classpath:org.apache.fineract.messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Validator validatorFactoryBean(MessageSource messageSource) {
|
||||
var localValidatorFactoryBean = new LocalValidatorFactoryBean();
|
||||
localValidatorFactoryBean.setValidationMessageSource(messageSource);
|
||||
|
||||
return localValidatorFactoryBean;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.api;
|
||||
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.service.DefaultDummyTenantService;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping(value = "/test/dummy", consumes = APPLICATION_JSON_VALUE, produces = { APPLICATION_JSON_VALUE,
|
||||
APPLICATION_PROBLEM_JSON_VALUE })
|
||||
class DummyApiController {
|
||||
// "application/vnd.fineract+json;charset=UTF-8;version=1.0"
|
||||
|
||||
private final DefaultDummyTenantService tenantService;
|
||||
|
||||
private final CommandPipeline pipeline;
|
||||
|
||||
@PostMapping("/sync")
|
||||
DummyResponse dummySync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
|
||||
@RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @Valid @RequestBody DummyRequest request) {
|
||||
var command = new DummyCommand();
|
||||
command.setId(requestId);
|
||||
command.setPayload(request);
|
||||
command.setCreatedAt(OffsetDateTime.now(ZoneId.of("UTC")));
|
||||
|
||||
tenantService.set(tenantId);
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
@Async
|
||||
@PostMapping("/async")
|
||||
CompletableFuture<DummyResponse> dummyAsync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
|
||||
@RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @RequestBody DummyRequest request) {
|
||||
var command = new DummyCommand();
|
||||
command.setId(requestId);
|
||||
command.setPayload(request);
|
||||
|
||||
tenantService.set(tenantId);
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
return CompletableFuture.supplyAsync(result);
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.api;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.sample.data.DummyError;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestControllerAdvice
|
||||
public class DummyExceptionHandler {
|
||||
|
||||
private final MessageSource messageSource;
|
||||
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public ResponseEntity<ProblemDetail> handleRuntimeException(Throwable ex) {
|
||||
var problemDetails = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage());
|
||||
problemDetails.setType(URI.create("https://fineract.apache.org/validation"));
|
||||
problemDetails.setTitle(messageSource.getMessage("org.apache.fineract.common.validation.error", null, Locale.US)); // TODO:
|
||||
// check
|
||||
// this,
|
||||
// get
|
||||
// locale
|
||||
|
||||
List<DummyError> errors = new ArrayList<>();
|
||||
|
||||
if (ex instanceof ConstraintViolationException cve) {
|
||||
for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
|
||||
errors.add(new DummyError(violation.getPropertyPath().toString(), violation.getMessage(), violation.getMessageTemplate()));
|
||||
}
|
||||
} else if (ex instanceof WebExchangeBindException webe) {
|
||||
for (FieldError fieldError : webe.getFieldErrors()) {
|
||||
errors.add(new DummyError(fieldError.getField(), fieldError.getDefaultMessage(), fieldError.getCode()));
|
||||
}
|
||||
} else if (ex instanceof MethodArgumentNotValidException manve) {
|
||||
for (FieldError fieldError : manve.getBindingResult().getFieldErrors()) {
|
||||
errors.add(new DummyError(fieldError.getField(), fieldError.getDefaultMessage(), fieldError.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
problemDetails.setProperty("errors", errors);
|
||||
}
|
||||
|
||||
return ResponseEntity.badRequest().contentType(APPLICATION_PROBLEM_JSON).body(problemDetails);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.command;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DummyCommand extends Command<DummyRequest> {}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyError implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String field;
|
||||
|
||||
private String message;
|
||||
|
||||
private String code;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "{org.apache.fineract.dummy.request.content.not-empty}")
|
||||
private String content;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyResponse implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID requestId;
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String content;
|
||||
|
||||
private String error;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.service.DummyService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DummyCommandHandler implements CommandHandler<DummyRequest, DummyResponse> {
|
||||
|
||||
private final DummyService dummyService;
|
||||
|
||||
@Override
|
||||
public DummyResponse handle(Command<DummyRequest> command) {
|
||||
return dummyService.process(command.getPayload());
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.middleware;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.exception.CommandIllegalArgumentException;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DummyIdempotencyMiddleware implements CommandMiddleware {
|
||||
|
||||
// NOTE: in production you would use of course a database or Redis
|
||||
private static final List<UUID> IDS = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void invoke(Command<?> command) {
|
||||
if (command instanceof DummyCommand c) {
|
||||
if (IDS.contains(c.getId())) {
|
||||
throw new CommandIllegalArgumentException(c, "Duplicate request ID: " + c.getId());
|
||||
}
|
||||
|
||||
IDS.add(c.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.middleware;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DummyMiddleware implements CommandMiddleware {
|
||||
|
||||
@Override
|
||||
public void invoke(Command<?> command) {
|
||||
if (command instanceof DummyCommand c) {
|
||||
c.getPayload().setId(command.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.service;
|
||||
|
||||
import java.util.Locale;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class DefaultDummyService implements DummyService {
|
||||
|
||||
private final DefaultDummyTenantService tenantService;
|
||||
|
||||
@Override
|
||||
public DummyResponse process(DummyRequest request) {
|
||||
return DummyResponse.builder().requestId(request.getId()).tenantId(tenantService.get())
|
||||
.content(request.getContent().toUpperCase(Locale.ROOT)).build();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DefaultDummyTenantService {
|
||||
|
||||
private static final ThreadLocal<String> tenantHolder = new ThreadLocal<>();
|
||||
|
||||
public void set(String tenantId) {
|
||||
tenantHolder.set(tenantId);
|
||||
}
|
||||
|
||||
public String get() {
|
||||
return tenantHolder.get();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.service;
|
||||
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
|
||||
public interface DummyService {
|
||||
|
||||
DummyResponse process(DummyRequest request);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
#server.error.include-message=always
|
||||
#server.error.include-binding-errors=always
|
||||
#server.error.include-stacktrace=always
|
||||
debug=true
|
||||
|
||||
fineract.command.enabled=true
|
||||
fineract.command.executor=sync
|
||||
fineract.command.ring-buffer-size=1024
|
||||
fineract.command.producer-type=single
|
||||
|
||||
spring.liquibase.enabled=true
|
||||
spring.liquibase.drop-first=true
|
||||
spring.liquibase.default-schema=public
|
||||
spring.liquibase.contexts=postgresql
|
||||
spring.liquibase.change-log=classpath:/db/custom-changelog/0001_command_init.xml
|
||||
@@ -0,0 +1,71 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
# common
|
||||
org.apache.fineract.common.validation.error=Validation Error
|
||||
org.apache.fineract.common.validation.AssertFalse.message=must be false
|
||||
org.apache.fineract.common.validation.AssertTrue.message=must be true
|
||||
org.apache.fineract.common.validation.DecimalMax.message=must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
|
||||
org.apache.fineract.common.validation.DecimalMin.message=must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
|
||||
org.apache.fineract.common.validation.Digits.message=numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
|
||||
org.apache.fineract.common.validation.Email.message=must be a well-formed email address
|
||||
org.apache.fineract.common.validation.Future.message=must be a future date
|
||||
org.apache.fineract.common.validation.FutureOrPresent.message=must be a date in the present or in the future
|
||||
org.apache.fineract.common.validation.Max.message=must be less than or equal to {value}
|
||||
org.apache.fineract.common.validation.Min.message=must be greater than or equal to {value}
|
||||
org.apache.fineract.common.validation.Negative.message=must be less than 0
|
||||
org.apache.fineract.common.validation.NegativeOrZero.message=must be less than or equal to 0
|
||||
org.apache.fineract.common.validation.NotBlank.message=must not be blank
|
||||
org.apache.fineract.common.validation.NotEmpty.message=must not be empty
|
||||
org.apache.fineract.common.validation.NotNull.message=must not be null
|
||||
org.apache.fineract.common.validation.Null.message=must be null
|
||||
org.apache.fineract.common.validation.Past.message=must be a past date
|
||||
org.apache.fineract.common.validation.PastOrPresent.message=must be a date in the past or in the present
|
||||
org.apache.fineract.common.validation.Pattern.message=must match "{regexp}"
|
||||
org.apache.fineract.common.validation.Positive.message=must be greater than 0
|
||||
org.apache.fineract.common.validation.PositiveOrZero.message=must be greater than or equal to 0
|
||||
org.apache.fineract.common.validation.Size.message=size must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.CreditCardNumber.message=invalid credit card number
|
||||
org.apache.fineract.common.validation.Currency.message=invalid currency (must be one of {value})
|
||||
org.apache.fineract.common.validation.EAN.message=invalid {type} barcode
|
||||
org.apache.fineract.common.validation.ISBN.message=invalid ISBN
|
||||
org.apache.fineract.common.validation.Length.message=length must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.CodePointLength.message=length must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.LuhnCheck.message=the check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
|
||||
org.apache.fineract.common.validation.Mod10Check.message=the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
|
||||
org.apache.fineract.common.validation.Mod11Check.message=the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
|
||||
org.apache.fineract.common.validation.ModCheck.message=the check digit for ${validatedValue} is invalid, {modType} checksum failed
|
||||
org.apache.fineract.common.validation.Normalized.message=must be normalized
|
||||
org.apache.fineract.common.validation.ParametersScriptAssert.message=script expression "{script}" didn't evaluate to true
|
||||
org.apache.fineract.common.validation.Range.message=must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.ScriptAssert.message=script expression "{script}" didn't evaluate to true
|
||||
org.apache.fineract.common.validation.UniqueElements.message=must only contain unique elements
|
||||
org.apache.fineract.common.validation.URL.message=must be a valid URL
|
||||
org.apache.fineract.common.validation.UUID.message=must be a valid UUID
|
||||
org.apache.fineract.common.validation.br.CNPJ.message=invalid Brazilian corporate taxpayer registry number (CNPJ)
|
||||
org.apache.fineract.common.validation.br.CPF.message=invalid Brazilian individual taxpayer registry number (CPF)
|
||||
org.apache.fineract.common.validation.br.TituloEleitoral.message=invalid Brazilian Voter ID card number
|
||||
org.apache.fineract.common.validation.pl.REGON.message=invalid Polish Taxpayer Identification Number (REGON)
|
||||
org.apache.fineract.common.validation.pl.NIP.message=invalid VAT Identification Number (NIP)
|
||||
org.apache.fineract.common.validation.pl.PESEL.message=invalid Polish National Identification Number (PESEL)
|
||||
org.apache.fineract.common.validation.ru.INN.message=invalid Russian taxpayer identification number (INN)
|
||||
org.apache.fineract.common.validation.time.DurationMax.message=must be shorter than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
|
||||
org.apache.fineract.common.validation.time.DurationMin.message=must be longer than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
|
||||
# dummy
|
||||
org.apache.fineract.dummy.request.content.not-empty=Dummy request content must have a value
|
||||
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
# common
|
||||
org.apache.fineract.common.validation.error=Fehler
|
||||
org.apache.fineract.common.validation.AssertFalse.message=muss falsch sein
|
||||
org.apache.fineract.common.validation.AssertTrue.message=muss wahr sein
|
||||
org.apache.fineract.common.validation.DecimalMax.message=muss kleiner ${inclusive == true ? 'oder gleich ' : ''}{value} sein
|
||||
org.apache.fineract.common.validation.DecimalMin.message=muss gr\u00f6\u00dfer ${inclusive == true ? 'oder gleich ' : ''}{value} sein
|
||||
org.apache.fineract.common.validation.Digits.message=numerischer Wert au\u00dferhalb des g\u00fcltigen Bereichs (<{integer} digits>.<{fraction} digits> erwartet)
|
||||
org.apache.fineract.common.validation.Email.message=muss eine korrekt formatierte E-Mail-Adresse sein
|
||||
org.apache.fineract.common.validation.Future.message=muss ein Datum in der Zukunft sein
|
||||
org.apache.fineract.common.validation.FutureOrPresent.message=muss ein Datum in der Gegenwart oder in der Zukunft sein
|
||||
org.apache.fineract.common.validation.Max.message=muss kleiner-gleich {value} sein
|
||||
org.apache.fineract.common.validation.Min.message=muss gr\u00f6\u00dfer-gleich {value} sein
|
||||
org.apache.fineract.common.validation.Negative.message=muss kleiner als 0 sein
|
||||
org.apache.fineract.common.validation.NegativeOrZero.message=muss kleiner-gleich 0 sein
|
||||
org.apache.fineract.common.validation.NotBlank.message=darf nicht leer sein
|
||||
org.apache.fineract.common.validation.NotEmpty.message=darf nicht leer sein
|
||||
org.apache.fineract.common.validation.NotNull.message=darf nicht null sein
|
||||
org.apache.fineract.common.validation.Null.message=muss null sein
|
||||
org.apache.fineract.common.validation.Past.message=muss ein Datum in der Vergangenheit sein
|
||||
org.apache.fineract.common.validation.PastOrPresent.message=muss ein Datum in der Vergangenheit oder in der Gegenwart sein
|
||||
org.apache.fineract.common.validation.Pattern.message=muss mit "{regexp}" \u00fcbereinstimmen
|
||||
org.apache.fineract.common.validation.Positive.message=muss gr\u00f6\u00dfer als 0 sein
|
||||
org.apache.fineract.common.validation.PositiveOrZero.message=muss gr\u00f6\u00dfer-gleich 0 sein
|
||||
org.apache.fineract.common.validation.Size.message=Gr\u00f6\u00dfe muss zwischen {min} und {max} sein
|
||||
org.apache.fineract.common.validation.CreditCardNumber.message=ung\u00fcltige Kreditkartennummer
|
||||
org.apache.fineract.common.validation.Currency.message=ung\u00fcltige W\u00e4hrung (muss eine der folgenden sein: {value})
|
||||
org.apache.fineract.common.validation.EAN.message=ung\u00fcltiger {type}-Barcode
|
||||
org.apache.fineract.common.validation.ISBN.message=ung\u00fcltige ISBN
|
||||
org.apache.fineract.common.validation.Length.message=L\u00e4nge muss zwischen {min} und {max} sein
|
||||
org.apache.fineract.common.validation.CodePointLength.message=L\u00e4nge muss zwischen {min} und {max} sein
|
||||
org.apache.fineract.common.validation.LuhnCheck.message=die Pr\u00fcfziffer f\u00fcr ${validatedValue} ist ung\u00fcltig, Luhn Modulo 10-Kontrollsumme ist fehlgeschlagen
|
||||
org.apache.fineract.common.validation.Mod10Check.message=die Pr\u00fcfziffer f\u00fcr ${validatedValue} ist ung\u00fcltig, Modulo 10-Kontrollsumme ist fehlgeschlagen
|
||||
org.apache.fineract.common.validation.Mod11Check.message=die Pr\u00fcfziffer f\u00fcr ${validatedValue} ist ung\u00fcltig, Modulo 11-Kontrollsumme ist fehlgeschlagen
|
||||
org.apache.fineract.common.validation.ModCheck.message=die Pr\u00fcfziffer f\u00fcr ${validatedValue} ist ung\u00fcltig, {modType}-Kontrollsumme ist fehlgeschlagen
|
||||
org.apache.fineract.common.validation.ParametersScriptAssert.message=die Evaluierung des Scriptausdrucks "{script}" ergab nicht true
|
||||
org.apache.fineract.common.validation.Range.message=muss zwischen {min} und {max} sein
|
||||
org.apache.fineract.common.validation.ScriptAssert.message=die Evaluierung des Scriptausdrucks "{script}" ergab nicht true
|
||||
org.apache.fineract.common.validation.UniqueElements.message=darf nur eindeutige Elemente enthalten
|
||||
org.apache.fineract.common.validation.URL.message=muss eine g\u00fcltige URL sein
|
||||
org.apache.fineract.common.validation.UUID.message=muss eine g\u00fcltige UUID sein
|
||||
org.apache.fineract.common.validation.br.CNPJ.message=ung\u00fcltige brasilianische Registriernummer f\u00fcr K\u00f6rperschaftssteuerzahlungen (CNPJ)
|
||||
org.apache.fineract.common.validation.br.CPF.message=ung\u00fcltige brasilianische Registriernummer f\u00fcr Steuerzahlungen nat\u00fcrlicher Personen (CPF)
|
||||
org.apache.fineract.common.validation.br.TituloEleitoral.message=ung\u00fcltige brasilianische ID-Karte f\u00fcr Entscheidungsberechtigte
|
||||
org.apache.fineract.common.validation.pl.REGON.message=ung\u00fcltige polnische Steuerzahleridentifikationsnummer (REGON)
|
||||
org.apache.fineract.common.validation.pl.NIP.message=ung\u00fcltige Mehrwertsteueridentifikationsnummer (NIP)
|
||||
org.apache.fineract.common.validation.pl.PESEL.message=ung\u00fcltige polnische nationale Identifikationsnummer (PESEL)
|
||||
org.apache.fineract.common.validation.ru.INN.message=ung\u00fcltige russisch Steuerzahleridentifikationsnummer (INN)
|
||||
org.apache.fineract.common.validation.time.DurationMax.message=muss k\u00fcrzer sein als${inclusive == true ? ' oder gleich' : ''}${days == 0 ? '' : days == 1 ? ' 1 Tag' : ' ' += days += ' Tage'}${hours == 0 ? '' : hours == 1 ? ' 1 Stunde' : ' ' += hours += ' Stunden'}${minutes == 0 ? '' : minutes == 1 ? ' 1 Minute' : ' ' += minutes += ' Minuten'}${seconds == 0 ? '' : seconds == 1 ? ' 1 Sekunde' : ' ' += seconds += ' Sekunden'}${millis == 0 ? '' : millis == 1 ? ' 1 Millisekunde' : ' ' += millis += ' Millisekunden'}${nanos == 0 ? '' : nanos == 1 ? ' 1 Nanosekunde' : ' ' += nanos += ' Nanosekunden'}
|
||||
org.apache.fineract.common.validation.time.DurationMin.message=muss gr\u00f6\u00dfer sein als${inclusive == true ? ' oder gleich' : ''}${days == 0 ? '' : days == 1 ? ' 1 Tag' : ' ' += days += ' Tage'}${hours == 0 ? '' : hours == 1 ? ' 1 Stunde' : ' ' += hours += ' Stunden'}${minutes == 0 ? '' : minutes == 1 ? ' 1 Minute' : ' ' += minutes += ' Minuten'}${seconds == 0 ? '' : seconds == 1 ? ' 1 Sekunde' : ' ' += seconds += ' Sekunden'}${millis == 0 ? '' : millis == 1 ? ' 1 Millisekunde' : ' ' += millis += ' Millisekunden'}${nanos == 0 ? '' : nanos == 1 ? ' 1 Nanosekunde' : ' ' += nanos += ' Nanosekunden'}
|
||||
# dummy
|
||||
org.apache.fineract.dummy.request.content.not-empty=Dummy Request Attribut muss einen Wert enthalten
|
||||
@@ -0,0 +1,71 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
# common
|
||||
org.apache.fineract.common.validation.error=Validation Error
|
||||
org.apache.fineract.common.validation.AssertFalse.message=must be false
|
||||
org.apache.fineract.common.validation.AssertTrue.message=must be true
|
||||
org.apache.fineract.common.validation.DecimalMax.message=must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
|
||||
org.apache.fineract.common.validation.DecimalMin.message=must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
|
||||
org.apache.fineract.common.validation.Digits.message=numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
|
||||
org.apache.fineract.common.validation.Email.message=must be a well-formed email address
|
||||
org.apache.fineract.common.validation.Future.message=must be a future date
|
||||
org.apache.fineract.common.validation.FutureOrPresent.message=must be a date in the present or in the future
|
||||
org.apache.fineract.common.validation.Max.message=must be less than or equal to {value}
|
||||
org.apache.fineract.common.validation.Min.message=must be greater than or equal to {value}
|
||||
org.apache.fineract.common.validation.Negative.message=must be less than 0
|
||||
org.apache.fineract.common.validation.NegativeOrZero.message=must be less than or equal to 0
|
||||
org.apache.fineract.common.validation.NotBlank.message=must not be blank
|
||||
org.apache.fineract.common.validation.NotEmpty.message=must not be empty
|
||||
org.apache.fineract.common.validation.NotNull.message=must not be null
|
||||
org.apache.fineract.common.validation.Null.message=must be null
|
||||
org.apache.fineract.common.validation.Past.message=must be a past date
|
||||
org.apache.fineract.common.validation.PastOrPresent.message=must be a date in the past or in the present
|
||||
org.apache.fineract.common.validation.Pattern.message=must match "{regexp}"
|
||||
org.apache.fineract.common.validation.Positive.message=must be greater than 0
|
||||
org.apache.fineract.common.validation.PositiveOrZero.message=must be greater than or equal to 0
|
||||
org.apache.fineract.common.validation.Size.message=size must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.CreditCardNumber.message=invalid credit card number
|
||||
org.apache.fineract.common.validation.Currency.message=invalid currency (must be one of {value})
|
||||
org.apache.fineract.common.validation.EAN.message=invalid {type} barcode
|
||||
org.apache.fineract.common.validation.ISBN.message=invalid ISBN
|
||||
org.apache.fineract.common.validation.Length.message=length must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.CodePointLength.message=length must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.LuhnCheck.message=the check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
|
||||
org.apache.fineract.common.validation.Mod10Check.message=the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
|
||||
org.apache.fineract.common.validation.Mod11Check.message=the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
|
||||
org.apache.fineract.common.validation.ModCheck.message=the check digit for ${validatedValue} is invalid, {modType} checksum failed
|
||||
org.apache.fineract.common.validation.Normalized.message=must be normalized
|
||||
org.apache.fineract.common.validation.ParametersScriptAssert.message=script expression "{script}" didn't evaluate to true
|
||||
org.apache.fineract.common.validation.Range.message=must be between {min} and {max}
|
||||
org.apache.fineract.common.validation.ScriptAssert.message=script expression "{script}" didn't evaluate to true
|
||||
org.apache.fineract.common.validation.UniqueElements.message=must only contain unique elements
|
||||
org.apache.fineract.common.validation.URL.message=must be a valid URL
|
||||
org.apache.fineract.common.validation.UUID.message=must be a valid UUID
|
||||
org.apache.fineract.common.validation.br.CNPJ.message=invalid Brazilian corporate taxpayer registry number (CNPJ)
|
||||
org.apache.fineract.common.validation.br.CPF.message=invalid Brazilian individual taxpayer registry number (CPF)
|
||||
org.apache.fineract.common.validation.br.TituloEleitoral.message=invalid Brazilian Voter ID card number
|
||||
org.apache.fineract.common.validation.pl.REGON.message=invalid Polish Taxpayer Identification Number (REGON)
|
||||
org.apache.fineract.common.validation.pl.NIP.message=invalid VAT Identification Number (NIP)
|
||||
org.apache.fineract.common.validation.pl.PESEL.message=invalid Polish National Identification Number (PESEL)
|
||||
org.apache.fineract.common.validation.ru.INN.message=invalid Russian taxpayer identification number (INN)
|
||||
org.apache.fineract.common.validation.time.DurationMax.message=must be shorter than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
|
||||
org.apache.fineract.common.validation.time.DurationMin.message=must be longer than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
|
||||
# dummy
|
||||
org.apache.fineract.dummy.request.content.not-empty=Dummy request content must have a value
|
||||
+3
-9
@@ -20,23 +20,17 @@ package org.apache.fineract.infrastructure.core.domain;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
|
||||
public final class BatchRequestContextHolder {
|
||||
|
||||
private BatchRequestContextHolder() {}
|
||||
|
||||
private static final ThreadLocal<Map<String, Object>> batchAttributes = new NamedThreadLocal<>("batchAttributes");
|
||||
private static final ThreadLocal<Map<String, Object>> batchAttributes = new ThreadLocal<>();
|
||||
|
||||
private static final ThreadLocal<Optional<TransactionStatus>> batchTransaction = new NamedThreadLocal<>("batchTransaction") {
|
||||
private static final ThreadLocal<Optional<TransactionStatus>> batchTransaction = ThreadLocal.withInitial(Optional::empty);
|
||||
|
||||
@Override
|
||||
protected Optional<TransactionStatus> initialValue() {
|
||||
return Optional.empty();
|
||||
}
|
||||
};
|
||||
private static final ThreadLocal<Boolean> isEnclosingTransaction = new NamedThreadLocal<>("isEnclosingTransaction");
|
||||
private static final ThreadLocal<Boolean> isEnclosingTransaction = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* True if the batch attributes are set
|
||||
|
||||
@@ -168,7 +168,7 @@ configurations {
|
||||
dependencies {
|
||||
driver 'org.mariadb.jdbc:mariadb-java-client'
|
||||
driver 'org.postgresql:postgresql'
|
||||
driver 'mysql:mysql-connector-java:8.0.33'
|
||||
driver 'com.mysql:mysql-connector-j'
|
||||
}
|
||||
|
||||
URLClassLoader loader = GroovyObject.class.classLoader
|
||||
|
||||
@@ -36,6 +36,7 @@ dependencies {
|
||||
implementation(project(path: ':fineract-progressive-loan'))
|
||||
implementation(project(path: ':fineract-report'))
|
||||
implementation(project(path: ':fineract-tax'))
|
||||
implementation(project(path: ':fineract-command'))
|
||||
|
||||
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
|
||||
|
||||
|
||||
+2
-1
@@ -58,7 +58,8 @@ import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
@EnableJpaRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.repository" })
|
||||
@EnableJpaRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.repository",
|
||||
"org.apache.fineract.command.persistence" })
|
||||
@EnableConfigurationProperties(JpaProperties.class)
|
||||
@Import(JpaAuditingHandlerRegistrar.class)
|
||||
public class JPAConfig extends JpaBaseConfiguration {
|
||||
|
||||
@@ -460,3 +460,8 @@ resilience4j.retry.instances.postInterest.wait-duration=${FINERACT_PROCESS_POST_
|
||||
resilience4j.retry.instances.postInterest.enable-exponential-backoff=${FINERACT_PROCESS_POST_INTEREST_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
|
||||
|
||||
fineract.command.enabled=true
|
||||
fineract.command.executor=sync
|
||||
fineract.command.ring-buffer-size=1024
|
||||
fineract.command.producer-type=single
|
||||
|
||||
@@ -65,6 +65,7 @@ import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -86,6 +87,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ScheduledJobRunnerConfig.class) })
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@PropertySource("classpath:application-test.properties")
|
||||
public class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -285,3 +285,8 @@ fineract.insecure-http-client=true
|
||||
fineract.client-connect-timeout=30
|
||||
fineract.client-read-timeout=30
|
||||
fineract.client-write-timeout=30
|
||||
|
||||
fineract.command.enabled=true
|
||||
fineract.command.executor=sync
|
||||
fineract.command.ring-buffer-size=1024
|
||||
fineract.command.producer-type=single
|
||||
|
||||
@@ -34,7 +34,7 @@ configurations {
|
||||
driver
|
||||
}
|
||||
dependencies {
|
||||
driver 'mysql:mysql-connector-java:8.0.33'
|
||||
driver 'com.mysql:mysql-connector-j'
|
||||
}
|
||||
|
||||
cargo {
|
||||
|
||||
@@ -34,7 +34,7 @@ configurations {
|
||||
driver
|
||||
}
|
||||
dependencies {
|
||||
driver 'mysql:mysql-connector-java:8.0.33'
|
||||
driver 'com.mysql:mysql-connector-j'
|
||||
}
|
||||
|
||||
cargo {
|
||||
|
||||
@@ -48,6 +48,7 @@ buildCache {
|
||||
|
||||
rootProject.name='fineract'
|
||||
include ':fineract-core'
|
||||
include ':fineract-command'
|
||||
include ':fineract-accounting'
|
||||
include ':fineract-provider'
|
||||
include ':fineract-branch'
|
||||
|
||||
@@ -34,7 +34,7 @@ configurations {
|
||||
driver
|
||||
}
|
||||
dependencies {
|
||||
driver 'mysql:mysql-connector-java:8.0.33'
|
||||
driver 'com.mysql:mysql-connector-j'
|
||||
}
|
||||
|
||||
cargo {
|
||||
|
||||
Reference in New Issue
Block a user