diff --git a/.gitignore b/.gitignore index 524f096..f480e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,16 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* +api/target/ +api/src/main/java/META-INF/ +/api/src/main/resources/application-local.properties + + +# IDE +.idea +*.iml +api/src/main/resources/*.http +api/*.iml +.envrc +shell.nix +Session.vim diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2291722 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM artifacts.developer.gov.bc.ca/docker-remote/maven:3.8.1-openjdk-21-slim AS build +WORKDIR /workspace/app + +COPY api/pom.xml . +COPY api/src src +RUN mvn package -DskipTests +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM artifacts.developer.gov.bc.ca/docker-remote/openjdk:21.0.0-jdk-oracle +RUN useradd -ms /bin/bash spring +RUN mkdir -p /logs +RUN chown -R spring:spring /logs +RUN chmod 755 /logs +USER spring +VOLUME /tmp +ARG DEPENDENCY=/workspace/app/target/dependency +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app +ENTRYPOINT ["java","-Duser.name=EAS-API","-Xms600m","-Xmx800m","-noverify","-XX:TieredStopAtLevel=1","-XX:+UseParallelGC","-XX:MinHeapFreeRatio=20","-XX:MaxHeapFreeRatio=40","-XX:GCTimeRatio=4","-XX:AdaptiveSizePolicyWeight=90","-XX:MaxMetaspaceSize=300m","-XX:ParallelGCThreads=2","-Djava.util.concurrent.ForkJoinPool.common.parallelism=8","-XX:CICompilerCount=2","-XX:+ExitOnOutOfMemoryError","-Dspring.profiles.active=openshift","-Djava.security.egd=file:/dev/./urandom","-cp","app:app/lib/*","ca.bc.gov.educ.eas.api.EasApiApplication"] diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..135408b --- /dev/null +++ b/api/README.md @@ -0,0 +1,14 @@ +# EDUC-EAS-API +## Build Setup + +``` bash +#Prepare to run +- Connect to VPN +- start up nats-server using `docker run -d --name=nats-main -p 4222:4222 -p 6222:6222 -p 8222:8222 nats -js` + +#Run application with local properties +mvn clean install -Dspring.profiles.active=dev + +#Run application with default properties +mvn clean install + diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..f8eedbc --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,406 @@ + + + 4.0.0 + + ca.bc.gov.educ.eas + eas-api + 0.0.1-SNAPSHOT + eas-api + EAS API + + + java + + src/test/**, + src/main/resources/**, + src/main/java/ca/bc/gov/educ/eas/api/endpoint/**, + src/main/java/ca/bc/gov/educ/eas/api/config/**, + src/main/java/ca/bc/gov/educ/eas/api/mappers/**, + src/main/java/ca/bc/gov/educ/eas/api/exception/**, + src/main/java/ca/bc/gov/educ/eas/api/model/**, + src/main/java/ca/bc/gov/educ/eas/api/struct/** + + 18 + + 3.10.1 + ${java.version} + ${java.version} + 1.5.3.Final + 4.20.0 + 1.6.9 + 2.11.0 + 21.3.0.0 + 33.2.1-jre + 4.0.4 + 0.15 + 4.0.3 + 3.1.6 + 2.17.1 + + + + org.springframework.boot + spring-boot-starter-parent + 3.0.13 + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.flywaydb + flyway-core + + + com.oracle.database.jdbc + ojdbc11 + ${ojdbc.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-to-slf4j + ${log4j.version} + + + com.github.albfernandez + juniversalchardet + 2.4.0 + + + org.apache.commons + commons-lang3 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + ${lombok.version} + true + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.2 + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + io.micrometer + micrometer-registry-prometheus + + + net.sf.jasperreports + jasperreports + 6.21.0 + + + net.sf.jasperreports + jasperreports-fonts + 6.21.0 + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-core + + + com.h2database + h2 + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.14.1 + + + org.springframework.security + spring-security-test + test + + + org.springframework + spring-context-indexer + true + + + + net.javacrumbs.shedlock + shedlock-spring + ${shedlock.version} + + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + ${shedlock.version} + + + org.springframework.retry + spring-retry + + + net.sf.flatpack + flatpack + ${flatpack.version} + + + com.github.javafaker + javafaker + ${faker.varion} + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + org.awaitility + awaitility-proxy + ${awaitility-proxy.version} + test + + + + pl.pragmatists + JUnitParams + 1.1.1 + test + + + io.nats + jnats + ${nats.version} + + + com.google.guava + guava + ${guava.version} + + + commons-io + commons-io + 2.16.1 + + + org.apache.commons + commons-csv + 1.11.0 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.6.0.1398 + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + org.hibernate.orm.tooling + hibernate-enhance-maven-plugin + ${hibernate.version} + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + true + false + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + + + + + central + https://repo1.maven.org/maven2 + + + localrepository + file://${project.basedir}/src/main/resources/bcsans.jar + + + + + coverage + + true + + + + + maven-compiler-plugin + ${maven.compiler.version} + + ${java.version} + ${java.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.springframework + spring-context-indexer + ${spring-framework.version} + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-tests + + integration-test + verify + + + + + + org.hibernate.orm.tooling + hibernate-enhance-maven-plugin + ${hibernate.version} + + + + true + true + + + enhance + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + coverage-report + prepare-package + + report + + + + + + + + + diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/EasApiApplication.java b/api/src/main/java/ca/bc/gov/educ/eas/api/EasApiApplication.java new file mode 100644 index 0000000..2f7b2df --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/EasApiApplication.java @@ -0,0 +1,102 @@ +package ca.bc.gov.educ.eas.api; + +import lombok.val; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * The EAS api application. + * + */ +@SpringBootApplication +@EnableCaching +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "1s") +@EnableRetry +public class EasApiApplication { + + /** + * The entry point of application. + * + * @param args the input arguments + */ + public static void main(final String[] args) { + SpringApplication.run(EasApiApplication.class, args); + } + + /** + * Lock provider For distributed lock, to avoid multiple pods executing the same scheduled task. + * + * @param jdbcTemplate the jdbc template + * @param transactionManager the transaction manager + * @return the lock provider + */ + @Bean + public LockProvider lockProvider(@Autowired final JdbcTemplate jdbcTemplate, + @Autowired final PlatformTransactionManager transactionManager) { + return new JdbcTemplateLockProvider(jdbcTemplate, transactionManager, + "EAS_SHEDLOCK"); + } + + /** + * Thread pool task scheduler thread pool task scheduler. + * + * @return the thread pool task scheduler + */ + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + val threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(5); + return threadPoolTaskScheduler; + } + + /** + * The type Web security configuration. Add security exceptions for swagger UI and prometheus. + */ + @Configuration + @EnableMethodSecurity + static + class WebSecurityConfiguration { + + /** + * Instantiates a new Web security configuration. This makes sure that security context is + * propagated to async threads as well. + */ + public WebSecurityConfiguration() { + super(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/v3/api-docs/**", + "/actuator/health", "/actuator/prometheus", "/actuator/**", + "/swagger-ui/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + return http.build(); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/adapter/CustomRequestBodyAdviceAdapter.java b/api/src/main/java/ca/bc/gov/educ/eas/api/adapter/CustomRequestBodyAdviceAdapter.java new file mode 100644 index 0000000..bee0a07 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/adapter/CustomRequestBodyAdviceAdapter.java @@ -0,0 +1,34 @@ +package ca.bc.gov.educ.eas.api.adapter; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; + +import java.lang.reflect.Type; + +@ControllerAdvice +public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter { + + HttpServletRequest httpServletRequest; + + @Autowired + public void setHttpServletRequest(final HttpServletRequest httpServletRequest) { + this.httpServletRequest = httpServletRequest; + } + + @Override + public boolean supports(final MethodParameter methodParameter, final Type type, final Class> aClass) { + return true; + } + + @Override + public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter parameter, final Type targetType, + final Class> converterType) { + this.httpServletRequest.setAttribute("payload", body); + return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/config/AsyncConfiguration.java b/api/src/main/java/ca/bc/gov/educ/eas/api/config/AsyncConfiguration.java new file mode 100644 index 0000000..1a380ce --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/config/AsyncConfiguration.java @@ -0,0 +1,73 @@ +package ca.bc.gov.educ.eas.api.config; + +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import ca.bc.gov.educ.eas.api.util.ThreadFactoryBuilder; +import org.jboss.threads.EnhancedQueueExecutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableAsync; + +import java.time.Duration; +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +@Profile("!test") +public class AsyncConfiguration { + /** + * Thread pool task executor executor. + * + * @return the executor + */ + @Bean(name = "subscriberExecutor") + @Autowired + public Executor threadPoolTaskExecutor(final ApplicationProperties applicationProperties) { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("message-subscriber-%d").get()) + .setCorePoolSize(applicationProperties.getMinSubscriberThreads()).setMaximumPoolSize(applicationProperties.getMaxSubscriberThreads()).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "sagaRetryTaskExecutor") + public Executor sagaRetryTaskExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("async-saga-retry-executor-%d").get()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "processLoadedStudentsTaskExecutor") + public Executor processLoadedStudentsTaskExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("async-loaded-stud-executor-%d").get()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "processUncompletedSagasTaskExecutor") + public Executor processUncompletedSagasTaskExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("async-uncompleted-saga-executor-%d").get()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "findSchoolCollectionsForSubmissionTaskExecutor") + public Executor findSchoolCollectionsForSubmissionTaskExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("async-school-collections-executor-%d").get()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "findAllUnsubmittedIndependentSchoolsTaskExecutor") + public Executor findAllUnsubmittedIndependentSchoolsTaskExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().withNameFormat("async-unsubmitted-indies-executor-%d").get()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } + + @Bean(name = "publisherExecutor") + public Executor publisherExecutor() { + return new EnhancedQueueExecutor.Builder() + .setThreadFactory(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("message-publisher-%d").build()) + .setCorePoolSize(5).setMaximumPoolSize(10).setKeepAliveTime(Duration.ofSeconds(60)).build(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/config/EasAPIMVCConfig.java b/api/src/main/java/ca/bc/gov/educ/eas/api/config/EasAPIMVCConfig.java new file mode 100644 index 0000000..d1e0a89 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/config/EasAPIMVCConfig.java @@ -0,0 +1,43 @@ +package ca.bc.gov.educ.eas.api.config; + +import lombok.AccessLevel; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * The type Pen reg api mvc config. + * + * @author Om + */ +@Configuration +public class EasAPIMVCConfig implements WebMvcConfigurer { + + /** + * The Pen reg api interceptor. + */ + @Getter(AccessLevel.PRIVATE) + private final RequestResponseInterceptor requestResponseInterceptor; + + /** + * Instantiates a new Pen reg api mvc config. + * + * @param requestResponseInterceptor the pen reg api interceptor + */ + @Autowired + public EasAPIMVCConfig(final RequestResponseInterceptor requestResponseInterceptor) { + this.requestResponseInterceptor = requestResponseInterceptor; + } + + /** + * Add interceptors. + * + * @param registry the registry + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(requestResponseInterceptor).addPathPatterns("/**"); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/config/RequestResponseInterceptor.java b/api/src/main/java/ca/bc/gov/educ/eas/api/config/RequestResponseInterceptor.java new file mode 100644 index 0000000..a77d6a6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/config/RequestResponseInterceptor.java @@ -0,0 +1,47 @@ +package ca.bc.gov.educ.eas.api.config; + +import ca.bc.gov.educ.eas.api.helpers.LogHelper; +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + +import java.time.Instant; + +@Component +@Slf4j +public class RequestResponseInterceptor implements AsyncHandlerInterceptor { + + @Override + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { + // for async this is called twice so need a check to avoid setting twice. + if (request.getAttribute("startTime") == null) { + final long startTime = Instant.now().toEpochMilli(); + request.setAttribute("startTime", startTime); + } + return true; + } + + /** + * After completion. + * + * @param request the request + * @param response the response + * @param handler the handler + * @param ex the ex + */ + @Override + public void afterCompletion(@NonNull final HttpServletRequest request, final HttpServletResponse response, @NonNull final Object handler, final Exception ex) { + LogHelper.logServerHttpReqResponseDetails(request, response); + val correlationID = request.getHeader(ApplicationProperties.CORRELATION_ID); + if (correlationID != null) { + response.setHeader(ApplicationProperties.CORRELATION_ID, request.getHeader(ApplicationProperties.CORRELATION_ID)); + } + } + + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/config/TemplateEngineConfig.java b/api/src/main/java/ca/bc/gov/educ/eas/api/config/TemplateEngineConfig.java new file mode 100644 index 0000000..7ae30dc --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/config/TemplateEngineConfig.java @@ -0,0 +1,34 @@ +package ca.bc.gov.educ.eas.api.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class TemplateEngineConfig { + @Bean + public SpringTemplateEngine emailTemplateEngine() { + final SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(htmlTemplateResolver()); + return templateEngine; + } + + private ITemplateResolver htmlTemplateResolver() { + final var templateResolver = new StringTemplateResolver(); + templateResolver.setTemplateMode(TemplateMode.HTML); + return templateResolver; + } + + @Bean + @ConfigurationProperties(prefix = "email.template") + public Map templateConfig() { + return new HashMap<>(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventOutcome.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventOutcome.java new file mode 100644 index 0000000..73a1349 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventOutcome.java @@ -0,0 +1,9 @@ +package ca.bc.gov.educ.eas.api.constants; + +/** + * The enum Event outcome. + */ +public enum EventOutcome { + INITIATE_SUCCESS, + SAGA_COMPLETED, +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventStatus.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventStatus.java new file mode 100644 index 0000000..406e074 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventStatus.java @@ -0,0 +1,15 @@ +package ca.bc.gov.educ.eas.api.constants; + +/** + * The enum Event status. + */ +public enum EventStatus { + /** + * Db committed event status. + */ + DB_COMMITTED, + /** + * Message published event status. + */ + MESSAGE_PUBLISHED +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java new file mode 100644 index 0000000..d170674 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java @@ -0,0 +1,11 @@ +package ca.bc.gov.educ.eas.api.constants; + +/** + * The enum Event type. + */ +public enum EventType { + READ_FROM_TOPIC, + INITIATED, + MARK_SAGA_COMPLETE, + GET_PAGINATED_SCHOOLS +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaEnum.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaEnum.java new file mode 100644 index 0000000..9779801 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaEnum.java @@ -0,0 +1,5 @@ +package ca.bc.gov.educ.eas.api.constants; + +public enum SagaEnum { + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaLogMessages.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaLogMessages.java new file mode 100644 index 0000000..928378d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaLogMessages.java @@ -0,0 +1,25 @@ +package ca.bc.gov.educ.eas.api.constants; + +import lombok.Getter; + +public enum SagaLogMessages { + NO_RECORD_SAGA_ID_EVENT_TYPE("no record found for the saga id and event type combination, processing."), + RECORD_FOUND_FOR_SAGA_ID_EVENT_TYPE("record found for the saga id and event type combination, might be a duplicate or replay," + + " just updating the db status so that it will be polled and sent back again."), + EVENT_PAYLOAD("event is :: {}"); + + /** + * The Message. + */ + @Getter + private final String message; + + /** + * Instantiates a new sage log message. + * + * @param message the message + */ + SagaLogMessages(String message) { + this.message = message; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaStatusEnum.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaStatusEnum.java new file mode 100644 index 0000000..1a8c9dd --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/SagaStatusEnum.java @@ -0,0 +1,23 @@ +package ca.bc.gov.educ.eas.api.constants; + +/** + * The enum Saga status enum. + */ +public enum SagaStatusEnum{ + /** + * Started saga status enum. + */ + STARTED, + /** + * In progress saga status enum. + */ + IN_PROGRESS, + /** + * Completed saga status enum. + */ + COMPLETED, + /** + * Force stopped saga status enum. + */ + FORCE_STOPPED +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java new file mode 100644 index 0000000..4d44455 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java @@ -0,0 +1,8 @@ +package ca.bc.gov.educ.eas.api.constants; + + +public enum TopicsEnum { + EAS_API_TOPIC, + INSTITUTE_API_TOPIC, + EAS_EVENTS_TOPIC, +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/FacilityTypeCodes.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/FacilityTypeCodes.java new file mode 100644 index 0000000..d551a4c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/FacilityTypeCodes.java @@ -0,0 +1,43 @@ +package ca.bc.gov.educ.eas.api.constants.v1; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * The enum for school's facility type codes + */ +@Getter +public enum FacilityTypeCodes { + PROVINCIAL("PROVINCIAL"), + DIST_CONT("DIST_CONT"), + ELEC_DELIV("ELEC_DELIV"), + STANDARD("STANDARD"), + CONT_ED("CONT_ED"), + DIST_LEARN("DIST_LEARN"), + ALT_PROGS("ALT_PROGS"), + STRONG_CEN("STRONG_CEN"), + STRONG_OUT("STRONG_OUT"), + SHORT_PRP("SHORT_PRP"), + LONG_PRP("LONG_PRP"), + SUMMER("SUMMER"), + YOUTH("YOUTH"), + DISTONLINE("DISTONLINE"), + POST_SEC("POST_SEC"), + JUSTB4PRO("JUSTB4PRO"); + + private final String code; + FacilityTypeCodes(String code) { this.code = code; } + + public static String[] getFacilityCodesWithoutOLAndCE(){ + return new String[]{ALT_PROGS.getCode(), JUSTB4PRO.getCode(), LONG_PRP.getCode(), POST_SEC.getCode(), SHORT_PRP.getCode(), STANDARD.getCode(), STRONG_CEN.getCode(), STRONG_OUT.getCode(), SUMMER.getCode(), YOUTH.getCode()}; + } + + public static List getOnlineFacilityTypeCodes() { + List codes = new ArrayList<>(); + codes.add(DIST_LEARN.getCode()); + codes.add(DISTONLINE.getCode()); + return codes; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolCategoryCodes.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolCategoryCodes.java new file mode 100644 index 0000000..06d9e30 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolCategoryCodes.java @@ -0,0 +1,34 @@ +package ca.bc.gov.educ.eas.api.constants.v1; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * The enum for school category codes + */ +@Getter +public enum SchoolCategoryCodes { + IMM_DATA("IMM_DATA"), + CHILD_CARE("CHILD_CARE"), + MISC("MISC"), + PUBLIC("PUBLIC"), + INDEPEND("INDEPEND"), + FED_BAND("FED_BAND"), + OFFSHORE("OFFSHORE"), + EAR_LEARN("EAR_LEARN"), + YUKON("YUKON"), + POST_SEC("POST_SEC"), + INDP_FNS("INDP_FNS"); + + private final String code; + public static final Set INDEPENDENTS = new HashSet<>(Arrays.asList(INDEPEND.getCode(), INDP_FNS.getCode())); + public static final Set INDEPENDENTS_AND_OFFSHORE = new HashSet<>(Arrays.asList(INDEPEND.getCode(), INDP_FNS.getCode(), OFFSHORE.getCode())); + SchoolCategoryCodes(String code) { this.code = code; } + + public static String[] getActiveSchoolCategoryCodes(){ + return new String[]{EAR_LEARN.getCode(), FED_BAND.getCode(), INDEPEND.getCode(), INDP_FNS.getCode(), OFFSHORE.getCode(), POST_SEC.getCode(), PUBLIC.getCode(), YUKON.getCode()}; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolFundingCodes.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolFundingCodes.java new file mode 100644 index 0000000..d73b053 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolFundingCodes.java @@ -0,0 +1,29 @@ +package ca.bc.gov.educ.eas.api.constants.v1; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public enum SchoolFundingCodes { + EDUC_SERVICE_CHILDREN("05"), + OUT_OF_PROVINCE("14"), + NEWCOMER_REFUGEE("16"), + STATUS_FIRST_NATION("20"); + + @Getter + private final String code; + SchoolFundingCodes(String code) { + this.code = code; + } + + public static Optional findByValue(String value) { + return Arrays.stream(values()).filter(e -> Arrays.asList(e.code).contains(value)).findFirst(); + } + + public static List getCodes() { + return Arrays.stream(SchoolFundingCodes.values()).map(SchoolFundingCodes::getCode).collect(Collectors.toList()); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolGradeCodes.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolGradeCodes.java new file mode 100644 index 0000000..60e4986 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/SchoolGradeCodes.java @@ -0,0 +1,311 @@ +package ca.bc.gov.educ.eas.api.constants.v1; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Getter +public enum SchoolGradeCodes { + KINDHALF("KINDHALF", "KH", 1), + KINDFULL("KINDFULL","KF", 2), + GRADE01("GRADE01","01", 3), + GRADE02("GRADE02","02", 4), + GRADE03("GRADE03","03", 5), + GRADE04("GRADE04","04", 6), + GRADE05("GRADE05","05", 7), + GRADE06("GRADE06","06", 8), + GRADE07("GRADE07","07", 9), + ELEMUNGR("ELEMUNGR","EU", 10), + GRADE08("GRADE08","08", 11), + GRADE09("GRADE09","09", 12), + GRADE10("GRADE10","10", 13), + GRADE11("GRADE11","11", 14), + GRADE12("GRADE12","12", 15), + SECONDARY_UNGRADED("SECUNGR","SU", 16), + GRADUATED_ADULT("GRADULT","GA", 17), + HOMESCHOOL("HOMESCL","HS", 18); + + @Getter + private final String code; + @Getter + private final String typeCode; + @Getter + private final int sequence; + SchoolGradeCodes(String typeCode, String code, int sequence) { + this.code = code; + this.typeCode = typeCode; + this.sequence = sequence; + } + + public static Optional findByValue(String value) { + return Arrays.stream(values()).filter(e -> Arrays.asList(e.code).contains(value)).findFirst(); + } + + public static Optional findByTypeCode(String typeCode) { + return Arrays.stream(values()).filter(e -> Arrays.asList(e.typeCode).contains(typeCode)).findFirst(); + } + + /** + * Get all grades HS, K-9, and EU + * @return - all grades HS, K-9, and EU + */ + public static List getDistrictFundingGrades() { + List codes = new ArrayList<>(); + codes.add(HOMESCHOOL.getCode()); + codes.addAll(getKToNineGrades()); + return codes; + } + + public static List getKfOneToSevenEuGrades() { + List codes = new ArrayList<>(); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + return codes; + } + + public static List getKToSevenEuGrades() { + List codes = new ArrayList<>(); + codes.add(KINDHALF.getCode()); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + return codes; + } + + public static List getKToNineGrades() { + List codes = new ArrayList<>(); + codes.add(KINDHALF.getCode()); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + return codes; + } + public static List get1To7Grades() { + List codes = new ArrayList<>(); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + return codes; + } + + public static List get8To12Grades() { + List codes = new ArrayList<>(); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + return codes; + } + + public static List get8PlusGrades() { + List codes = new ArrayList<>(); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + codes.add(GRADUATED_ADULT.getCode()); + return codes; + } + + public static List get8PlusGradesNoGA() { + List codes = new ArrayList<>(); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + return codes; + } + + public static List getHighSchoolGrades() { + return getGrades10toSU(); + } + + public static List getAllowedAdultGrades() { + List codes = new ArrayList<>(); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + codes.add(GRADUATED_ADULT.getCode()); + return codes; + } + + public static List getAllowedAdultGradesNonGraduate() { + return getGrades10toSU(); + } + + public static List getSummerSchoolGrades() { + List codes = new ArrayList<>(); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + return codes; + } + + public static List getSupportBlockGrades() { + return getGrades10toSU(); + } + + public static List getGrades10toSU() { + List codes = new ArrayList<>(); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + return codes; + } + + public static List getGrades8and9() { + List codes = new ArrayList<>(); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + return codes; + } + + public static List getAllSchoolGrades() { + List codes = new ArrayList<>(); + codes.add(KINDHALF.getCode()); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + codes.add(GRADUATED_ADULT.getCode()); + codes.add(HOMESCHOOL.getCode()); + return codes; + } + + public static List getAllSchoolGradesExcludingHS(){ + List codes = new ArrayList<>(); + codes.add(KINDHALF.getCode()); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + codes.add(GRADUATED_ADULT.getCode()); + return codes; + } + + public static List getNonIndependentSchoolGrades() { + List codes = new ArrayList<>(); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + codes.add(GRADUATED_ADULT.getCode()); + codes.add(HOMESCHOOL.getCode()); + return codes; + } + public static List getNonIndependentKtoSUGrades() { + List codes = new ArrayList<>(); + codes.add(KINDFULL.getCode()); + codes.add(GRADE01.getCode()); + codes.add(GRADE02.getCode()); + codes.add(GRADE03.getCode()); + codes.add(GRADE04.getCode()); + codes.add(GRADE05.getCode()); + codes.add(GRADE06.getCode()); + codes.add(GRADE07.getCode()); + codes.add(ELEMUNGR.getCode()); + codes.add(GRADE08.getCode()); + codes.add(GRADE09.getCode()); + codes.add(GRADE10.getCode()); + codes.add(GRADE11.getCode()); + codes.add(GRADE12.getCode()); + codes.add(SECONDARY_UNGRADED.getCode()); + return codes; + } + + public static List getIndependentKtoSUGrades() { + List codes = getKToNineGrades(); + codes.addAll(getGrades10toSU()); + return codes; + } + + public static List getNonIndependentKtoGAGrades() { + List codes = new ArrayList<>(getNonIndependentKtoSUGrades()); + codes.add(GRADUATED_ADULT.getCode()); + return codes; + } + + public static List getIndependentKtoGAGrades() { + List codes = new ArrayList<>(); + codes.add(KINDHALF.getCode()); + codes.addAll(getNonIndependentKtoGAGrades()); + return codes; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/URL.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/URL.java new file mode 100644 index 0000000..3e2eacc --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/v1/URL.java @@ -0,0 +1,10 @@ +package ca.bc.gov.educ.eas.api.constants.v1; + +public final class URL { + + private URL(){ + } + + public static final String BASE_URL="/api/v1/eas"; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/HelloWorldController.java b/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/HelloWorldController.java new file mode 100644 index 0000000..2ad49a4 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/HelloWorldController.java @@ -0,0 +1,18 @@ +package ca.bc.gov.educ.eas.api.controller.v1; + +import ca.bc.gov.educ.eas.api.endpoint.v1.HelloWorldEndpoint; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloWorldController implements HelloWorldEndpoint { + + @Autowired + public HelloWorldController() {} + + + @Override + public String helloWorld() { + return "Hello World"; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/HelloWorldEndpoint.java b/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/HelloWorldEndpoint.java new file mode 100644 index 0000000..a9fb660 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/HelloWorldEndpoint.java @@ -0,0 +1,15 @@ +package ca.bc.gov.educ.eas.api.endpoint.v1; + +import ca.bc.gov.educ.eas.api.constants.v1.URL; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping(URL.BASE_URL + "/hello") +public interface HelloWorldEndpoint { + + @GetMapping() + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + String helloWorld(); +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EasAPIRuntimeException.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EasAPIRuntimeException.java new file mode 100644 index 0000000..94d6e7c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EasAPIRuntimeException.java @@ -0,0 +1,26 @@ +package ca.bc.gov.educ.eas.api.exception; + +/** + * The type EAS api runtime exception. + */ +public class EasAPIRuntimeException extends RuntimeException { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 5241655513745148898L; + + /** + * Instantiates a new EAS api runtime exception. + * + * @param message the message + */ + public EasAPIRuntimeException(String message) { + super(message); + } + + public EasAPIRuntimeException(Throwable exception) { + super(exception); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EntityNotFoundException.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EntityNotFoundException.java new file mode 100644 index 0000000..4e124c1 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/EntityNotFoundException.java @@ -0,0 +1,59 @@ +package ca.bc.gov.educ.eas.api.exception; + +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.IntStream; + +/** + * EntityNotFoundException to provide more details in error description + */ +@NoArgsConstructor +public class EntityNotFoundException extends RuntimeException { + + /** + * Instantiates a new Entity not found exception. + * + * @param clazz the clazz + * @param searchParamsMap the search params map + */ + public EntityNotFoundException(Class clazz, String... searchParamsMap) { + super(EntityNotFoundException.generateMessage(clazz.getSimpleName(), toMap(String.class, String.class, searchParamsMap))); + } + + /** + * Generate message string. + * + * @param entity the entity + * @param searchParams the search params + * @return the string + */ + private static String generateMessage(String entity, Map searchParams) { + return StringUtils.capitalize(entity) + + " was not found for parameters " + + searchParams; + } + + /** + * To map map. + * + * @param the type parameter + * @param the type parameter + * @param keyType the key type + * @param valueType the value type + * @param entries the entries + * @return the map + */ + private static Map toMap( + Class keyType, Class valueType, Object... entries) { + if (entries.length % 2 == 1) + throw new IllegalArgumentException("Invalid entries"); + return IntStream.range(0, entries.length / 2).map(i -> i * 2) + .collect(HashMap::new, + (m, i) -> m.put(keyType.cast(entries[i]), valueType.cast(entries[i + 1])), + Map::putAll); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidParameterException.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidParameterException.java new file mode 100644 index 0000000..6c7af80 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidParameterException.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.eas.api.exception; + +/** + * InvalidParameterException to provide error details when unexpected parameters are passed to endpoint + * + */ +public class InvalidParameterException extends RuntimeException { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = -2325104800954988680L; + + /** + * Instantiates a new Invalid parameter exception. + * + * @param searchParamsMap the search params map + */ + public InvalidParameterException(String... searchParamsMap) { + super(InvalidParameterException.generateMessage(searchParamsMap)); + } + + /** + * Generate message string. + * + * @param searchParams the search params + * @return the string + */ + private static String generateMessage(String... searchParams) { + StringBuilder message = new StringBuilder("Unexpected request parameters provided: "); + String prefix = ""; + for (String parameter : searchParams) { + message.append(prefix); + prefix = ","; + message.append(parameter); + } + return message.toString(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidPayloadException.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidPayloadException.java new file mode 100644 index 0000000..fb5515a --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/InvalidPayloadException.java @@ -0,0 +1,27 @@ +package ca.bc.gov.educ.eas.api.exception; + +import ca.bc.gov.educ.eas.api.exception.errors.ApiError; +import lombok.Getter; + +/** + * The type Invalid payload exception. + */ +@SuppressWarnings("squid:S1948") +public class InvalidPayloadException extends RuntimeException { + + /** + * The Error. + */ + @Getter + private final ApiError error; + + /** + * Instantiates a new Invalid payload exception. + * + * @param error the error + */ + public InvalidPayloadException(final ApiError error) { + super(error.getMessage()); + this.error = error; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/RestExceptionHandler.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/RestExceptionHandler.java new file mode 100644 index 0000000..2e4f8d2 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/RestExceptionHandler.java @@ -0,0 +1,141 @@ +package ca.bc.gov.educ.eas.api.exception; + +import ca.bc.gov.educ.eas.api.exception.errors.ApiError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.LocalDateTime; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * The type Rest exception handler. + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@ControllerAdvice +public class RestExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * The constant log. + */ + private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class); + + /** + * Handle http message not readable response entity. + * + * @param ex the ex + * @param headers the headers + * @param status the status + * @param request the request + * @return the response entity + */ + @Override + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + String error = "Malformed JSON request"; + log.error("{} ", error, ex); + return buildResponseEntity(new ApiError(BAD_REQUEST, error, ex)); + } + + /** + * Build response entity response entity. + * + * @param apiError the api error + * @return the response entity + */ + private ResponseEntity buildResponseEntity(ApiError apiError) { + return new ResponseEntity<>(apiError, apiError.getStatus()); + } + + /** + * Handles EntityNotFoundException. Created to encapsulate errors with more detail than jakarta.persistence.EntityNotFoundException. + * + * @param ex the EntityNotFoundException + * @return the ApiError object + */ + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFound( + EntityNotFoundException ex) { + ApiError apiError = new ApiError(NOT_FOUND); + apiError.setMessage(ex.getMessage()); + log.info("{} ", apiError.getMessage(), ex); + return buildResponseEntity(apiError); + } + + /** + * Handles IllegalArgumentException + * + * @param ex the InvalidParameterException + * @return the ApiError object + */ + @ExceptionHandler(IllegalArgumentException.class) + protected ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage(ex.getMessage()); + log.error("{} ", apiError.getMessage(), ex); + return buildResponseEntity(apiError); + } + + /** + * Handles InvalidParameterException + * + * @param ex the InvalidParameterException + * @return the ApiError object + */ + @ExceptionHandler(InvalidParameterException.class) + protected ResponseEntity handleInvalidParameter(InvalidParameterException ex) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage(ex.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + log.error("{} ", apiError.getMessage(), ex); + return buildResponseEntity(apiError); + } + + /** + * Handles MethodArgumentNotValidException. Triggered when an object fails @Valid validation. + * + * @param ex the MethodArgumentNotValidException that is thrown when @Valid validation fails + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage("Validation error"); + apiError.addValidationErrors(ex.getBindingResult().getFieldErrors()); + apiError.addValidationError(ex.getBindingResult().getGlobalErrors()); + log.error("{} ", apiError.getMessage(), ex); + return buildResponseEntity(apiError); + } + + /** + * Handles EntityNotFoundException. Created to encapsulate errors with more detail than jakarta.persistence.EntityNotFoundException. + * + * @param ex the EntityNotFoundException + * @return the ApiError object + */ + @ExceptionHandler(InvalidPayloadException.class) + protected ResponseEntity handleInvalidPayload( + InvalidPayloadException ex) { + log.error("", ex); + return buildResponseEntity(ex.getError()); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/SagaRuntimeException.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/SagaRuntimeException.java new file mode 100644 index 0000000..382de1e --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/SagaRuntimeException.java @@ -0,0 +1,15 @@ +package ca.bc.gov.educ.eas.api.exception; + +public class SagaRuntimeException extends RuntimeException { + + private static final long serialVersionUID = 5241655513745148898L; + + public SagaRuntimeException(String message) { + super(message); + } + + public SagaRuntimeException(Throwable exception) { + super(exception); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiError.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiError.java new file mode 100644 index 0000000..fe465c0 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiError.java @@ -0,0 +1,277 @@ +package ca.bc.gov.educ.eas.api.exception.errors; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.ConstraintViolation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * The type Api error. + */ +@AllArgsConstructor +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@SuppressWarnings("squid:S1948") +public class ApiError implements Serializable { + + /** + * The Status. + */ + private HttpStatus status; + /** + * The Timestamp. + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") + private LocalDateTime timestamp; + /** + * The Message. + */ + private String message; + /** + * The Debug message. + */ + private String debugMessage; + /** + * The Sub errors. + */ + private List subErrors; + + /** + * Instantiates a new Api error. + */ + private ApiError() { + timestamp = LocalDateTime.now(); + } + + /** + * Instantiates a new Api error. + * + * @param status the status + */ + public ApiError(HttpStatus status) { + this(); + this.status = status; + } + + /** + * Instantiates a new Api error. + * + * @param status the status + * @param ex the ex + */ + ApiError(HttpStatus status, Throwable ex) { + this(); + this.status = status; + this.message = "Unexpected error"; + this.debugMessage = ex.getLocalizedMessage(); + } + + /** + * Instantiates a new Api error. + * + * @param status the status + * @param message the message + * @param ex the ex + */ + public ApiError(HttpStatus status, String message, Throwable ex) { + this(); + this.status = status; + this.message = message; + this.debugMessage = ex.getLocalizedMessage(); + } + + /** + * Add sub error. + * + * @param subError the sub error + */ + private void addSubError(ApiSubError subError) { + if (subErrors == null) { + subErrors = new ArrayList<>(); + } + subErrors.add(subError); + } + + /** + * Add validation error. + * + * @param object the object + * @param field the field + * @param rejectedValue the rejected value + * @param message the message + */ + private void addValidationError(String object, String field, Object rejectedValue, String message) { + addSubError(new ApiValidationError(object, field, rejectedValue, message)); + } + + /** + * Add validation error. + * + * @param object the object + * @param message the message + */ + private void addValidationError(String object, String message) { + addSubError(new ApiValidationError(object, message)); + } + + /** + * Add validation error. + * + * @param fieldError the field error + */ + private void addValidationError(FieldError fieldError) { + this.addValidationError(fieldError.getObjectName(), fieldError.getField(), fieldError.getRejectedValue(), + fieldError.getDefaultMessage()); + } + + /** + * Add validation errors. + * + * @param fieldErrors the field errors + */ + public void addValidationErrors(List fieldErrors) { + fieldErrors.forEach(this::addValidationError); + } + + /** + * Add validation error. + * + * @param objectError the object error + */ + private void addValidationError(ObjectError objectError) { + this.addValidationError(objectError.getObjectName(), objectError.getDefaultMessage()); + } + + /** + * Add validation error. + * + * @param globalErrors the global errors + */ + public void addValidationError(List globalErrors) { + globalErrors.forEach(this::addValidationError); + } + + /** + * Utility method for adding error of ConstraintViolation. Usually when + * a @Validated validation fails. + * + * @param cv the ConstraintViolation + */ + private void addValidationError(ConstraintViolation cv) { + this.addValidationError(cv.getRootBeanClass().getSimpleName(), + ((PathImpl) cv.getPropertyPath()).getLeafNode().asString(), cv.getInvalidValue(), cv.getMessage()); + } + + /** + * Add validation errors. + * + * @param constraintViolations the constraint violations + */ + public void addValidationErrors(Set> constraintViolations) { + constraintViolations.forEach(this::addValidationError); + } + + /** + * Gets status. + * + * @return the status + */ + public HttpStatus getStatus() { + return status; + } + + /** + * Sets status. + * + * @param status the status + */ + public void setStatus(HttpStatus status) { + this.status = status; + } + + /** + * Gets timestamp. + * + * @return the timestamp + */ + public LocalDateTime getTimestamp() { + return timestamp; + } + + /** + * Sets timestamp. + * + * @param timestamp the timestamp + */ + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + /** + * Gets message. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets message. + * + * @param message the message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets debug message. + * + * @return the debug message + */ + public String getDebugMessage() { + return debugMessage; + } + + /** + * Sets debug message. + * + * @param debugMessage the debug message + */ + public void setDebugMessage(String debugMessage) { + this.debugMessage = debugMessage; + } + + /** + * Gets sub errors. + * + * @return the sub errors + */ + public List getSubErrors() { + return subErrors; + } + + /** + * Sets sub errors. + * + * @param subErrors the sub errors + */ + public void setSubErrors(List subErrors) { + this.subErrors = subErrors; + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiSubError.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiSubError.java new file mode 100644 index 0000000..475067e --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiSubError.java @@ -0,0 +1,37 @@ +package ca.bc.gov.educ.eas.api.exception.errors; + +import java.io.Serializable; + +/** + * The interface Api sub error. + */ +public interface ApiSubError extends Serializable { + + /** + * Gets field. + * + * @return the field + */ + String getField(); + + /** + * Gets message. + * + * @return the message + */ + String getMessage(); + + /** + * Gets object. + * + * @return the object + */ + String getObject(); + + /** + * Gets rejected value. + * + * @return the rejected value + */ + Object getRejectedValue(); +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiValidationError.java b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiValidationError.java new file mode 100644 index 0000000..edb86cf --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/exception/errors/ApiValidationError.java @@ -0,0 +1,42 @@ +package ca.bc.gov.educ.eas.api.exception.errors; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * The type Api validation error. + */ +@AllArgsConstructor +@Data +@Builder +@SuppressWarnings("squid:S1948") +public class ApiValidationError implements ApiSubError { + /** + * The Object. + */ + private String object; + /** + * The Field. + */ + private String field; + /** + * The Rejected value. + */ + private Object rejectedValue; + /** + * The Message. + */ + private String message; + + /** + * Instantiates a new Api validation error. + * + * @param object the object + * @param message the message + */ + ApiValidationError(String object, String message) { + this.object = object; + this.message = message; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/filter/BaseFilterSpecs.java b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/BaseFilterSpecs.java new file mode 100644 index 0000000..80f43c1 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/BaseFilterSpecs.java @@ -0,0 +1,121 @@ +package ca.bc.gov.educ.eas.api.filter; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; + +import java.time.chrono.ChronoLocalDate; +import java.time.chrono.ChronoLocalDateTime; +import java.util.UUID; +import java.util.function.Function; + +/** + * this is the generic class to support all kind of filter specifications for different entities + * + * @param the entity type. + * @author Om + */ +@RequiredArgsConstructor +public abstract class BaseFilterSpecs { + + private final FilterSpecifications dateFilterSpecifications; + private final FilterSpecifications> dateTimeFilterSpecifications; + private final FilterSpecifications integerFilterSpecifications; + private final FilterSpecifications stringFilterSpecifications; + private final FilterSpecifications longFilterSpecifications; + private final FilterSpecifications uuidFilterSpecifications; + private final FilterSpecifications booleanFilterSpecifications; + private final Converters converters; + + /** + * Gets date type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the date type specification + */ + public Specification getDateTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(ChronoLocalDate.class), dateFilterSpecifications); + } + + /** + * Gets date time type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the date time type specification + */ + public Specification getDateTimeTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(ChronoLocalDateTime.class), dateTimeFilterSpecifications); + } + + /** + * Gets integer type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the integer type specification + */ + public Specification getIntegerTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(Integer.class), integerFilterSpecifications); + } + + /** + * Gets long type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the long type specification + */ + public Specification getLongTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(Long.class), longFilterSpecifications); + } + + /** + * Gets string type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the string type specification + */ + public Specification getStringTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(String.class), stringFilterSpecifications); + } + + /** + * Gets boolean type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the string type specification + */ + public Specification getBooleanTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(Boolean.class), booleanFilterSpecifications); + } + + /** + * Gets uuid type specification. + * + * @param fieldName the field name + * @param filterValue the filter value + * @param filterOperation the filter operation + * @return the uuid type specification + */ + public Specification getUUIDTypeSpecification(String fieldName, String filterValue, FilterOperation filterOperation) { + return getSpecification(fieldName, filterValue, filterOperation, converters.getFunction(UUID.class), uuidFilterSpecifications); + } + + private > Specification getSpecification(String fieldName, + String filterValue, + FilterOperation filterOperation, + Function converter, + FilterSpecifications specifications) { + FilterCriteria criteria = new FilterCriteria<>(fieldName, filterValue, filterOperation, converter); + return specifications.getSpecification(criteria.getOperation()).apply(criteria); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/filter/Converters.java b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/Converters.java new file mode 100644 index 0000000..8b19dc6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/Converters.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.eas.api.filter; + +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.chrono.ChronoLocalDate; +import java.time.chrono.ChronoLocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; + +/** + * @author om + */ +@Service +public class Converters { + + private final Map, Function>> map = new HashMap<>(); + + @PostConstruct + public void init() { + map.put(String.class, s -> s); + map.put(Long.class, Long::valueOf); + map.put(Integer.class, Integer::valueOf); + map.put(ChronoLocalDate.class, LocalDate::parse); + map.put(ChronoLocalDateTime.class, LocalDateTime::parse); + map.put(UUID.class, UUID::fromString); + map.put(Boolean.class, Boolean::valueOf); + } + + @SuppressWarnings("unchecked") + public > Function getFunction(Class classObj) { + return (Function) map.get(classObj); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterCriteria.java b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterCriteria.java new file mode 100644 index 0000000..b710712 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterCriteria.java @@ -0,0 +1,195 @@ +package ca.bc.gov.educ.eas.api.filter; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; + +/** + * Filter Criteria Holder + * + * @param is the java type of the DB table column + * @author om + */ +public class FilterCriteria> { + + /** + * Holds the operation {@link FilterOperation} + */ + private final FilterOperation operation; + + /** + * Table column name + */ + private final String fieldName; + + /** + * Holds the Function to convertString to + */ + private final Function converterFunction; + /** + * Holds the filter criteria + */ + private final Collection originalValues; + /** + * Holds the filter criteria as type + */ + private final Collection convertedValues; + /** + * Converted value + */ + private T convertedSingleValue; + /** + * minimum value - application only for {@link FilterOperation#BETWEEN} + */ + private T minValue; + /** + * maximum value - application only for {@link FilterOperation#BETWEEN} + */ + private T maxValue; + + /** + * Instantiates a new Filter criteria. + * + * @param fieldName the field name + * @param fieldValue the field value + * @param filterOperation the filter operation + * @param converterFunction the converter function + */ + public FilterCriteria(@NonNull String fieldName, String fieldValue, @NonNull FilterOperation filterOperation, Function converterFunction) { + + this.fieldName = fieldName; + this.converterFunction = converterFunction; + + String[] operationValues; + + if (filterOperation == FilterOperation.BETWEEN || filterOperation == FilterOperation.IN || filterOperation == FilterOperation.NOT_IN || filterOperation == FilterOperation.IN_LEFT_JOIN || filterOperation == FilterOperation.NONE_IN || filterOperation == FilterOperation.IN_NOT_DISTINCT) { + if (fieldValue != null) { + // Split the fieldValue value as comma separated. + operationValues = StringUtils.split(fieldValue, ","); + } else { + operationValues = new String[]{null}; + } + if (operationValues.length < 1) { + throw new IllegalArgumentException("multiple values expected(comma separated) for IN, NOT IN and BETWEEN operations."); + } + } else { + operationValues = new String[]{fieldValue}; + } + this.operation = filterOperation; + this.originalValues = Arrays.asList(operationValues); + this.convertedValues = new ArrayList<>(); + + // Validate other conditions + validateAndAssign(operationValues); + + } + + private void validateAndAssign(String[] operationValues) { + + //For operation 'btn' + if (FilterOperation.BETWEEN == operation) { + if (operationValues.length != 2) { + throw new IllegalArgumentException("For 'btn' operation two values are expected"); + } else { + + //Convert + T value1 = this.converterFunction.apply(operationValues[0]); + T value2 = this.converterFunction.apply(operationValues[1]); + + //Set min and max values + if (value1.compareTo(value2) > 0) { + this.minValue = value2; + this.maxValue = value1; + } else { + this.minValue = value1; + this.maxValue = value2; + } + } + + //For 'in' or 'nin' operation + } else if (FilterOperation.IN == operation || FilterOperation.NOT_IN == operation || FilterOperation.IN_LEFT_JOIN == operation || FilterOperation.NONE_IN == operation || FilterOperation.IN_NOT_DISTINCT == operation) { + convertedValues.addAll(originalValues.stream().map(converterFunction).toList()); + } else { + //All other operation + this.convertedSingleValue = converterFunction.apply(operationValues[0]); + } + + } + + /** + * Gets converted single value. + * + * @return the converted single value + */ + public T getConvertedSingleValue() { + return convertedSingleValue; + } + + /** + * Gets min value. + * + * @return the min value + */ + public T getMinValue() { + return minValue; + } + + /** + * Gets max value. + * + * @return the max value + */ + public T getMaxValue() { + return maxValue; + } + + /** + * Gets operation. + * + * @return the operation + */ + public FilterOperation getOperation() { + return operation; + } + + /** + * Gets field name. + * + * @return the field name + */ + public String getFieldName() { + return fieldName; + } + + /** + * Gets converter function. + * + * @return the converter function + */ + public Function getConverterFunction() { + return converterFunction; + } + + /** + * Gets original values. + * + * @return the original values + */ + public Collection getOriginalValues() { + return originalValues; + } + + /** + * Gets converted values. + * + * @return the converted values + */ + public Collection getConvertedValues() { + return convertedValues; + } + +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterOperation.java b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterOperation.java new file mode 100644 index 0000000..79488b7 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterOperation.java @@ -0,0 +1,101 @@ +package ca.bc.gov.educ.eas.api.filter; + +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Optional; + +public enum FilterOperation { + + /** + * Equal filter operation. + */ + EQUAL("eq"), + /** + * Equal Other Field filter operation. + */ + NOT_EQUAL_OTHER_COLUMN("neqc"), + /** + * Not equal filter operation. + */ + NOT_EQUAL("neq"), + /** + * Greater than filter operation. + */ + GREATER_THAN("gt"), + /** + * Greater than or equal to filter operation. + */ + GREATER_THAN_OR_EQUAL_TO("gte"), + /** + * Less than filter operation. + */ + LESS_THAN("lt"), + /** + * Less than or equal to filter operation. + */ + LESS_THAN_OR_EQUAL_TO("lte"), + /** + * In filter operation. + */ + IN("in"), + /** + * Filter to return when none of the child records includes the values + */ + NONE_IN("none_in"), + /** + * Not in filter operation. + */ + NOT_IN("nin"), + /** + * Between filter operation. + */ + BETWEEN("btn"), + /** + * Contains filter operation. + */ + CONTAINS("like"), + /** + * Starts with filter operation. + */ + STARTS_WITH("starts_with"), + /** + * Not Starts with filter operation. + */ + NOT_STARTS_WITH("not_starts_with"), + /** + * Ends with filter operation. + */ + ENDS_WITH("ends_with"), + /** + * Starts with ignore case filter operation. + */ + STARTS_WITH_IGNORE_CASE("starts_with_ignore_case"), + /** + * Contains ignore case filter operation. + */ + CONTAINS_IGNORE_CASE("like_ignore_case"), + IN_LEFT_JOIN("in_left_join"), + IN_NOT_DISTINCT("in_not_distinct"); + + private final String value; + + FilterOperation(String value) { + this.value = value; + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + public static Optional fromValue(String value) { + for (FilterOperation op : FilterOperation.values()) { + if (String.valueOf(op.value).equalsIgnoreCase(value)) { + return Optional.of(op); + } + } + return Optional.empty(); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterSpecifications.java b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterSpecifications.java new file mode 100644 index 0000000..9509ba6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/filter/FilterSpecifications.java @@ -0,0 +1,153 @@ +package ca.bc.gov.educ.eas.api.filter; + +import ca.bc.gov.educ.eas.api.exception.EasAPIRuntimeException; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.criteria.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import java.util.EnumMap; +import java.util.function.Function; + +@Service +public class FilterSpecifications> { + + private EnumMap, Specification>> map; + + public FilterSpecifications() { + initSpecifications(); + } + + public Function, Specification> getSpecification(FilterOperation operation) { + return map.get(operation); + } + + @PostConstruct + public void initSpecifications() { + + map = new EnumMap<>(FilterOperation.class); + + // Equal + map.put(FilterOperation.EQUAL, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + if(splits.length == 2) { + return criteriaBuilder.equal(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } else { + return criteriaBuilder.equal(root.join(splits[0]).get(splits[1]).get(splits[2]), filterCriteria.getConvertedSingleValue()); + } + + } else if(filterCriteria.getConvertedSingleValue() == null) { + return criteriaBuilder.isNull(root.get(filterCriteria.getFieldName())); + } + return criteriaBuilder.equal(root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.NOT_EQUAL_OTHER_COLUMN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(",")) { + String[] splits = filterCriteria.getFieldName().split(","); + if(splits.length == 2) { + return criteriaBuilder.notEqual(root.get(splits[0]), root.get(splits[1])); + } + } + throw new EasAPIRuntimeException("Invalid search criteria provided"); + }); + + map.put(FilterOperation.NOT_EQUAL, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.notEqual(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } else if(filterCriteria.getConvertedSingleValue() == null) { + return criteriaBuilder.isNotNull(root.get(filterCriteria.getFieldName())); + } + return criteriaBuilder.notEqual(root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.GREATER_THAN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.greaterThan(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } + return criteriaBuilder.greaterThan(root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.GREATER_THAN_OR_EQUAL_TO, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.greaterThanOrEqualTo(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } + return criteriaBuilder.greaterThanOrEqualTo( + root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.LESS_THAN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.lessThan(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } + return criteriaBuilder.lessThan(root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.LESS_THAN_OR_EQUAL_TO, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.lessThanOrEqualTo(root.join(splits[0]).get(splits[1]), filterCriteria.getConvertedSingleValue()); + } + return criteriaBuilder.lessThanOrEqualTo(root.get(filterCriteria.getFieldName()), filterCriteria.getConvertedSingleValue()); + }); + + map.put(FilterOperation.IN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + criteriaQuery.distinct(true); + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return root.join(splits[0]).get(splits[1]).in(filterCriteria.getConvertedValues()); + } + return root.get(filterCriteria.getFieldName()).in(filterCriteria.getConvertedValues()); + }); + + map.put(FilterOperation.IN_NOT_DISTINCT, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return root.join(splits[0]).get(splits[1]).in(filterCriteria.getConvertedValues()); + } + return root.get(filterCriteria.getFieldName()).in(filterCriteria.getConvertedValues()); + }); + + map.put(FilterOperation.NOT_IN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.or(criteriaBuilder.not(root.join(splits[0], JoinType.LEFT).get(splits[1]).in(filterCriteria.getConvertedValues())), criteriaBuilder.isEmpty(root.get(splits[0]))); + } + return criteriaBuilder.or(criteriaBuilder.not(root.get(filterCriteria.getFieldName()).in(filterCriteria.getConvertedValues())), criteriaBuilder.isNull(root.get(filterCriteria.getFieldName()))); + }); + + map.put(FilterOperation.BETWEEN, + filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.between(root.join(splits[0]).get(splits[1]), filterCriteria.getMinValue(), + filterCriteria.getMaxValue()); + } else { + return criteriaBuilder.between( + root.get(filterCriteria.getFieldName()), filterCriteria.getMinValue(), + filterCriteria.getMaxValue()); + } + }); + + map.put(FilterOperation.CONTAINS, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder + .like(root.get(filterCriteria.getFieldName()), "%" + filterCriteria.getConvertedSingleValue() + "%")); + + map.put(FilterOperation.CONTAINS_IGNORE_CASE, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder + .like(criteriaBuilder.lower(root.get(filterCriteria.getFieldName())), "%" + filterCriteria.getConvertedSingleValue().toString().toLowerCase() + "%")); + + + map.put(FilterOperation.IN_LEFT_JOIN, filterCriteria -> (root, criteriaQuery, criteriaBuilder) -> { + criteriaQuery.distinct(true); + if (filterCriteria.getFieldName().contains(".")) { + String[] splits = filterCriteria.getFieldName().split("\\."); + return criteriaBuilder.or(root.join(splits[0], JoinType.LEFT).get(splits[1]).in(filterCriteria.getConvertedValues())); + } + return root.get(filterCriteria.getFieldName()).in(filterCriteria.getConvertedValues()); + }); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/health/EasAPICustomHealthCheck.java b/api/src/main/java/ca/bc/gov/educ/eas/api/health/EasAPICustomHealthCheck.java new file mode 100644 index 0000000..977755d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/health/EasAPICustomHealthCheck.java @@ -0,0 +1,37 @@ +package ca.bc.gov.educ.eas.api.health; + +import io.nats.client.Connection; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class EasAPICustomHealthCheck implements HealthIndicator { + private final Connection natsConnection; + + public EasAPICustomHealthCheck(final Connection natsConnection) { + this.natsConnection = natsConnection; + } + + @Override + public Health getHealth(final boolean includeDetails) { + return this.healthCheck(); + } + + + @Override + public Health health() { + return this.healthCheck(); + } + + private Health healthCheck() { + if (this.natsConnection.getStatus() == Connection.Status.CLOSED) { + log.warn("Health Check failed for NATS"); + return Health.down().withDetail("NATS", " Connection is Closed.").build(); + } + return Health.up().build(); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/helpers/LogHelper.java b/api/src/main/java/ca/bc/gov/educ/eas/api/helpers/LogHelper.java new file mode 100644 index 0000000..6834335 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/helpers/LogHelper.java @@ -0,0 +1,99 @@ +package ca.bc.gov.educ.eas.api.helpers; + +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public final class LogHelper { + private static final ObjectMapper mapper = new ObjectMapper(); + private static final String EXCEPTION = "Exception "; + + private LogHelper() { + + } + + public static void logServerHttpReqResponseDetails(@NonNull final HttpServletRequest request, final HttpServletResponse response) { + try { + final int status = response.getStatus(); + val totalTime = Instant.now().toEpochMilli() - (Long) request.getAttribute("startTime"); + final Map httpMap = new HashMap<>(); + httpMap.put("server_http_response_code", status); + httpMap.put("server_http_request_method", request.getMethod()); + httpMap.put("server_http_query_params", request.getQueryString()); + val correlationID = request.getHeader(ApplicationProperties.CORRELATION_ID); + if (correlationID != null) { + httpMap.put("correlation_id", correlationID); + } + httpMap.put("server_http_request_url", String.valueOf(request.getRequestURL())); + httpMap.put("server_http_request_processing_time_ms", totalTime); + httpMap.put("server_http_request_payload", String.valueOf(request.getAttribute("payload"))); + httpMap.put("server_http_request_remote_address", request.getRemoteAddr()); + httpMap.put("server_http_request_client_name", StringUtils.trimToEmpty(request.getHeader("X-Client-Name"))); + MDC.putCloseable("httpEvent", mapper.writeValueAsString(httpMap)); + log.info(""); + MDC.clear(); + } catch (final Exception exception) { + log.error(EXCEPTION, exception); + } + } + + public static void logClientHttpReqResponseDetails(@NonNull final HttpMethod method, final String url, final int responseCode, final List correlationID) { + try { + final Map httpMap = new HashMap<>(); + httpMap.put("client_http_response_code", responseCode); + httpMap.put("client_http_request_method", method.toString()); + httpMap.put("client_http_request_url", url); + if (correlationID != null) { + httpMap.put("correlation_id", String.join(",", correlationID)); + } + MDC.putCloseable("httpEvent", mapper.writeValueAsString(httpMap)); + log.info(""); + MDC.clear(); + } catch (final Exception exception) { + log.error(EXCEPTION, exception); + } + } + + /** + * the event is a json string. + * + * @param event the json string + */ + public static void logMessagingEventDetails(final String event) { + try { + MDC.putCloseable("messageEvent", event); + log.debug(""); + MDC.clear(); + } catch (final Exception exception) { + log.error(EXCEPTION, exception); + } + } + + public static void logSagaRetry(final EasSagaEntity saga) { + final Map retrySagaMap = new HashMap<>(); + try { + retrySagaMap.put("sagaName", saga.getSagaName()); + retrySagaMap.put("sagaId", saga.getSagaId()); + retrySagaMap.put("retryCount", saga.getRetryCount()); + MDC.putCloseable("sagaRetry", mapper.writeValueAsString(retrySagaMap)); + log.info("Saga is being retried."); + MDC.clear(); + } catch (final Exception ex) { + log.error(EXCEPTION, ex); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/LocalDateTimeMapper.java b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/LocalDateTimeMapper.java new file mode 100644 index 0000000..c8452ff --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/LocalDateTimeMapper.java @@ -0,0 +1,37 @@ +package ca.bc.gov.educ.eas.api.mappers; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * The type Local date time mapper. + */ +public class LocalDateTimeMapper { + + /** + * Map string. + * + * @param dateTime the date time + * @return the string + */ + public String map(LocalDateTime dateTime) { + if (dateTime == null) { + return null; + } + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dateTime); + } + + /** + * Map local date time. + * + * @param dateTime the date time + * @return the local date time + */ + public LocalDateTime map(String dateTime) { + if (dateTime == null) { + return null; + } + return LocalDateTime.parse(dateTime); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/StringMapper.java b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/StringMapper.java new file mode 100644 index 0000000..4e15f59 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/StringMapper.java @@ -0,0 +1,38 @@ +package ca.bc.gov.educ.eas.api.mappers; + +import org.apache.commons.lang3.StringUtils; + +public class StringMapper { + + private StringMapper() { + } + + public static String map(final String value) { + if (StringUtils.isNotBlank(value)) { + return value.trim(); + } + return value; + } + + public static String trimAndUppercase(String value){ + if (StringUtils.isNotBlank(value)) { + return StringUtils.trim(value).toUpperCase(); + } + return value; + } + + public static String removeLeadingApostrophes(String value) { + if (StringUtils.isNotBlank(value)) { + value = value.trim(); + if (value.startsWith("'") && value.length() == 1) { + return null; + } + } + return value; + } + + public static String processGivenName(String value) { + String legalGivenName = removeLeadingApostrophes(value); + return trimAndUppercase(legalGivenName); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/UUIDMapper.java b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/UUIDMapper.java new file mode 100644 index 0000000..c36c445 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/mappers/UUIDMapper.java @@ -0,0 +1,37 @@ +package ca.bc.gov.educ.eas.api.mappers; + +import org.apache.commons.lang3.StringUtils; + +import java.util.UUID; + +/** + * The type Uuid mapper. + */ +public class UUIDMapper { + + /** + * Map uuid. + * + * @param value the value + * @return the uuid + */ + public UUID map(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + return UUID.fromString(value); + } + + /** + * Map string. + * + * @param value the value + * @return the string + */ + public String map(UUID value) { + if (value == null) { + return null; + } + return value.toString(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessagePublisher.java b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessagePublisher.java new file mode 100644 index 0000000..c5054ed --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessagePublisher.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.eas.api.messaging; + +import io.nats.client.Connection; +import io.nats.client.Message; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +/** + * The type Message publisher. + */ +@Component +@Slf4j +public class MessagePublisher { + + + private final Connection connection; + + @Autowired + public MessagePublisher(final Connection con) { + this.connection = con; + } + + /** + * Dispatch message. + * + * @param subject the subject + * @param message the message + */ + public void dispatchMessage(final String subject, final byte[] message) { + this.connection.publish(subject, message); + } + + public CompletableFuture requestMessage(final String subject, final byte[] message) { + return this.connection.request(subject, message); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessageSubscriber.java b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessageSubscriber.java new file mode 100644 index 0000000..7da0f45 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/MessageSubscriber.java @@ -0,0 +1,68 @@ +package ca.bc.gov.educ.eas.api.messaging; + +import ca.bc.gov.educ.eas.api.helpers.LogHelper; +import ca.bc.gov.educ.eas.api.orchestrator.base.EventHandler; +import ca.bc.gov.educ.eas.api.struct.Event; +import ca.bc.gov.educ.eas.api.util.JsonUtil; +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.MessageHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static lombok.AccessLevel.PRIVATE; + +@Component +@Slf4j +public class MessageSubscriber { + + /** + * The Handlers. + */ + @Getter(PRIVATE) + private final Map handlerMap = new HashMap<>(); + private final Connection connection; + + @Autowired + public MessageSubscriber(final Connection con, final List eventHandlers) { + this.connection = con; + eventHandlers.forEach(handler -> { + this.handlerMap.put(handler.getTopicToSubscribe(), handler); + this.subscribe(handler.getTopicToSubscribe(), handler); + }); + } + + public void subscribe(final String topic, final EventHandler eventHandler) { + this.handlerMap.computeIfAbsent(topic, k -> eventHandler); + final String queue = topic.replace("_", "-"); + final var dispatcher = this.connection.createDispatcher(this.onMessage(eventHandler)); + dispatcher.subscribe(topic, queue); + } + + /** + * On message message handler. + * + * @return the message handler + */ + public MessageHandler onMessage(final EventHandler eventHandler) { + return (Message message) -> { + if (message != null) { + log.debug("Message received subject :: {}, replyTo :: {}, subscriptionID :: {}", message.getSubject(), message.getReplyTo(), message.getSID()); + try { + final var eventString = new String(message.getData()); + LogHelper.logMessagingEventDetails(eventString); + final var event = JsonUtil.getJsonObjectFromString(Event.class, eventString); + eventHandler.handleEvent(event); + } catch (final Exception e) { + log.error("Exception ", e); + } + } + }; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/NatsConnection.java b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/NatsConnection.java new file mode 100644 index 0000000..ab94dd6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/NatsConnection.java @@ -0,0 +1,80 @@ +package ca.bc.gov.educ.eas.api.messaging; + +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.nats.client.Connection; +import io.nats.client.ConnectionListener; +import io.nats.client.Nats; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jboss.threads.EnhancedQueueExecutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; + +/** + * The type Nats connection. + */ +@Component +@Slf4j +public class NatsConnection implements Closeable { + @Getter + private final Connection natsCon; + + /** + * Instantiates a new Nats connection. + * + * @param applicationProperties the application properties + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ + @Autowired + public NatsConnection(final ApplicationProperties applicationProperties) throws IOException, InterruptedException { + this.natsCon = connectToNats(applicationProperties.getServer(), applicationProperties.getMaxReconnect(), applicationProperties.getConnectionName()); + } + + private Connection connectToNats(String serverUrl, int maxReconnect, String connectionName) throws IOException, InterruptedException { + io.nats.client.Options natsOptions = new io.nats.client.Options.Builder() + .connectionListener(this::connectionListener) + .maxPingsOut(5) + .pingInterval(Duration.ofSeconds(2)) + .connectionName(connectionName) + .connectionTimeout(Duration.ofSeconds(5)) + .executor(new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().setNameFormat("core-nats-%d").build()) + .setCorePoolSize(10).setMaximumPoolSize(50).setKeepAliveTime(Duration.ofSeconds(60)).build()) + .maxReconnects(maxReconnect) + .reconnectWait(Duration.ofSeconds(2)) + .servers(new String[]{serverUrl}) + .build(); + return Nats.connect(natsOptions); + } + + private void connectionListener(Connection connection, ConnectionListener.Events events) { + log.info("NATS -> {}", events.toString()); + } + + + @Override + public void close() { + if (natsCon != null) { + log.info("closing nats connection..."); + try { + natsCon.close(); + } catch (InterruptedException e) { + log.error("error while closing nats connection...", e); + Thread.currentThread().interrupt(); + } + log.info("nats connection closed..."); + } + } + + @Bean + public Connection getConnection() { + return natsCon; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/jetstream/Publisher.java b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/jetstream/Publisher.java new file mode 100644 index 0000000..961db09 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/messaging/jetstream/Publisher.java @@ -0,0 +1,88 @@ +package ca.bc.gov.educ.eas.api.messaging.jetstream; + +import ca.bc.gov.educ.eas.api.constants.EventOutcome; +import ca.bc.gov.educ.eas.api.constants.EventType; +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import ca.bc.gov.educ.eas.api.struct.Event; +import ca.bc.gov.educ.eas.api.struct.v1.ChoreographedEvent; +import ca.bc.gov.educ.eas.api.util.JsonUtil; +import io.nats.client.Connection; +import io.nats.client.JetStream; +import io.nats.client.JetStreamApiException; +import io.nats.client.api.StreamConfiguration; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static ca.bc.gov.educ.eas.api.constants.TopicsEnum.EAS_EVENTS_TOPIC; + +@Component("publisher") +@Slf4j +public class Publisher { + + private final JetStream jetStream; + + public static final String STREAM_NAME= "EAS_EVENTS"; + + /** + * Instantiates a new Publisher. + * + * @param natsConnection the nats connection + * @throws IOException the io exception + * @throws JetStreamApiException the jet stream api exception + */ + @Autowired + public Publisher(final Connection natsConnection) throws IOException, JetStreamApiException { + this.jetStream = natsConnection.jetStream(); + this.createOrUpdateEasEventStream(natsConnection); + } + + /** + * here only name and replicas and max messages are set, rest all are library default. + * + * @param natsConnection the nats connection + * @throws IOException the io exception + * @throws JetStreamApiException the jet stream api exception + */ + private void createOrUpdateEasEventStream(final Connection natsConnection) throws IOException, JetStreamApiException { + val streamConfiguration = StreamConfiguration.builder().name(STREAM_NAME).replicas(1).maxMessages(10000).addSubjects(EAS_EVENTS_TOPIC.toString()).build(); + try { + natsConnection.jetStreamManagement().updateStream(streamConfiguration); + } catch (final JetStreamApiException exception) { + if (exception.getErrorCode() == 404) { // the stream does not exist , lets create it. + natsConnection.jetStreamManagement().addStream(streamConfiguration); + } else { + log.info("exception", exception); + } + } + + } + + + /** + * Dispatch choreography event. + * + * @param event the event + */ + public void dispatchChoreographyEvent(final Event event, EasSagaEntity saga) { + if (event != null && event.getSagaId() != null) { + val choreographedEvent = new ChoreographedEvent(); + choreographedEvent.setEventType(EventType.valueOf(event.getEventType().toString())); + choreographedEvent.setEventOutcome(EventOutcome.valueOf(event.getEventOutcome().toString())); + choreographedEvent.setEventPayload(event.getEventPayload()); + choreographedEvent.setEventID(event.getSagaId().toString()); + choreographedEvent.setCreateUser(saga.getCreateUser()); + choreographedEvent.setUpdateUser(saga.getUpdateUser()); + try { + log.info("Broadcasting event :: {}", choreographedEvent); + val pub = this.jetStream.publishAsync(EAS_EVENTS_TOPIC.toString(), JsonUtil.getJsonBytesFromObject(choreographedEvent)); + pub.thenAcceptAsync(result -> log.info("Event ID :: {} Published to JetStream :: {}", event.getSagaId(), result.getSeqno())); + } catch (IOException e) { + log.error("exception while broadcasting message to JetStream", e); + } + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/EasSagaEntity.java b/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/EasSagaEntity.java new file mode 100644 index 0000000..9784072 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/EasSagaEntity.java @@ -0,0 +1,73 @@ +package ca.bc.gov.educ.eas.api.model.v1; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * The type Saga. + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +@Entity +@Table(name = "EAS_SAGA") +@DynamicUpdate +public class EasSagaEntity { + + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator", parameters = { + @org.hibernate.annotations.Parameter(name = "uuid_gen_strategy_class", value = "org.hibernate.id.uuid.CustomVersionOneStrategy")}) + @Column(name = "SAGA_ID", unique = true, updatable = false, columnDefinition = "BINARY(16)") + UUID sagaId; + + @NotNull(message = "saga name cannot be null") + @Column(name = "SAGA_NAME") + String sagaName; + + @NotNull(message = "saga state cannot be null") + @Column(name = "SAGA_STATE") + String sagaState; + + @NotNull(message = "payload cannot be null") + @Column(name = "PAYLOAD", length = 10485760) + private String payload; + + @NotNull(message = "status cannot be null") + @Column(name = "STATUS") + String status; + + @NotNull(message = "create user cannot be null") + @Column(name = "CREATE_USER", updatable = false) + @Size(max = 32) + String createUser; + + @NotNull(message = "update user cannot be null") + @Column(name = "UPDATE_USER") + @Size(max = 32) + String updateUser; + + @PastOrPresent + @Column(name = "CREATE_DATE", updatable = false) + LocalDateTime createDate; + + @PastOrPresent + @Column(name = "UPDATE_DATE") + LocalDateTime updateDate; + + @Column(name = "RETRY_COUNT") + private Integer retryCount; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/SagaEventStatesEntity.java b/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/SagaEventStatesEntity.java new file mode 100644 index 0000000..f9d3d42 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/model/v1/SagaEventStatesEntity.java @@ -0,0 +1,104 @@ +package ca.bc.gov.educ.eas.api.model.v1; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * The type Saga event. + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "EAS_SAGA_EVENT_STATES") +@DynamicUpdate +public class SagaEventStatesEntity { + + /** + * The Saga event id. + */ + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator", parameters = { + @org.hibernate.annotations.Parameter(name = "uuid_gen_strategy_class", value = "org.hibernate.id.uuid.CustomVersionOneStrategy")}) + @Column(name = "SAGA_EVENT_ID", unique = true, updatable = false, columnDefinition = "BINARY(16)") + UUID sagaEventId; + + + /** + * The Saga. + */ + @ToString.Exclude + @EqualsAndHashCode.Exclude + @ManyToOne + @JoinColumn(name = "SAGA_ID", updatable = false, columnDefinition = "BINARY(16)") + EasSagaEntity saga; + + /** + * The Saga event state. + */ + @NotNull(message = "saga_event_state cannot be null") + @Column(name = "SAGA_EVENT_STATE") + String sagaEventState; + + /** + * The Saga event outcome. + */ + @NotNull(message = "saga_event_outcome cannot be null") + @Column(name = "SAGA_EVENT_OUTCOME") + String sagaEventOutcome; + + /** + * The Saga step number. + */ + @NotNull(message = "saga_step_number cannot be null") + @Column(name = "SAGA_STEP_NUMBER") + Integer sagaStepNumber; + + /** + * The Saga event response. + */ + @Column(name = "SAGA_EVENT_RESPONSE", length = 10485760) + String sagaEventResponse; + + /** + * The Create user. + */ + @NotNull(message = "create user cannot be null") + @Column(name = "CREATE_USER", updatable = false) + @Size(max = 32) + String createUser; + + /** + * The Update user. + */ + @NotNull(message = "update user cannot be null") + @Column(name = "UPDATE_USER") + @Size(max = 32) + String updateUser; + + /** + * The Create date. + */ + @PastOrPresent + @Column(name = "CREATE_DATE", updatable = false) + LocalDateTime createDate; + + /** + * The Update date. + */ + @PastOrPresent + @Column(name = "UPDATE_DATE") + LocalDateTime updateDate; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/BaseOrchestrator.java b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/BaseOrchestrator.java new file mode 100644 index 0000000..56061c0 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/BaseOrchestrator.java @@ -0,0 +1,621 @@ +package ca.bc.gov.educ.eas.api.orchestrator.base; + +import ca.bc.gov.educ.eas.api.constants.EventOutcome; +import ca.bc.gov.educ.eas.api.constants.EventType; +import ca.bc.gov.educ.eas.api.messaging.MessagePublisher; +import ca.bc.gov.educ.eas.api.model.v1.SagaEventStatesEntity; +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import ca.bc.gov.educ.eas.api.service.v1.SagaService; +import ca.bc.gov.educ.eas.api.struct.Event; +import ca.bc.gov.educ.eas.api.struct.NotificationEvent; +import ca.bc.gov.educ.eas.api.util.JsonUtil; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; + +import static ca.bc.gov.educ.eas.api.constants.EventOutcome.INITIATE_SUCCESS; +import static ca.bc.gov.educ.eas.api.constants.EventOutcome.SAGA_COMPLETED; +import static ca.bc.gov.educ.eas.api.constants.EventType.INITIATED; +import static ca.bc.gov.educ.eas.api.constants.EventType.MARK_SAGA_COMPLETE; +import static ca.bc.gov.educ.eas.api.constants.SagaStatusEnum.COMPLETED; +import static lombok.AccessLevel.PROTECTED; +import static lombok.AccessLevel.PUBLIC; + +/** + * The type Base orchestrator. + * + * @param the type parameter + */ +@Slf4j +public abstract class BaseOrchestrator implements EventHandler, Orchestrator { + /** + * The constant SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT. + */ + protected static final String SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT = "system is going to execute next event :: {} for current event {} and SAGA ID :: {}"; + /** + * The constant SELF + */ + protected static final String SELF = "SELF"; + /** + * The Clazz. + */ + protected final Class clazz; + /** + * The Next steps to execute. + */ + protected final Map>> nextStepsToExecute = new LinkedHashMap<>(); + /** + * The Saga service. + */ + @Getter(PROTECTED) + private final SagaService sagaService; + /** + * The Message publisher. + */ + @Getter(PROTECTED) + private final MessagePublisher messagePublisher; + /** + * The Saga name. + */ + @Getter(PUBLIC) + private final String sagaName; + /** + * The Topic to subscribe. + */ + @Getter(PUBLIC) + private final String topicToSubscribe; + /** + * The flag to indicate whether t + */ + @Setter(PROTECTED) + protected boolean shouldSendNotificationEvent = true; + + /** + * Instantiates a new Base orchestrator. + * + * @param sagaService the saga service + * @param messagePublisher the message publisher + * @param clazz the clazz + * @param sagaName the saga name + * @param topicToSubscribe the topic to subscribe + */ + protected BaseOrchestrator(final SagaService sagaService, final MessagePublisher messagePublisher, + final Class clazz, final String sagaName, + final String topicToSubscribe) { + this.sagaService = sagaService; + this.messagePublisher = messagePublisher; + this.clazz = clazz; + this.sagaName = sagaName; + this.topicToSubscribe = topicToSubscribe; + this.populateStepsToExecuteMap(); + } + + /** + * Create single collection event state list. + * + * @param eventOutcome the event outcome + * @param nextStepPredicate the next step predicate + * @param nextEventType the next event type + * @param stepToExecute the step to execute + * @return the list + */ + protected List> createSingleCollectionEventState(final EventOutcome eventOutcome, final Predicate nextStepPredicate, final EventType nextEventType, final SagaStep stepToExecute) { + final List> eventStates = new ArrayList<>(); + eventStates.add(this.buildSagaEventState(eventOutcome, nextStepPredicate, nextEventType, stepToExecute)); + return eventStates; + } + + + /** + * Build saga event state saga event state. + * + * @param eventOutcome the event outcome + * @param nextStepPredicate the next step predicate + * @param nextEventType the next event type + * @param stepToExecute the step to execute + * @return the saga event state + */ + protected SagaEventState buildSagaEventState(final EventOutcome eventOutcome, final Predicate nextStepPredicate, final EventType nextEventType, final SagaStep stepToExecute) { + return SagaEventState.builder().currentEventOutcome(eventOutcome).nextStepPredicate(nextStepPredicate).nextEventType(nextEventType).stepToExecute(stepToExecute).build(); + } + + + /** + * Register step to execute base orchestrator. + * + * @param initEvent the init event + * @param outcome the outcome + * @param nextStepPredicate the next step predicate + * @param nextEvent the next event + * @param stepToExecute the step to execute + * @return the base orchestrator + */ + protected BaseOrchestrator registerStepToExecute(final EventType initEvent, final EventOutcome outcome, final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + if (this.nextStepsToExecute.containsKey(initEvent)) { + final List> states = this.nextStepsToExecute.get(initEvent); + states.add(this.buildSagaEventState(outcome, nextStepPredicate, nextEvent, stepToExecute)); + } else { + this.nextStepsToExecute.put(initEvent, this.createSingleCollectionEventState(outcome, nextStepPredicate, nextEvent, stepToExecute)); + } + return this; + } + + /** + * Step base orchestrator. + * + * @param currentEvent the event that has occurred. + * @param outcome outcome of the event. + * @param nextEvent next event that will occur. + * @param stepToExecute which method to execute for the next event. it is a lambda function. + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator step(final EventType currentEvent, final EventOutcome outcome, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, nextEvent, stepToExecute); + } + + /** + * Step base orchestrator. + * + * @param currentEvent the event that has occurred. + * @param outcome outcome of the event. + * @param nextStepPredicate whether to execute the next step. + * @param nextEvent next event that will occur. + * @param stepToExecute which method to execute for the next event. it is a lambda function. + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator step(final EventType currentEvent, final EventOutcome outcome, final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, nextStepPredicate, nextEvent, stepToExecute); + } + + /** + * Beginning step base orchestrator. + * + * @param nextEvent next event that will occur. + * @param stepToExecute which method to execute for the next event. it is a lambda function. + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator begin(final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(INITIATED, INITIATE_SUCCESS, (T sagaData) -> true, nextEvent, stepToExecute); + } + + /** + * Beginning step base orchestrator. + * + * @param nextStepPredicate whether to execute the next step. + * @param nextEvent next event that will occur. + * @param stepToExecute which method to execute for the next event. it is a lambda function. + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator begin(final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(INITIATED, INITIATE_SUCCESS, nextStepPredicate, nextEvent, stepToExecute); + } + + /** + * End step base orchestrator with complete status. + * + * @param currentEvent the event that has occurred. + * @param outcome outcome of the event. + */ + public void end(final EventType currentEvent, final EventOutcome outcome) { + this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, MARK_SAGA_COMPLETE, this::markSagaComplete); + } + + /** + * End step with method to execute with complete status. + * + * @param currentEvent the event that has occurred. + * @param outcome outcome of the event. + * @param stepToExecute which method to execute for the MARK_SAGA_COMPLETE event. it is a lambda function. + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator end(final EventType currentEvent, final EventOutcome outcome, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, MARK_SAGA_COMPLETE, (Event event, EasSagaEntity saga, T sagaData) -> { + stepToExecute.apply(event, saga, sagaData); + this.markSagaComplete(event, saga, sagaData); + }); + } + + /** + * Syntax sugar to make the step statement expressive + * + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator or() { + return this; + } + + /** + * this is a simple and convenient method to trigger builder pattern in the child classes. + * + * @return {@link BaseOrchestrator} + */ + public BaseOrchestrator stepBuilder() { + return this; + } + + /** + * this method will check if the event is not already processed. this could happen in SAGAs due to duplicate messages. + * Application should be able to handle this. + * + * @param currentEventType current event. + * @param saga the model object. + * @param eventTypes event types stored in the hashmap + * @return true or false based on whether the current event with outcome received from the queue is already processed or not. + */ + protected boolean isNotProcessedEvent(final EventType currentEventType, final EasSagaEntity saga, final Set eventTypes) { + final EventType eventTypeInDB = EventType.valueOf(saga.getSagaState()); + final List events = new LinkedList<>(eventTypes); + final int dbEventIndex = events.indexOf(eventTypeInDB); + final int currentEventIndex = events.indexOf(currentEventType); + return currentEventIndex >= dbEventIndex; + } + + /** + * creates the PenRequestSagaEventState object + * + * @param saga the payload. + * @param eventType event type + * @param eventOutcome outcome + * @param eventPayload payload. + * @return {@link SagaEventStatesEntity} + */ + protected SagaEventStatesEntity createEventState(@NotNull final EasSagaEntity saga, @NotNull final EventType eventType, @NotNull final EventOutcome eventOutcome, final String eventPayload) { + final var user = this.sagaName.length() > 32 ? this.sagaName.substring(0, 32) : this.sagaName; + return SagaEventStatesEntity.builder() + .createDate(LocalDateTime.now()) + .createUser(user) + .updateDate(LocalDateTime.now()) + .updateUser(user) + .saga(saga) + .sagaEventOutcome(eventOutcome.toString()) + .sagaEventState(eventType.toString()) + .sagaStepNumber(this.calculateStep(saga)) + .sagaEventResponse(StringUtils.isBlank(eventPayload) ? "NO-PAYLOAD-IN-RESPONSE" : eventPayload) + .build(); + } + + /** + * This method updates the DB and marks the process as complete. + * + * @param event the current event. + * @param saga the saga model object. + * @param sagaData the payload string as object. + */ + protected void markSagaComplete(final Event event, final EasSagaEntity saga, final T sagaData) { + this.markSagaComplete(event, saga, sagaData, ""); + } + + /** + * This method updates the DB and marks the process as complete. + * + * @param event the current event. + * @param saga the saga model object. + * @param sagaData the payload string as object. + * @param payloadToSubscribers the event payload to subscribers + */ + protected void markSagaComplete(final Event event, final EasSagaEntity saga, final T sagaData, final String payloadToSubscribers) { + //Added to slow down complete write + try { + Thread.sleep(1000); + }catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + log.trace("payload is {}", sagaData); + if (this.shouldSendNotificationEvent) { + final var finalEvent = new NotificationEvent(); + BeanUtils.copyProperties(event, finalEvent); + finalEvent.setEventType(MARK_SAGA_COMPLETE); + finalEvent.setEventOutcome(SAGA_COMPLETED); + finalEvent.setSagaStatus(COMPLETED.toString()); + + finalEvent.setSagaName(this.getSagaName()); + finalEvent.setEventPayload(payloadToSubscribers); + this.postMessageToTopic(this.getTopicToSubscribe(), finalEvent); + } + + final SagaEventStatesEntity sagaEventStates = this.createEventState(saga, event.getEventType(), event.getEventOutcome(), event.getEventPayload()); + saga.setSagaState(COMPLETED.toString()); + saga.setStatus(COMPLETED.toString()); + saga.setUpdateDate(LocalDateTime.now()); + this.getSagaService().updateAttachedSagaWithEvents(saga, sagaEventStates); + + } + + /** + * calculate step number + * + * @param saga the model object. + * @return step number that was calculated. + */ + private int calculateStep(final EasSagaEntity saga) { + val sagaStates = this.getSagaService().findAllSagaStates(saga); + return (sagaStates.size() + 1); + } + + /** + * convenient method to post message to topic, to be used by child classes. + * + * @param topicName topic name where the message will be posted. + * @param nextEvent the next event object. + */ + protected void postMessageToTopic(final String topicName, final Event nextEvent) { + final var eventStringOptional = JsonUtil.getJsonString(nextEvent); + if (eventStringOptional.isPresent()) { + this.getMessagePublisher().dispatchMessage(topicName, eventStringOptional.get().getBytes()); + } else { + log.error("event string is not present for :: {} :: this should not have happened", nextEvent); + } + } + + /** + * it finds the last event that was processed successfully for this saga. + * + * @param eventStates event states corresponding to the Saga. + * @return {@link SagaEventStatesEntity} if found else null. + */ + protected Optional findTheLastEventOccurred(final List eventStates) { + final int step = eventStates.stream().map(SagaEventStatesEntity::getSagaStepNumber).mapToInt(x -> x).max().orElse(0); + return eventStates.stream().filter(element -> element.getSagaStepNumber() == step).findFirst(); + } + + /** + * this method is called from the cron job , which will replay the saga process based on its current state. + * + * @param saga the model object. + * @throws IOException if there is connectivity problem + * @throws InterruptedException if thread is interrupted. + * @throws TimeoutException if connection to messaging system times out. + */ + @Override + @Transactional + @Async("sagaRetryTaskExecutor") + public void replaySaga(final EasSagaEntity saga) throws IOException, InterruptedException, TimeoutException { + final var eventStates = this.getSagaService().findAllSagaStates(saga); + final var t = JsonUtil.getJsonObjectFromString(this.clazz, saga.getPayload()); + if (eventStates.isEmpty()) { //process did not start last time, lets start from beginning. + this.replayFromBeginning(saga, t); + } else { + this.replayFromLastEvent(saga, eventStates, t); + } + } + + /** + * This method will restart the saga process from where it was left the last time. which could occur due to various reasons + * + * @param saga the model object. + * @param eventStates the event states corresponding to the saga + * @param t the payload string as an object + * @throws InterruptedException if thread is interrupted. + * @throws TimeoutException if connection to messaging system times out. + * @throws IOException if there is connectivity problem + */ + private void replayFromLastEvent(final EasSagaEntity saga, final List eventStates, final T t) throws InterruptedException, TimeoutException, IOException { + val sagaEventOptional = this.findTheLastEventOccurred(eventStates); + if (sagaEventOptional.isPresent()) { + val sagaEvent = sagaEventOptional.get(); + log.trace(sagaEventOptional.toString()); + final EventType currentEvent = EventType.valueOf(sagaEvent.getSagaEventState()); + final EventOutcome eventOutcome = EventOutcome.valueOf(sagaEvent.getSagaEventOutcome()); + final Event event = Event.builder() + .eventOutcome(eventOutcome) + .eventType(currentEvent) + .eventPayload(sagaEvent.getSagaEventResponse()) + .build(); + this.findAndInvokeNextStep(saga, t, currentEvent, eventOutcome, event); + } + } + + /** + * Find and invoke next step. + * + * @param saga the saga + * @param t the t + * @param currentEvent the current event + * @param eventOutcome the event outcome + * @param event the event + * @throws InterruptedException the interrupted exception + * @throws TimeoutException the timeout exception + * @throws IOException the io exception + */ + private void findAndInvokeNextStep(final EasSagaEntity saga, final T t, final EventType currentEvent, final EventOutcome eventOutcome, final Event event) throws InterruptedException, TimeoutException, IOException { + final Optional> sagaEventState = this.findNextSagaEventState(currentEvent, eventOutcome, t); + if (sagaEventState.isPresent()) { + log.trace(SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT, sagaEventState.get().getNextEventType(), event.toString(), saga.getSagaId()); + this.invokeNextEvent(event, saga, t, sagaEventState.get()); + } + } + + /** + * This method will restart the saga process from the beginning. which could occur due to various reasons + * + * @param saga the model object. + * @param t the payload string as an object + * @throws InterruptedException if thread is interrupted. + * @throws TimeoutException if connection to messaging system times out. + * @throws IOException if there is connectivity problem + */ + private void replayFromBeginning(final EasSagaEntity saga, final T t) throws InterruptedException, TimeoutException, IOException { + final Event event = Event.builder() + .eventOutcome(INITIATE_SUCCESS) + .eventType(INITIATED) + .build(); + this.findAndInvokeNextStep(saga, t, INITIATED, INITIATE_SUCCESS, event); + } + + /** + * this method is called if there is a new message on this specific topic which this service is listening. + * + * @param event the event + * @throws InterruptedException if thread is interrupted. + * @throws IOException if there is connectivity problem + * @throws TimeoutException if connection to messaging system times out. + */ + @Override + @Async("subscriberExecutor") + @Transactional + public void handleEvent(@NotNull final Event event) throws InterruptedException, IOException, TimeoutException { + log.debug("Executing saga event {}", event); + if (this.sagaEventExecutionNotRequired(event)) { + log.trace("Execution is not required for this message returning EVENT is :: {}", event); + return; + } + this.broadcastSagaInitiatedMessage(event); + + log.debug("About to find saga by ID with event :: {}", event); + final var sagaOptional = this.getSagaService().findSagaById(event.getSagaId()); // system expects a saga record to be present here. + if (sagaOptional.isPresent()) { + val saga = sagaOptional.get(); + if (!COMPLETED.toString().equalsIgnoreCase(sagaOptional.get().getStatus())) {//possible duplicate message or force stop scenario check + final T sagaData = JsonUtil.getJsonObjectFromString(this.clazz, saga.getPayload()); + final var sagaEventState = this.findNextSagaEventState(event.getEventType(), event.getEventOutcome(), sagaData); + log.trace("found next event as {}", sagaEventState); + if (sagaEventState.isPresent()) { + this.process(event, saga, sagaData, sagaEventState.get()); + } else { + log.error("This should not have happened, please check that both the saga api and all the participating apis are in sync in terms of events and their outcomes. {}", event); // more explicit error message, + } + } else { + log.debug("Got message to process saga for saga ID :: {} but saga is already :: {}", saga.getSagaId(), saga.getStatus()); + } + } else { + log.error("Saga process without DB record is not expected. {}", event); + } + } + + /** + * Start to execute saga + * + * @param saga the saga data + */ + @Override + @Async("subscriberExecutor") + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void startSaga(@NotNull final EasSagaEntity saga) { + try { + log.debug("Starting saga with the following payload :: {}", saga); + this.handleEvent(Event.builder() + .eventType(EventType.INITIATED) + .eventOutcome(EventOutcome.INITIATE_SUCCESS) + .sagaId(saga.getSagaId()) + .eventPayload(saga.getPayload()) + .build()); + } catch (final InterruptedException e) { + log.error("InterruptedException while startSaga", e); + Thread.currentThread().interrupt(); + } catch (final TimeoutException | IOException e) { + log.error("Exception while startSaga", e); + } + } + + @Override + @Transactional + public EasSagaEntity createSaga(@NotNull final String payload, final String userName) { + return this.sagaService.createSagaRecordInDB(this.sagaName, userName, payload); + } + + @Transactional + public List createSagas(final List sagaEntities) { + return this.sagaService.createSagaRecordsInDB(sagaEntities); + } + + /** + * DONT DO ANYTHING the message was broad-casted for the frontend listeners, that a saga process has initiated, completed. + * + * @param event the event object received from queue. + * @return true if this message need not be processed further. + */ + private boolean sagaEventExecutionNotRequired(@NotNull final Event event) { + return (event.getEventType() == INITIATED && event.getEventOutcome() == INITIATE_SUCCESS && SELF.equalsIgnoreCase(event.getReplyTo())) + || event.getEventType() == MARK_SAGA_COMPLETE && event.getEventOutcome() == SAGA_COMPLETED; + } + + /** + * Broadcast the saga initiated message + * + * @param event the event object + */ + private void broadcastSagaInitiatedMessage(@NotNull final Event event) { + // !SELF.equalsIgnoreCase(event.getReplyTo()):- this check makes sure it is not broadcast-ed infinitely. + if (this.shouldSendNotificationEvent && event.getEventType() == INITIATED && event.getEventOutcome() == INITIATE_SUCCESS + && !SELF.equalsIgnoreCase(event.getReplyTo())) { + final var notificationEvent = new NotificationEvent(); + BeanUtils.copyProperties(event, notificationEvent); + notificationEvent.setSagaStatus(INITIATED.toString()); + notificationEvent.setReplyTo(SELF); + notificationEvent.setSagaName(this.getSagaName()); + this.postMessageToTopic(this.getTopicToSubscribe(), notificationEvent); + } + } + + /** + * this method finds the next event that needs to be executed. + * + * @param currentEvent current event + * @param eventOutcome event outcome. + * @param sagaData the saga data + * @return {@link Optional} + */ + protected Optional> findNextSagaEventState(final EventType currentEvent, final EventOutcome eventOutcome, final T sagaData) { + val sagaEventStates = this.nextStepsToExecute.get(currentEvent); + return sagaEventStates == null ? Optional.empty() : sagaEventStates.stream().filter(el -> + el.getCurrentEventOutcome() == eventOutcome && el.nextStepPredicate.test(sagaData) + ).findFirst(); + } + + /** + * this method starts the process of saga event execution. + * + * @param event the current event. + * @param saga the model object. + * @param sagaData the saga data + * @param sagaEventState the next next event from {@link BaseOrchestrator#nextStepsToExecute} + * @throws InterruptedException if thread is interrupted. + * @throws TimeoutException if connection to messaging system times out. + * @throws IOException if there is connectivity problem + */ + protected void process(@NotNull final Event event, final EasSagaEntity saga, final T sagaData, final SagaEventState sagaEventState) throws InterruptedException, TimeoutException, IOException { + if (!saga.getSagaState().equalsIgnoreCase(COMPLETED.toString()) + && this.isNotProcessedEvent(event.getEventType(), saga, this.nextStepsToExecute.keySet())) { + log.debug(SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT, sagaEventState.getNextEventType(), event, saga.getSagaId()); + this.invokeNextEvent(event, saga, sagaData, sagaEventState); + } else { + log.debug("Ignoring this message as we have already processed it or it is completed. {}", event.toString()); // it is expected to receive duplicate message in saga pattern, system should be designed to handle duplicates. + } + } + + /** + * this method will invoke the next event in the {@link BaseOrchestrator#nextStepsToExecute} + * + * @param event the current event. + * @param saga the model object. + * @param sagaData the payload string + * @param sagaEventState the next next event from {@link BaseOrchestrator#nextStepsToExecute} + * @throws InterruptedException if thread is interrupted. + * @throws TimeoutException if connection to messaging system times out. + * @throws IOException if there is connectivity problem + */ + protected void invokeNextEvent(final Event event, final EasSagaEntity saga, final T sagaData, final SagaEventState sagaEventState) throws InterruptedException, TimeoutException, IOException { + final SagaStep stepToExecute = sagaEventState.getStepToExecute(); + stepToExecute.apply(event, saga, sagaData); + } + + /** + * Populate steps to execute map. + */ + public abstract void populateStepsToExecuteMap(); + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/EventHandler.java b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/EventHandler.java new file mode 100644 index 0000000..09c034c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/EventHandler.java @@ -0,0 +1,29 @@ +package ca.bc.gov.educ.eas.api.orchestrator.base; + +import ca.bc.gov.educ.eas.api.struct.Event; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + + +/** + * The interface event handler. + */ +public interface EventHandler { + /** + * On event. + * + * @param event the event + * @throws InterruptedException the interrupted exception + * @throws IOException the io exception + * @throws TimeoutException the timeout exception + */ + void handleEvent(Event event) throws InterruptedException, IOException, TimeoutException; + + /** + * Get message topic to subscribe the handler to MessageSubscriber + * + * @return the topic to subscribe + */ + String getTopicToSubscribe(); +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/Orchestrator.java b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/Orchestrator.java new file mode 100644 index 0000000..112897d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/Orchestrator.java @@ -0,0 +1,46 @@ +package ca.bc.gov.educ.eas.api.orchestrator.base; + +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * The interface Orchestrator. + */ +public interface Orchestrator { + + + /** + * Gets saga name. + * + * @return the saga name + */ + String getSagaName(); + + /** + * Start saga. + * + * @param saga the saga data + */ + void startSaga(EasSagaEntity saga); + + /** + * create saga. + * + * @param payload the payload + * @param userName the user who created the saga + * @return the saga + */ + EasSagaEntity createSaga(String payload, String userName); + + /** + * Replay saga. + * + * @param saga the saga + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + * @throws TimeoutException the timeout exception + */ + void replaySaga(EasSagaEntity saga) throws IOException, InterruptedException, TimeoutException; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaEventState.java b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaEventState.java new file mode 100644 index 0000000..d7a7de4 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaEventState.java @@ -0,0 +1,38 @@ +package ca.bc.gov.educ.eas.api.orchestrator.base; + +import ca.bc.gov.educ.eas.api.constants.EventOutcome; +import ca.bc.gov.educ.eas.api.constants.EventType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.function.Predicate; + +/** + * The type Saga event state. + * + * @param the type parameter + */ +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Data +public class SagaEventState { + /** + * The Current event outcome. + */ + EventOutcome currentEventOutcome; + /** + * The function to check the next step + */ + Predicate nextStepPredicate; + /** + * The Next event type. + */ + EventType nextEventType; + /** + * The Step to execute. + */ + SagaStep stepToExecute; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaStep.java b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaStep.java new file mode 100644 index 0000000..1af62e9 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/orchestrator/base/SagaStep.java @@ -0,0 +1,28 @@ +package ca.bc.gov.educ.eas.api.orchestrator.base; + + +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import ca.bc.gov.educ.eas.api.struct.Event; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * The interface Saga step. + * + * @param the type parameter + */ +@FunctionalInterface +public interface SagaStep { + /** + * Apply. + * + * @param event the event + * @param saga the saga + * @param sagaData the saga data + * @throws InterruptedException the interrupted exception + * @throws TimeoutException the timeout exception + * @throws IOException the io exception + */ + void apply(Event event, EasSagaEntity saga, T sagaData) throws InterruptedException, TimeoutException, IOException; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/properties/ApplicationProperties.java b/api/src/main/java/ca/bc/gov/educ/eas/api/properties/ApplicationProperties.java new file mode 100644 index 0000000..af59b5f --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/properties/ApplicationProperties.java @@ -0,0 +1,63 @@ +package ca.bc.gov.educ.eas.api.properties; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import lombok.Getter; +import lombok.Setter; +import org.jboss.threads.EnhancedQueueExecutor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.concurrent.Executor; + +/** + * Class holds all application properties + * + */ +@Component +@Getter +@Setter +public class ApplicationProperties { + public static final Executor bgTask = new EnhancedQueueExecutor.Builder() + .setThreadFactory(new ThreadFactoryBuilder().setNameFormat("bg-task-executor-%d").build()) + .setCorePoolSize(1).setMaximumPoolSize(1).setKeepAliveTime(Duration.ofSeconds(60)).build(); + public static final String EAS_API = "EAS_API"; + public static final String CORRELATION_ID = "correlationID"; + /** + * The Client id. + */ + @Value("${client.id}") + private String clientID; + /** + * The Client secret. + */ + @Value("${client.secret}") + private String clientSecret; + /** + * The Token url. + */ + @Value("${url.token}") + private String tokenURL; + + @Value("${nats.server}") + private String server; + + @Value("${nats.maxReconnect}") + private int maxReconnect; + + @Value("${nats.connectionName}") + private String connectionName; + + @Value("${threads.min.subscriber}") + private Integer minSubscriberThreads; + @Value("${threads.max.subscriber}") + private Integer maxSubscriberThreads; + @Value("${sagas.max.pending}") + private Integer maxPendingSagas; + @Value("${sagas.max.parallel}") + private Integer maxParallelSagas; + @Value("${url.api.institute}") + private String instituteApiURL; + @Value("${url.api.edx}") + private String edxApiURL; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaEventRepository.java b/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaEventRepository.java new file mode 100644 index 0000000..391d8a2 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaEventRepository.java @@ -0,0 +1,45 @@ +package ca.bc.gov.educ.eas.api.repository.v1; + + +import ca.bc.gov.educ.eas.api.model.v1.SagaEventStatesEntity; +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * The interface Saga event repository. + */ +@Repository +public interface SagaEventRepository extends JpaRepository { + /** + * Find by saga list. + * + * @param saga the saga + * @return the list + */ + List findBySaga(EasSagaEntity saga); + + /** + * Find by saga and saga event outcome and saga event state and saga step number optional. + * + * @param saga the saga + * @param eventOutcome the event outcome + * @param eventState the event state + * @param stepNumber the step number + * @return the optional + */ + Optional findBySagaAndSagaEventOutcomeAndSagaEventStateAndSagaStepNumber(EasSagaEntity saga, String eventOutcome, String eventState, int stepNumber); + + @Transactional + @Modifying + @Query(value = "delete from EAS_SAGA_EVENT_STATES e where exists(select 1 from EAS_SAGA s where s.SAGA_ID = e.SAGA_ID and s.CREATE_DATE <= :createDate)", nativeQuery = true) + void deleteBySagaCreateDateBefore(LocalDateTime createDate); +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaRepository.java b/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaRepository.java new file mode 100644 index 0000000..61431cd --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/repository/v1/SagaRepository.java @@ -0,0 +1,25 @@ +package ca.bc.gov.educ.eas.api.repository.v1; + + +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * The interface Saga repository. + */ +@Repository +public interface SagaRepository extends JpaRepository, JpaSpecificationExecutor { + + @Transactional + @Modifying + @Query("delete from EasSagaEntity where createDate <= :createDate") + void deleteByCreateDateBefore(LocalDateTime createDate); +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java new file mode 100644 index 0000000..f2e78b6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java @@ -0,0 +1,365 @@ +package ca.bc.gov.educ.eas.api.rest; + +import ca.bc.gov.educ.eas.api.constants.EventType; +import ca.bc.gov.educ.eas.api.constants.TopicsEnum; +import ca.bc.gov.educ.eas.api.exception.EasAPIRuntimeException; +import ca.bc.gov.educ.eas.api.messaging.MessagePublisher; +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import ca.bc.gov.educ.eas.api.struct.Event; +import ca.bc.gov.educ.eas.api.struct.external.institute.v1.*; +import ca.bc.gov.educ.eas.api.util.JsonUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class is used for REST calls + * + */ +@Component +@Slf4j +public class RestUtils { + public static final String NATS_TIMEOUT = "Either NATS timed out or the response is null , correlationID :: "; + private static final String CONTENT_TYPE = "Content-Type"; + private final Map authorityMap = new ConcurrentHashMap<>(); + private final Map schoolMap = new ConcurrentHashMap<>(); + private final Map schoolMincodeMap = new ConcurrentHashMap<>(); + private final Map districtMap = new ConcurrentHashMap<>(); + private final Map allSchoolMap = new ConcurrentHashMap<>(); + private final Map facilityTypeCodesMap = new ConcurrentHashMap<>(); + private final Map schoolCategoryCodesMap = new ConcurrentHashMap<>(); + public static final String PAGE_SIZE = "pageSize"; + private final WebClient webClient; + private final MessagePublisher messagePublisher; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ReadWriteLock facilityTypesLock = new ReentrantReadWriteLock(); + private final ReadWriteLock schoolCategoriesLock = new ReentrantReadWriteLock(); + private final ReadWriteLock authorityLock = new ReentrantReadWriteLock(); + private final ReadWriteLock schoolLock = new ReentrantReadWriteLock(); + private final ReadWriteLock districtLock = new ReentrantReadWriteLock(); + private final ReadWriteLock allSchoolLock = new ReentrantReadWriteLock(); + @Getter + private final ApplicationProperties props; + + @Value("${initialization.background.enabled}") + private Boolean isBackgroundInitializationEnabled; + + private final Map> independentAuthorityToSchoolIDMap = new ConcurrentHashMap<>(); + + @Autowired + public RestUtils(WebClient webClient, final ApplicationProperties props, final MessagePublisher messagePublisher) { + this.webClient = webClient; + this.props = props; + this.messagePublisher = messagePublisher; + } + + @PostConstruct + public void init() { + if (this.isBackgroundInitializationEnabled != null && this.isBackgroundInitializationEnabled) { + ApplicationProperties.bgTask.execute(this::initialize); + } + } + + private void initialize() { + this.populateAllSchoolMap(); + this.populateSchoolCategoryCodesMap(); + this.populateFacilityTypeCodesMap(); + this.populateSchoolMap(); + this.populateSchoolMincodeMap(); + this.populateDistrictMap(); + this.populateAuthorityMap(); + } + + @Scheduled(cron = "${schedule.jobs.load.school.cron}") + public void scheduled() { + this.init(); + } + + public void populateAuthorityMap() { + val writeLock = this.authorityLock.writeLock(); + try { + writeLock.lock(); + for (val authority : this.getAuthorities()) { + this.authorityMap.put(authority.getIndependentAuthorityId(), authority); + } + } catch (Exception ex) { + log.error("Unable to load map cache authorities ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} authorities to memory", this.authorityMap.values().size()); + } + + public void populateSchoolCategoryCodesMap() { + val writeLock = this.schoolCategoriesLock.writeLock(); + try { + writeLock.lock(); + for (val categoryCode : this.getSchoolCategoryCodes()) { + this.schoolCategoryCodesMap.put(categoryCode.getSchoolCategoryCode(), categoryCode); + } + } catch (Exception ex) { + log.error("Unable to load map cache school categories ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} school categories to memory", this.schoolCategoryCodesMap.values().size()); + } + + public void populateFacilityTypeCodesMap() { + val writeLock = this.facilityTypesLock.writeLock(); + try { + writeLock.lock(); + for (val categoryCode : this.getFacilityTypeCodes()) { + this.facilityTypeCodesMap.put(categoryCode.getFacilityTypeCode(), categoryCode); + } + } catch (Exception ex) { + log.error("Unable to load map cache facility types ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} facility types to memory", this.facilityTypeCodesMap.values().size()); + } + + public void populateSchoolMap() { + val writeLock = this.schoolLock.writeLock(); + try { + writeLock.lock(); + for (val school : this.getSchools()) { + this.schoolMap.put(school.getSchoolId(), school); + if (StringUtils.isNotBlank(school.getIndependentAuthorityId())) { + this.independentAuthorityToSchoolIDMap.computeIfAbsent(school.getIndependentAuthorityId(), k -> new ArrayList<>()).add(UUID.fromString(school.getSchoolId())); + } + } + } catch (Exception ex) { + log.error("Unable to load map cache school ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} schools to memory", this.schoolMap.values().size()); + } + + public void populateSchoolMincodeMap() { + val writeLock = this.schoolLock.writeLock(); + try { + writeLock.lock(); + for (val school : this.getSchools()) { + this.schoolMincodeMap.put(school.getMincode(), school); + if (StringUtils.isNotBlank(school.getIndependentAuthorityId())) { + this.independentAuthorityToSchoolIDMap.computeIfAbsent(school.getIndependentAuthorityId(), k -> new ArrayList<>()).add(UUID.fromString(school.getSchoolId())); + } + } + } catch (Exception ex) { + log.error("Unable to load map cache school mincodes ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} school mincodes to memory", this.schoolMincodeMap.values().size()); + } + + public List getSchools() { + log.info("Calling Institute api to load schools to memory"); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/school") + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(SchoolTombstone.class) + .collectList() + .block(); + } + + public List getAuthorities() { + log.info("Calling Institute api to load authority to memory"); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/authority") + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(IndependentAuthority.class) + .collectList() + .block(); + } + + public List getSchoolCategoryCodes() { + log.info("Calling Institute api to load school categories to memory"); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/category-codes") + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(SchoolCategoryCode.class) + .collectList() + .block(); + } + + public List getFacilityTypeCodes() { + log.info("Calling Institute api to load facility type codes to memory"); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/facility-codes") + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(FacilityTypeCode.class) + .collectList() + .block(); + } + + public School getSchoolDetails(UUID schoolID) { + log.debug("Retrieving school by ID: {}", schoolID); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/school/" + schoolID) + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(School.class) + .blockFirst(); + } + + public void populateDistrictMap() { + val writeLock = this.districtLock.writeLock(); + try { + writeLock.lock(); + for (val district : this.getDistricts()) { + this.districtMap.put(district.getDistrictId(), district); + } + } catch (Exception ex) { + log.error("Unable to load map cache district ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} districts to memory", this.districtMap.values().size()); + } + + public List getDistricts() { + log.info("Calling Institute api to load districts to memory"); + return this.webClient.get() + .uri(this.props.getInstituteApiURL() + "/district") + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToFlux(District.class) + .collectList() + .block(); + } + + public Optional getSchoolCategoryCode(final String schoolCategoryCode) { + if (this.schoolCategoryCodesMap.isEmpty()) { + log.info("School categories map is empty reloading them"); + this.populateSchoolCategoryCodesMap(); + } + return Optional.ofNullable(this.schoolCategoryCodesMap.get(schoolCategoryCode)); + } + + public Optional getFacilityTypeCode(final String facilityTypeCode) { + if (this.facilityTypeCodesMap.isEmpty()) { + log.info("Facility types map is empty reloading them"); + this.populateFacilityTypeCodesMap(); + } + return Optional.ofNullable(this.facilityTypeCodesMap.get(facilityTypeCode)); + } + + public Optional getSchoolBySchoolID(final String schoolID) { + if (this.schoolMap.isEmpty()) { + log.info("School map is empty reloading schools"); + this.populateSchoolMap(); + } + return Optional.ofNullable(this.schoolMap.get(schoolID)); + } + + public Optional getAuthorityByAuthorityID(final String authorityID) { + if (this.authorityMap.isEmpty()) { + log.info("Authority map is empty reloading authorities"); + this.populateAuthorityMap(); + } + return Optional.ofNullable(this.authorityMap.get(authorityID)); + } + + public Optional getSchoolByMincode(final String mincode) { + if (this.schoolMincodeMap.isEmpty()) { + log.info("School mincode map is empty reloading schools"); + this.populateSchoolMincodeMap(); + } + return Optional.ofNullable(this.schoolMincodeMap.get(mincode)); + } + + public List getSchoolFundingGroupsBySchoolID(final String schoolID) { + if (this.allSchoolMap.isEmpty()) { + log.info("All schools map is empty reloading schools"); + this.populateAllSchoolMap(); + } + if(!this.allSchoolMap.containsKey(schoolID)){ + return new ArrayList<>(); + } + return this.allSchoolMap.get(schoolID).getSchoolFundingGroups(); + } + + public Optional getDistrictByDistrictID(final String districtID) { + if (this.districtMap.isEmpty()) { + log.info("District map is empty reloading schools"); + this.populateDistrictMap(); + } + return Optional.ofNullable(this.districtMap.get(districtID)); + } + + public Optional> getSchoolIDsByIndependentAuthorityID(final String independentAuthorityID) { + if (this.independentAuthorityToSchoolIDMap.isEmpty()) { + log.info("The map is empty reloading schools"); + this.populateSchoolMap(); + } + return Optional.ofNullable(this.independentAuthorityToSchoolIDMap.get(independentAuthorityID)); + } + + public void populateAllSchoolMap() { + val writeLock = this.allSchoolLock.writeLock(); + try { + writeLock.lock(); + List allSchools = this.getAllSchoolList(UUID.randomUUID()); + for (val school : allSchools) { + this.allSchoolMap.put(school.getSchoolId(), school); + } + } catch (Exception ex) { + log.error("Unable to load map cache for allSchool ", ex); + } finally { + writeLock.unlock(); + } + log.info("Loaded {} allSchools to memory", this.allSchoolMap.values().size()); + } + + public Optional getAllSchoolBySchoolID(final String schoolID) { + if (this.allSchoolMap.isEmpty()) { + log.info("All School map is empty reloading schools"); + this.populateAllSchoolMap(); + } + return Optional.ofNullable(this.allSchoolMap.get(schoolID)); + } + + @Retryable(retryFor = {Exception.class}, noRetryFor = {EasAPIRuntimeException.class}, backoff = @Backoff(multiplier = 2, delay = 2000)) + public List getAllSchoolList(UUID correlationID) { + try { + log.info("Calling Institute api to load all schools to memory"); + final TypeReference> ref = new TypeReference<>() { + }; + val event = Event.builder().sagaId(correlationID).eventType(EventType.GET_PAGINATED_SCHOOLS).eventPayload(PAGE_SIZE.concat("=").concat("100000")).build(); + val responseMessage = this.messagePublisher.requestMessage(TopicsEnum.INSTITUTE_API_TOPIC.toString(), JsonUtil.getJsonBytesFromObject(event)).completeOnTimeout(null, 60, TimeUnit.SECONDS).get(); + if (null != responseMessage) { + return objectMapper.readValue(responseMessage.getData(), ref); + } else { + throw new EasAPIRuntimeException(NATS_TIMEOUT + correlationID); + } + } catch (final Exception ex) { + Thread.currentThread().interrupt(); + throw new EasAPIRuntimeException(NATS_TIMEOUT + correlationID + ex.getMessage()); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestWebClient.java b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestWebClient.java new file mode 100644 index 0000000..05d3cda --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestWebClient.java @@ -0,0 +1,89 @@ +package ca.bc.gov.educ.eas.api.rest; + +import ca.bc.gov.educ.eas.api.helpers.LogHelper; +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; +import reactor.netty.http.client.HttpClient; + +/** + * The type Rest web client. + */ +@Configuration +@Profile("!test") +public class RestWebClient { + private final DefaultUriBuilderFactory factory; + private final ClientHttpConnector connector; + /** + * The Props. + */ + private final ApplicationProperties props; + + /** + * Instantiates a new Rest web client. + * + * @param props the props + */ + public RestWebClient(final ApplicationProperties props) { + this.props = props; + this.factory = new DefaultUriBuilderFactory(); + this.factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + final HttpClient client = HttpClient.create().compress(true); + client.warmup() + .block(); + this.connector = new ReactorClientHttpConnector(client); + } + + /** + * Web client web client. + * + * @return the web client + */ + @Bean + @Autowired + WebClient webClient(final WebClient.Builder builder) { + val clientRegistryRepo = new InMemoryReactiveClientRegistrationRepository(ClientRegistration + .withRegistrationId(this.props.getClientID()) + .tokenUri(this.props.getTokenURL()) + .clientId(this.props.getClientID()) + .clientSecret(this.props.getClientSecret()) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build()); + val clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo); + val authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService); + val oauthFilter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauthFilter.setDefaultClientRegistrationId(this.props.getClientID()); + return builder + .defaultHeader("X-Client-Name", ApplicationProperties.EAS_API) + .codecs(configurer -> configurer + .defaultCodecs() + .maxInMemorySize(100 * 1024 * 1024)) + .filter(this.log()) + .clientConnector(this.connector) + .uriBuilderFactory(this.factory) + .filter(oauthFilter) + .build(); + } + + private ExchangeFilterFunction log() { + return (clientRequest, next) -> + next + .exchange(clientRequest) + .doOnNext((clientResponse -> LogHelper.logClientHttpReqResponseDetails(clientRequest.method(), clientRequest.url().toString(), clientResponse.rawStatusCode(), clientRequest.headers().get(ApplicationProperties.CORRELATION_ID)))); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/schedulers/PurgeOldSagaRecordsScheduler.java b/api/src/main/java/ca/bc/gov/educ/eas/api/schedulers/PurgeOldSagaRecordsScheduler.java new file mode 100644 index 0000000..6b0b3c7 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/schedulers/PurgeOldSagaRecordsScheduler.java @@ -0,0 +1,56 @@ +package ca.bc.gov.educ.eas.api.schedulers; + +import ca.bc.gov.educ.eas.api.repository.v1.SagaEventRepository; +import ca.bc.gov.educ.eas.api.repository.v1.SagaRepository; +import jakarta.transaction.Transactional; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.core.LockAssert; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +import static lombok.AccessLevel.PRIVATE; + +@Component +@Slf4j +public class PurgeOldSagaRecordsScheduler { + @Getter(PRIVATE) + private final SagaRepository sagaRepository; + + @Getter(PRIVATE) + private final SagaEventRepository sagaEventRepository; + + @Value("${purge.records.saga.after.days}") + @Setter + @Getter + Integer sagaRecordStaleInDays; + + public PurgeOldSagaRecordsScheduler(final SagaRepository sagaRepository, final SagaEventRepository sagaEventRepository) { + this.sagaRepository = sagaRepository; + this.sagaEventRepository = sagaEventRepository; + } + + + /** + * run the job based on configured scheduler(a cron expression) and purge old records from DB. + */ + @Scheduled(cron = "${scheduled.jobs.purge.old.saga.records.cron}") + @SchedulerLock(name = "PurgeOldSagaRecordsLock", lockAtLeastFor = "55s", lockAtMostFor = "57s") + @Transactional + public void pollSagaTableAndPurgeOldRecords() { + LockAssert.assertLocked(); + final LocalDateTime createDateToCompare = this.calculateCreateDateBasedOnStaleSagaRecordInDays(); + this.sagaEventRepository.deleteBySagaCreateDateBefore(createDateToCompare); + this.sagaRepository.deleteByCreateDateBefore(createDateToCompare); + log.info("Purged old saga and event records from EDUC-STUDENT-DATA-COLLECTION-SAGA-API"); + } + + private LocalDateTime calculateCreateDateBasedOnStaleSagaRecordInDays() { + return LocalDateTime.now().minusDays(this.getSagaRecordStaleInDays()); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/SagaService.java b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/SagaService.java new file mode 100644 index 0000000..531d42d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/SagaService.java @@ -0,0 +1,187 @@ +package ca.bc.gov.educ.eas.api.service.v1; + +import ca.bc.gov.educ.eas.api.model.v1.SagaEventStatesEntity; +import ca.bc.gov.educ.eas.api.model.v1.EasSagaEntity; +import ca.bc.gov.educ.eas.api.repository.v1.SagaEventRepository; +import ca.bc.gov.educ.eas.api.repository.v1.SagaRepository; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static ca.bc.gov.educ.eas.api.constants.EventType.INITIATED; +import static ca.bc.gov.educ.eas.api.constants.SagaStatusEnum.STARTED; +import static lombok.AccessLevel.PRIVATE; + +/** + * The type Saga service. + */ +@Service +@Slf4j +public class SagaService { + /** + * The Saga repository. + */ + @Getter(AccessLevel.PRIVATE) + private final SagaRepository sagaRepository; + /** + * The Saga event repository. + */ + @Getter(PRIVATE) + private final SagaEventRepository sagaEventRepository; + + /** + * Instantiates a new Saga service. + * + * @param sagaRepository the saga repository + * @param sagaEventRepository the saga event repository + */ + @Autowired + public SagaService(final SagaRepository sagaRepository, final SagaEventRepository sagaEventRepository) { + this.sagaRepository = sagaRepository; + this.sagaEventRepository = sagaEventRepository; + } + + + /** + * Create saga record saga. + * + * @param saga the saga + * @return the saga + */ + @Transactional(propagation = Propagation.MANDATORY) + public EasSagaEntity createSagaRecord(final EasSagaEntity saga) { + return this.getSagaRepository().save(saga); + } + + /** + * Create saga records. + * + * @param sagas the sagas + * @return the saga + */ + @Transactional(propagation = Propagation.MANDATORY) + public List createSagaRecords(final List sagas) { + return this.getSagaRepository().saveAll(sagas); + } + + /** + * no need to do a get here as it is an attached entity + * first find the child record, if exist do not add. this scenario may occur in replay process, + * so dont remove this check. removing this check will lead to duplicate records in the child table. + * + * @param saga the saga object. + * @param sagaEventStates the saga event + */ + @Retryable(maxAttempts = 5, backoff = @Backoff(multiplier = 2, delay = 2000)) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateAttachedSagaWithEvents(final EasSagaEntity saga, final SagaEventStatesEntity sagaEventStates) { + saga.setUpdateDate(LocalDateTime.now()); + this.getSagaRepository().save(saga); + val result = this.getSagaEventRepository() + .findBySagaAndSagaEventOutcomeAndSagaEventStateAndSagaStepNumber(saga, sagaEventStates.getSagaEventOutcome(), sagaEventStates.getSagaEventState(), sagaEventStates.getSagaStepNumber() - 1); //check if the previous step was same and had same outcome, and it is due to replay. + if (result.isEmpty()) { + this.getSagaEventRepository().save(sagaEventStates); + } + } + + /** + * Find saga by id optional. + * + * @param sagaId the saga id + * @return the optional + */ + public Optional findSagaById(final UUID sagaId) { + return this.getSagaRepository().findById(sagaId); + } + + /** + * Find all saga states list. + * + * @param saga the saga + * @return the list + */ + public List findAllSagaStates(final EasSagaEntity saga) { + return this.getSagaEventRepository().findBySaga(saga); + } + + + /** + * Update saga record. + * + * @param saga the saga + */ + @Transactional(propagation = Propagation.MANDATORY) + public void updateSagaRecord(final EasSagaEntity saga) { // saga here MUST be an attached entity + this.getSagaRepository().save(saga); + } + + + /** + * Create saga record in db saga. + * + * @param sagaName the saga name + * @param userName the username + * @param payload the payload + * @return the saga + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public EasSagaEntity createSagaRecordInDB(final String sagaName, final String userName, final String payload) { + final var saga = EasSagaEntity + .builder() + .payload(payload) + .sagaName(sagaName) + .status(STARTED.toString()) + .sagaState(INITIATED.toString()) + .createDate(LocalDateTime.now()) + .createUser(userName) + .updateUser(userName) + .updateDate(LocalDateTime.now()) + .build(); + return this.createSagaRecord(saga); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public List createSagaRecordsInDB(final List easSagaEntities) { + return this.createSagaRecords(easSagaEntities); + } + + /** + * Find all completable future. + * + * @param specs the saga specs + * @param pageNumber the page number + * @param pageSize the page size + * @param sorts the sorts + * @return the completable future + */ + @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) + public CompletableFuture> findAll(final Specification specs, final Integer pageNumber, final Integer pageSize, final List sorts) { + return CompletableFuture.supplyAsync(() -> { + final Pageable paging = PageRequest.of(pageNumber, pageSize, Sort.by(sorts)); + try { + return this.sagaRepository.findAll(specs, paging); + } catch (final Exception ex) { + throw new CompletionException(ex); + } + }); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/events/EventPublisherService.java b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/events/EventPublisherService.java new file mode 100644 index 0000000..acedb4f --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/events/EventPublisherService.java @@ -0,0 +1,50 @@ +package ca.bc.gov.educ.eas.api.service.v1.events; + +import ca.bc.gov.educ.eas.api.messaging.MessagePublisher; +import ca.bc.gov.educ.eas.api.struct.Event; +import ca.bc.gov.educ.eas.api.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import static lombok.AccessLevel.PRIVATE; + +@Service +@Slf4j +public class EventPublisherService { + + /** + * The constant RESPONDING_BACK_TO_NATS_ON_CHANNEL. + */ + public static final String RESPONDING_BACK_TO_NATS_ON_CHANNEL = "responding back to NATS on {} channel "; + + @Getter(PRIVATE) + private final MessagePublisher messagePublisher; + + @Autowired + public EventPublisherService(final MessagePublisher messagePublisher) { + this.messagePublisher = messagePublisher; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void send(final Event event) throws JsonProcessingException { + if (event.getReplyTo() != null) { + log.debug(RESPONDING_BACK_TO_NATS_ON_CHANNEL, event.getReplyTo()); + this.getMessagePublisher().dispatchMessage(event.getReplyTo(), this.easEventProcessed(event)); + } + } + + private byte[] easEventProcessed(final Event easEvent) throws JsonProcessingException { + final Event event = Event.builder() + .sagaId(easEvent.getSagaId()) + .eventType(easEvent.getEventType()) + .eventOutcome(easEvent.getEventOutcome()) + .eventPayload(easEvent.getEventPayload()).build(); + return JsonUtil.getJsonStringFromObject(event).getBytes(); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/Event.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/Event.java new file mode 100644 index 0000000..80c56b8 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/Event.java @@ -0,0 +1,42 @@ +package ca.bc.gov.educ.eas.api.struct; + +import ca.bc.gov.educ.eas.api.constants.EventOutcome; +import ca.bc.gov.educ.eas.api.constants.EventType; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +/** + * The type Event. + */ +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Event { + /** + * The Event type. + */ + private EventType eventType; + /** + * The Event outcome. + */ + private EventOutcome eventOutcome; + /** + * The Saga id. + */ + private UUID sagaId; + /** + * The Reply to. + */ + private String replyTo; + /** + * The Event payload. + */ + private String eventPayload; // json string +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/NotificationEvent.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/NotificationEvent.java new file mode 100644 index 0000000..32dafec --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/NotificationEvent.java @@ -0,0 +1,13 @@ +package ca.bc.gov.educ.eas.api.struct; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NotificationEvent extends Event{ + private String sagaStatus; + private String sagaName; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/District.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/District.java new file mode 100644 index 0000000..c028d5b --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/District.java @@ -0,0 +1,65 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * The type Student. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@SuperBuilder +@JsonIgnoreProperties(ignoreUnknown = true) +public class District extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String districtId; + + @Size(max = 3) + @NotNull(message = "districtNumber can not be null.") + @Getter + private String districtNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String faxNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String phoneNumber; + + @Size(max = 255) + @Email(message = "Email address should be a valid email address") + private String email; + + @Size(max = 255) + private String website; + + @Size(max = 255) + @NotNull(message = "displayName cannot be null") + private String displayName; + + @Size(max = 10) + @NotNull(message = "districtRegionCode cannot be null") + private String districtRegionCode; + + @Size(max = 10) + @NotNull(message = "districtStatusCode cannot be null") + private String districtStatusCode; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/FacilityTypeCode.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/FacilityTypeCode.java new file mode 100644 index 0000000..a0ddc03 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/FacilityTypeCode.java @@ -0,0 +1,32 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@SuppressWarnings("squid:S1700") +public class FacilityTypeCode implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String facilityTypeCode; + + private String label; + + private String description; + + private String legacyCode; + + private Integer displayOrder; + + private String effectiveDate; + + private String expiryDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentAuthority.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentAuthority.java new file mode 100644 index 0000000..839a570 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentAuthority.java @@ -0,0 +1,59 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * The type Student. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@SuperBuilder +@JsonIgnoreProperties(ignoreUnknown = true) +public class IndependentAuthority extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String independentAuthorityId; + + @Size(max = 4) + private String authorityNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String faxNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String phoneNumber; + + @Size(max = 255) + @Email(message = "Email address should be a valid email address") + private String email; + + @Size(max = 255) + @NotNull(message = "displayName cannot be null") + private String displayName; + + @Size(max = 10) + @NotNull(message = "authorityTypeCode cannot be null") + private String authorityTypeCode; + + @NotNull(message = "openedDate cannot be null") + private String openedDate; + + private String closedDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentSchoolFundingGroup.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentSchoolFundingGroup.java new file mode 100644 index 0000000..c785edc --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/IndependentSchoolFundingGroup.java @@ -0,0 +1,33 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@SuperBuilder +@SuppressWarnings("squid:S1700") +public class IndependentSchoolFundingGroup extends BaseRequest implements Serializable { + private static final long serialVersionUID = 1L; + + private String schoolFundingGroupID; + + private String schoolID; + + @NotNull(message = "schoolGradeCode cannot be null") + private String schoolGradeCode; + + @NotNull(message = "schoolFundingGroupCode cannot be null") + private String schoolFundingGroupCode; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/NeighborhoodLearning.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/NeighborhoodLearning.java new file mode 100644 index 0000000..0dc82ab --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/NeighborhoodLearning.java @@ -0,0 +1,26 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NeighborhoodLearning extends BaseRequest implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String neighborhoodLearningId; + + private String schoolId; + + @Size(max = 10) + @NotNull(message = "neighborhoodLearningTypeCode cannot be null") + private String neighborhoodLearningTypeCode; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/Note.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/Note.java new file mode 100644 index 0000000..b6c8f70 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/Note.java @@ -0,0 +1,36 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * The type Student. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Note extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String noteId; + + private String schoolId; + + private String districtId; + + private String independentAuthorityId; + + @Size(max = 4000) + @NotNull(message = "content cannot be null") + private String content; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/School.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/School.java new file mode 100644 index 0000000..f538e9c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/School.java @@ -0,0 +1,101 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@SuperBuilder +@JsonIgnoreProperties(ignoreUnknown = true) +public class School extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String schoolId; + @NotNull(message = "districtId can not be null.") + private String districtId; + + private String mincode; + + private String independentAuthorityId; + + @Size(max = 5) + private String schoolNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String faxNumber; + + @Size(max = 10) + @Pattern(regexp = "^$|\\d{10}", message = "Invalid phone number format") + private String phoneNumber; + + @Size(max = 255) + @Email(message = "Email address should be a valid email address") + private String email; + + @Size(max = 255) + private String website; + + @Size(max = 10) + @NotNull(message = "schoolReportingRequirementCode cannot be null") + private String schoolReportingRequirementCode; + + @Size(max = 255) + @NotNull(message = "displayName cannot be null") + private String displayName; + + @Size(max = 255) + private String displayNameNoSpecialChars; + + @Size(max = 10) + @NotNull(message = "schoolOrganizationCode cannot be null") + private String schoolOrganizationCode; + + @Size(max = 10) + @NotNull(message = "schoolCategoryCode cannot be null") + private String schoolCategoryCode; + + @Size(max = 10) + @NotNull(message = "facilityTypeCode cannot be null") + private String facilityTypeCode; + + private String openedDate; + + private String closedDate; + + private Boolean canIssueTranscripts; + + private Boolean canIssueCertificates; + + @Valid + private List contacts; + + @Valid + private List addresses; + + @Valid + private List grades; + + @Valid + private List schoolFundingGroups; + + @Valid + private List neighborhoodLearning; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolAddress.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolAddress.java new file mode 100644 index 0000000..5a1df11 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolAddress.java @@ -0,0 +1,24 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseAddress; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * The type Student. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchoolAddress extends BaseAddress implements Serializable { + + private static final long serialVersionUID = 1L; + + private String schoolAddressId; + + private String schoolId; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolCategoryCode.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolCategoryCode.java new file mode 100644 index 0000000..8021e25 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolCategoryCode.java @@ -0,0 +1,32 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@SuppressWarnings("squid:S1700") +public class SchoolCategoryCode implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String schoolCategoryCode; + + private String label; + + private String description; + + private String legacyCode; + + private Integer displayOrder; + + private String effectiveDate; + + private String expiryDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolContact.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolContact.java new file mode 100644 index 0000000..065811d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolContact.java @@ -0,0 +1,60 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * The type Student. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchoolContact extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String schoolContactId; + + private String schoolId; + + @Size(max = 10) + @NotNull(message = "schoolContactTypeCode cannot be null") + private String schoolContactTypeCode; + + @Size(max = 10) + private String phoneNumber; + + @Size(max = 10) + private String phoneExtension; + + @Size(max = 10) + private String alternatePhoneNumber; + + @Size(max = 10) + private String alternatePhoneExtension; + + @Size(max = 255) + @Email(message = "Email address should be a valid email address") + private String email; + + @Size(max = 255) + private String firstName; + + @Size(max = 255) + @NotNull(message = "lastName cannot be null") + private String lastName; + + private String effectiveDate; + + private String expiryDate; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGrade.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGrade.java new file mode 100644 index 0000000..cd7ed68 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGrade.java @@ -0,0 +1,26 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchoolGrade extends BaseRequest implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String schoolGradeId; + + private String schoolId; + + @Size(max = 10) + @NotNull(message = "schoolGradeCode cannot be null") + private String schoolGradeCode; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGradeCode.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGradeCode.java new file mode 100644 index 0000000..61f942c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolGradeCode.java @@ -0,0 +1,30 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@SuppressWarnings("squid:S1700") +public class SchoolGradeCode implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String schoolGradeCode; + + private String label; + + private String description; + + private Integer displayOrder; + + private String effectiveDate; + + private String expiryDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolTombstone.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolTombstone.java new file mode 100644 index 0000000..88e56ba --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/external/institute/v1/SchoolTombstone.java @@ -0,0 +1,75 @@ +package ca.bc.gov.educ.eas.api.struct.external.institute.v1; + +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@SuperBuilder +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchoolTombstone extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + private String schoolId; + + private String districtId; + + private String mincode; + + private String independentAuthorityId; + + @Size(max = 5) + @NotNull(message = "schoolNumber can not be null.") + private String schoolNumber; + + @Size(max = 10) + private String faxNumber; + + @Size(max = 10) + private String phoneNumber; + + @Size(max = 255) + @Email(message = "Email address should be a valid email address") + private String email; + + @Size(max = 255) + private String website; + + @Size(max = 255) + @NotNull(message = "displayName cannot be null") + private String displayName; + + @Size(max = 10) + @NotNull(message = "schoolOrganizationCode cannot be null") + private String schoolOrganizationCode; + + @Size(max = 10) + @NotNull(message = "schoolCategoryCode cannot be null") + private String schoolCategoryCode; + + @Size(max = 10) + @NotNull(message = "facilityTypeCode cannot be null") + private String facilityTypeCode; + + @Size(max = 10) + @NotNull(message = "schoolReportingRequirementCode cannot be null") + private String schoolReportingRequirementCode; + + private String openedDate; + + private String closedDate; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseAddress.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseAddress.java new file mode 100644 index 0000000..e00eb8f --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseAddress.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * The type Base address. + */ +@Data +public abstract class BaseAddress extends BaseRequest { + + @Size(max = 255) + @NotNull(message = "addressLine1 cannot be null") + private String addressLine1; + + @Size(max = 255) + private String addressLine2; + + @Size(max = 255) + @NotNull(message = "city cannot be null") + private String city; + + @Size(max = 255) + @NotNull(message = "postal cannot be null") + private String postal; + + @Size(max = 10) + @NotNull(message = "addressTypeCode cannot be null") + private String addressTypeCode; + + @Size(max = 10) + @NotNull(message = "provinceCode cannot be null") + private String provinceCode; + + @Size(max = 10) + @NotNull(message = "countryCode cannot be null") + private String countryCode; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseRequest.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseRequest.java new file mode 100644 index 0000000..67c7dca --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/BaseRequest.java @@ -0,0 +1,23 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public abstract class BaseRequest { + @Size(max = 100) + protected String createUser; + @Size(max = 100) + protected String updateUser; + @Null(message = "createDate should be null.") + protected String createDate; + @Null(message = "updateDate should be null.") + protected String updateDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ChoreographedEvent.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ChoreographedEvent.java new file mode 100644 index 0000000..ab4e194 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ChoreographedEvent.java @@ -0,0 +1,33 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import ca.bc.gov.educ.eas.api.constants.EventOutcome; +import ca.bc.gov.educ.eas.api.constants.EventType; +import lombok.Data; + +@Data +public class ChoreographedEvent { + /** + * The Event id. + */ + String eventID; // the primary key of student event table. + /** + * The Event type. + */ + EventType eventType; + /** + * The Event outcome. + */ + EventOutcome eventOutcome; + /** + * The Event payload. + */ + String eventPayload; + /** + * The Create user. + */ + String createUser; + /** + * The Update user. + */ + String updateUser; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Condition.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Condition.java new file mode 100644 index 0000000..d7a1f32 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Condition.java @@ -0,0 +1,15 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +/** + * The enum Condition. + */ +public enum Condition { + /** + * And condition. + */ + AND, + /** + * Or condition. + */ + OR +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingCode.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingCode.java new file mode 100644 index 0000000..05d6207 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingCode.java @@ -0,0 +1,30 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@SuppressWarnings("squid:S1700") +public class SchoolFundingCode implements Serializable { + private static final long serialVersionUID = 6118916290604876032L; + + private String schoolFundingCode; + + private String label; + + private String description; + + private Integer displayOrder; + + private String effectiveDate; + + private String expiryDate; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingGroupCode.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingGroupCode.java new file mode 100644 index 0000000..53efef2 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SchoolFundingGroupCode.java @@ -0,0 +1,30 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@SuppressWarnings("squid:S1700") +public class SchoolFundingGroupCode implements Serializable { + + private static final long serialVersionUID = 6118916290604876032L; + + private String schoolFundingGroupCode; + + private String label; + + private String description; + + private Integer displayOrder; + + private String effectiveDate; + + private String expiryDate; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Search.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Search.java new file mode 100644 index 0000000..2c9377c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/Search.java @@ -0,0 +1,28 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The type Search. + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class Search { + + /** + * The Condition. ENUM to hold and AND OR + */ + Condition condition; + + /** + * The Search criteria list. + */ + List searchCriteriaList; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SearchCriteria.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SearchCriteria.java new file mode 100644 index 0000000..065e573 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/SearchCriteria.java @@ -0,0 +1,43 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import ca.bc.gov.educ.eas.api.filter.FilterOperation; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The type Search criteria. + * + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class SearchCriteria { + /** + * The Key. + */ + @NotNull + String key; + /** + * The Operation. + */ + @NotNull + FilterOperation operation; + /** + * The Value. + */ + String value; + /** + * The Value type. + */ + @NotNull + ValueType valueType; + + /** + * The Condition. ENUM to hold and AND OR + */ + Condition condition; +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ValueType.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ValueType.java new file mode 100644 index 0000000..a8f8ced --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/ValueType.java @@ -0,0 +1,32 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +public enum ValueType { + /** + * String value type. + */ + STRING, + /** + * Integer value type. + */ + INTEGER, + /** + * Long value type. + */ + LONG, + /** + * Date value type. + */ + DATE, + /** + * Date time value type. + */ + DATE_TIME, + /** + * Uuid value type. + */ + UUID, + /** + * Boolean value type. + */ + BOOLEAN +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/util/JsonUtil.java b/api/src/main/java/ca/bc/gov/educ/eas/api/util/JsonUtil.java new file mode 100644 index 0000000..3660d3d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/util/JsonUtil.java @@ -0,0 +1,86 @@ +package ca.bc.gov.educ.eas.api.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Optional; + +/** + * The type Json util. + * + * @author OM + */ +@Slf4j +public class JsonUtil { + public static final ObjectMapper mapper = new ObjectMapper(); + /** + * Instantiates a new Json util. + */ + private JsonUtil(){ + } + + /** + * Gets json string from object. + * + * @param payload the payload + * @return the json string from object + * @throws JsonProcessingException the json processing exception + */ + public static String getJsonStringFromObject(Object payload) throws JsonProcessingException { + return mapper.writeValueAsString(payload); + } + + /** + * Gets json object from string. + * + * @param the type parameter + * @param clazz the clazz + * @param payload the payload + * @return the json object from string + * @throws JsonProcessingException the json processing exception + */ + public static T getJsonObjectFromString(Class clazz, String payload) throws JsonProcessingException { + return mapper.readValue(payload, clazz); + } + + /** + * Gets json object from string. + * + * @param the type parameter + * @param clazz the clazz + * @param payload the payload + * @return the json object from string + * @throws IOException the io exception + */ + public static T getJsonObjectFromByteArray(Class clazz, byte[] payload) throws IOException { + return mapper.readValue(payload, clazz); + } + + /** + * Get json bytes from object byte [ ]. + * + * @param payload the payload + * @return the byte [ ] + * @throws JsonProcessingException the json processing exception + */ + public static byte[] getJsonBytesFromObject(final Object payload) throws JsonProcessingException { + return new ObjectMapper().writeValueAsBytes(payload); + } + + /** + * Get json string optional. + * + * @param payload the payload + * @return the optional + */ + public static Optional getJsonString(Object payload){ + try{ + return Optional.ofNullable(mapper.writeValueAsString(payload)); + }catch (final Exception ex){ + log.error("Exception while converting object to JSON String :: {}", payload); + } + return Optional.empty(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/util/RequestUtil.java b/api/src/main/java/ca/bc/gov/educ/eas/api/util/RequestUtil.java new file mode 100644 index 0000000..0869a66 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/util/RequestUtil.java @@ -0,0 +1,84 @@ +package ca.bc.gov.educ.eas.api.util; + +import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; +import ca.bc.gov.educ.eas.api.struct.v1.BaseRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Sort; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * The type Request util. + */ +public class RequestUtil { + private RequestUtil() { + } + + /** + * set audit data to the object. + * + * @param baseRequest The object which will be persisted. + */ + public static void setAuditColumnsForCreate(@NotNull BaseRequest baseRequest) { + if (StringUtils.isBlank(baseRequest.getCreateUser())) { + baseRequest.setCreateUser(ApplicationProperties.EAS_API); + } + baseRequest.setCreateDate(LocalDateTime.now().toString()); + setAuditColumnsForUpdate(baseRequest); + } + + /** + * set audit data to the object if audit (createUser/createDate) is blank + * + * @param baseRequest The object which will be persisted. + */ + public static void setAuditColumnsForCreateIfBlank(@NotNull BaseRequest baseRequest) { + if (StringUtils.isBlank(baseRequest.getCreateUser())) { + baseRequest.setCreateUser(ApplicationProperties.EAS_API); + } + if (StringUtils.isBlank(baseRequest.getCreateDate())) { + baseRequest.setCreateDate(LocalDateTime.now().toString()); + } + setAuditColumnsForUpdate(baseRequest); + } + + /** + * set audit data to the object. + * + * @param baseRequest The object which will be persisted. + */ + public static void setAuditColumnsForUpdate(@NotNull BaseRequest baseRequest) { + if (StringUtils.isBlank(baseRequest.getUpdateUser())) { + baseRequest.setUpdateUser(ApplicationProperties.EAS_API); + } + baseRequest.setUpdateDate(LocalDateTime.now().toString()); + } + + /** + * Get the Sort.Order list from JSON string + * + * @param sortCriteriaJson The sort criterio JSON + * @param objectMapper The object mapper + * @param sorts The Sort.Order list + * @throws JsonProcessingException the json processing exception + */ + public static void getSortCriteria(String sortCriteriaJson, ObjectMapper objectMapper, List sorts) throws JsonProcessingException { + if (StringUtils.isNotBlank(sortCriteriaJson)) { + Map sortMap = objectMapper.readValue(sortCriteriaJson, new TypeReference<>() { + }); + sortMap.forEach((k, v) -> { + if ("ASC".equalsIgnoreCase(v)) { + sorts.add(new Sort.Order(Sort.Direction.ASC, k)); + } else { + sorts.add(new Sort.Order(Sort.Direction.DESC, k)); + } + }); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/util/ThreadFactoryBuilder.java b/api/src/main/java/ca/bc/gov/educ/eas/api/util/ThreadFactoryBuilder.java new file mode 100644 index 0000000..8f8d73b --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/util/ThreadFactoryBuilder.java @@ -0,0 +1,109 @@ +package ca.bc.gov.educ.eas.api.util; + +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +public class ThreadFactoryBuilder { + + private String nameFormat; + private Boolean daemonThread; + private Integer priority; + private Thread.UncaughtExceptionHandler uncaughtExceptionHandler = null; + private ThreadFactory backingThreadFactory = null; + + /** + * Returns new {@code ThreadFactory} builder. + */ + public static ThreadFactoryBuilder create() { + return new ThreadFactoryBuilder(); + } + + /** + * Sets the printf-compatible naming format for threads. + * Use {@code %d} to replace it with the thread number. + */ + public ThreadFactoryBuilder withNameFormat(final String nameFormat) { + this.nameFormat = nameFormat; + return this; + } + + /** + * Sets if new threads will be daemon. + */ + public ThreadFactoryBuilder withDaemon(final boolean daemon) { + this.daemonThread = daemon; + return this; + } + + /** + * Sets the threads priority. + */ + public ThreadFactoryBuilder withPriority(final int priority) { + this.priority = priority; + return this; + } + + /** + * Sets the {@code UncaughtExceptionHandler} for new threads created. + */ + public ThreadFactoryBuilder withUncaughtExceptionHandler( + final Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + + this.uncaughtExceptionHandler = Objects.requireNonNull(uncaughtExceptionHandler); + return this; + } + + /** + * Sets the backing {@code ThreadFactory} for new threads. Threads + * will be created by invoking {@code newThread(Runnable} on this backing factory. + */ + public ThreadFactoryBuilder withBackingThreadFactory(final ThreadFactory backingThreadFactory) { + this.backingThreadFactory = Objects.requireNonNull(backingThreadFactory); + return this; + } + + /** + * Returns a new thread factory using the options supplied during the building process. After + * building, it is still possible to change the options used to build the ThreadFactory and/or + * build again. + */ + public ThreadFactory get() { + return get(this); + } + + private static ThreadFactory get(final ThreadFactoryBuilder builder) { + final String nameFormat = builder.nameFormat; + final Boolean daemon = builder.daemonThread; + final Integer priority = builder.priority; + final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = builder.uncaughtExceptionHandler; + + final ThreadFactory backingThreadFactory = + (builder.backingThreadFactory != null) + ? builder.backingThreadFactory + : Executors.defaultThreadFactory(); + + final AtomicLong count = (nameFormat != null) ? new AtomicLong(0) : null; + + return runnable -> { + final Thread thread = backingThreadFactory.newThread(runnable); + if (nameFormat != null) { + final String name = String.format(nameFormat, count.getAndIncrement()); + + thread.setName(name); + } + if (daemon != null) { + thread.setDaemon(daemon); + } + if (priority != null) { + thread.setPriority(priority); + } + if (uncaughtExceptionHandler != null) { + thread.setUncaughtExceptionHandler(uncaughtExceptionHandler); + } + return thread; + }; + } + +} diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties new file mode 100644 index 0000000..33aa25c --- /dev/null +++ b/api/src/main/resources/application.properties @@ -0,0 +1,74 @@ +#logging Properties +logging.level.org.springframework.security=${SPRING_SECURITY_LOG_LEVEL} +logging.level.org.springframework.web=${SPRING_WEB_LOG_LEVEL} +logging.level.ca.bc.gov.educ.studentdatacollection=${APP_LOG_LEVEL} +logging.level.org.springframework.boot.autoconfigure.logging=${SPRING_BOOT_AUTOCONFIG_LOG_LEVEL} +spring.mvc.log-request-details=${SPRING_SHOW_REQUEST_DETAILS} + +#DB Properties +spring.datasource.url=${JDBC_URL} +spring.datasource.username=${ORACLE_USERNAME} +spring.datasource.password=${ORACLE_PASSWORD} +spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect +spring.jpa.hibernate.ddl-auto=none + +spring.jackson.deserialization.fail-on-unknown-properties=true +spring.security.oauth2.resourceserver.jwt.issuer-uri=${TOKEN_ISSUER_URL} +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${TOKEN_ISSUER_URL}/protocol/openid-connect/certs +management.endpoint.metrics.enabled=true +management.endpoints.web.exposure.include=* +management.endpoint.prometheus.enabled=true +management.prometheus.metrics.export.enabled=true +spring.jpa.properties.hibernate.generate_statistics=false +spring.jpa.properties.hibernate.jdbc.batch_size=999 +spring.jpa.properties.hibernate.order_inserts=true +spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true +spring.datasource.hikari.max-lifetime=120000 +spring.jmx.enabled=false +spring.flyway.baseline-on-migrate=true +spring.flyway.table=FLYWAY_SCHEMA_HISTORY +spring.flyway.baseline-version=0 +spring.flyway.enabled=true +logging.file.name=/logs/app.log +logging.logback.rollingpolicy.max-file-size=5MB +logging.logback.rollingpolicy.clean-history-on-start=true +logging.logback.rollingpolicy.max-history=1 +logging.pattern.file={"time_stamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%3p" ,"thread":"%t" ,"class":"%logger{36}","msg":"%replace(%msg){'[\n\r\"]',''}", "exception":"%replace(%rEx{10}){'[\n\r\"]',''}","http_event":%X{httpEvent:-""},"message_event":%X{messageEvent:-""}, "saga_retry":%X{sagaRetry:-""}}%nopex%n +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} | [%5p] | [%t] | [%logger{36}] | [%replace(%msg){'[\n\r\"]',''} %X{httpEvent} %X{messageEvent}] | %replace(%rEx{10}){'[\n\r\"]',''}%nopex%n + +#This is required to map long raw, please see below links, even if hibernate documentation mentions {hibernate.dialect.oracle.prefer_longvarbinary} +# this as the property name, it is not correct. +#https://hibernate.atlassian.net/browse/HHH-10345 +#https://in.relation.to/2016/02/17/hibernate-orm-508-final-release/ +#spring.jpa.properties.hibernate.dialect.oracle.prefer_long_raw=true +#Print the queries +spring.jpa.show-sql=${SPRING_JPA_SHOW_SQL} + +spring.jpa.open-in-view=false +#Client details to get token to make api calls. +client.id=${CLIENT_ID} +client.secret=${CLIENT_SECRET} +url.token=${TOKEN_URL} + +nats.server=${NATS_URL} +nats.maxReconnect=${NATS_MAX_RECONNECT} +nats.connectionName=EAS-API +initialization.background.enabled=true + +threads.min.subscriber=${THREADS_MIN_SUBSCRIBER} +threads.max.subscriber=${THREADS_MAX_SUBSCRIBER} +sagas.max.pending=${SAGAS_MAX_PENDING} +sagas.max.parallel=${SAGAS_MAX_PARALLEL} + +url.api.institute=${INSTITUTE_API_URL} +url.api.edx=${EDX_API_URL} +schedule.jobs.load.school.cron=0 0 0/12 * * * + +spring.datasource.hikari.maximum-pool-size=${MAXIMUM_DB_POOL_SIZE} +spring.datasource.hikari.minimum-idle=${MINIMUM_IDLE_DB_POOL_SIZE} + +purge.records.saga.after.days=${PURGE_RECORDS_SAGA_AFTER_DAYS} +scheduled.jobs.purge.old.saga.records.cron=${SCHEDULED_JOBS_PURGE_OLD_SAGA_RECORDS_CRON} + +server.max-http-request-header-size=2MB +url.api.student=${STUDENT_API_URL} diff --git a/api/src/test/resources/application.properties b/api/src/test/resources/application.properties new file mode 100644 index 0000000..bedf7a4 --- /dev/null +++ b/api/src/test/resources/application.properties @@ -0,0 +1,49 @@ +spring.jpa.generate-ddl=false +spring.jpa.hibernate.ddl-auto=create-drop +logging.level.root=ERROR +logging.level.org.hibernate=ERROR +logging.level.ca.bc.gov.educ.studentdatacollection=INFO +#spring.jpa.properties.hibernate.generate_statistics=false +spring.jpa.show-sql=false +# SQL statements and parameters +#logging.level.org.hibernate.type.descriptor.sql=trace +spring.main.allow-bean-definition-overriding=true +spring.flyway.enabled=false +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://test +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://test + +#Client details to get token to make api calls. +client.id=123 +client.secret=123 +url.token=http://abcxyz.com +url.api.institute=http://abcxyz.com +url.api.edx=http://abcxyz.com +schedule.jobs.load.school.cron=- + +spring.datasource.hikari.maximum-pool-size=20 +spring.datasource.hikari.minimum-idle=20 +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR +nats.server=nats://localhost:4220 +nats.maxReconnect=60 +nats.connectionName=eas-api + +initialization.background.enabled=false + +spring.jackson.deserialization.fail-on-unknown-properties=false + +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} | [%5p] | [%t] | [%logger{36}] | [%replace(%msg){'[\n\r\"]',''} %X{httpEvent} %X{messageEvent}] | %replace(%rEx{10}){'[\n\r\"]',''}%nopex%n + +threads.min.subscriber=2 +threads.max.subscriber=2 +sagas.max.pending=100 +sagas.max.parallel=100 + + + + +purge.records.saga.after.days=365 +scheduled.jobs.purge.old.saga.records.cron=- + +server.undertow.max-http-post-size=2MB +url.api.student=http://abcxyz.com +