FINERACT-2169: New command processing infrastructure

This commit is contained in:
Aleksandar Vidakovic
2025-04-22 21:50:28 +02:00
committed by Aleksandar Vidakovic
parent fd45f43f36
commit 8d83b5547c
69 changed files with 3069 additions and 16 deletions
@@ -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");
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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> {}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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());
}
}
@@ -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());
}
}
}
@@ -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());
}
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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