diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/JobSuiteInitializerStep.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/JobSuiteInitializerStep.java index 7ffaea59239..15d382df660 100644 --- a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/JobSuiteInitializerStep.java +++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/JobSuiteInitializerStep.java @@ -26,7 +26,7 @@ import org.apache.fineract.client.feign.FineractFeignClient; import org.apache.fineract.client.models.ExecuteJobRequest; import org.apache.fineract.client.models.GetJobsResponse; -import org.apache.fineract.client.models.PutJobsJobIDRequest; +import org.apache.fineract.client.models.JobUpdateRequest; import org.springframework.stereotype.Component; @Slf4j @@ -126,7 +126,7 @@ private Long updateExternalEventJobFrequency(String cronExpression) { .filter(r -> r.getDisplayName().equals(SEND_ASYNCHRONOUS_EVENTS_JOB_NAME)).findAny() .orElseThrow(() -> new IllegalStateException(SEND_ASYNCHRONOUS_EVENTS_JOB_NAME + " is not found")); Long jobId = externalEventJobResponse.getJobId(); - executeVoid(() -> fineractClient.schedulerJob().updateJobDetail(jobId, new PutJobsJobIDRequest().cronExpression(cronExpression))); + executeVoid(() -> fineractClient.schedulerJob().updateJobDetail(jobId, new JobUpdateRequest().cronExpression(cronExpression))); return jobId; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java index 79df8a3d421..5b32e39fed6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java @@ -49,6 +49,7 @@ import java.util.Objects; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.command.core.CommandDispatcher; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; @@ -62,8 +63,11 @@ import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.jobs.command.JobUpdateCommand; import org.apache.fineract.infrastructure.jobs.data.JobDetailData; import org.apache.fineract.infrastructure.jobs.data.JobDetailHistoryData; +import org.apache.fineract.infrastructure.jobs.data.JobUpdateRequest; +import org.apache.fineract.infrastructure.jobs.data.JobUpdateResponse; import org.apache.fineract.infrastructure.jobs.service.JobRegisterService; import org.apache.fineract.infrastructure.jobs.service.SchedulerJobRunnerReadService; import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; @@ -87,6 +91,7 @@ public class SchedulerJobApiResource { private final PlatformSecurityContext context; private final FineractProperties fineractProperties; private final SqlValidator sqlValidator; + private final CommandDispatcher dispatcher; @GET @Operation(summary = "Retrieve Scheduler Jobs", operationId = "retrieveAllSchedulerJobs", description = "Returns the list of jobs.\n" @@ -173,14 +178,14 @@ public Response executeJobByShortName( } @PUT - @Path("{" + SchedulerJobApiConstants.JOB_ID + "}") - @Consumes({ MediaType.APPLICATION_JSON }) + @Path("{jobId}") + @Consumes(MediaType.APPLICATION_JSON) @Operation(summary = "Update a Job", description = "Updates the details of a job.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class))) - @ApiResponse(responseCode = "200", description = "OK") - public String updateJobDetail(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId, - @Parameter(hidden = true) final String jsonRequestBody) { - return updateJobDetail(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), jsonRequestBody); + public JobUpdateResponse updateJobDetail(@PathParam("jobId") Long jobId, JobUpdateRequest request) { + request.setJobId(jobId); + var command = new JobUpdateCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); } @PUT diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/command/JobUpdateCommand.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/command/JobUpdateCommand.java new file mode 100644 index 00000000000..ded7b475ceb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/command/JobUpdateCommand.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.infrastructure.jobs.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.infrastructure.jobs.data.JobUpdateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class JobUpdateCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailDataValidator.java deleted file mode 100644 index 90682c06973..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailDataValidator.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.jobs.data; - -import com.google.gson.JsonElement; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class JobDetailDataValidator { - - private final FromJsonHelper fromApiJsonHelper; - private static final Set JOB_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(SchedulerJobApiConstants.displayNameParamName, SchedulerJobApiConstants.jobActiveStatusParamName, - SchedulerJobApiConstants.cronExpressionParamName)); - - @Autowired - public JobDetailDataValidator(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } - - public void validateForUpdate(final String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - boolean atLeastOneParameterPassedForUpdate = false; - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, JOB_UPDATE_REQUEST_DATA_PARAMETERS); - final JsonElement element = this.fromApiJsonHelper.parse(json); - - final List dataValidationErrors = new ArrayList<>(); - - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SchedulerJobApiConstants.JOB_RESOURCE_NAME); - if (this.fromApiJsonHelper.parameterExists(SchedulerJobApiConstants.displayNameParamName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String displayName = this.fromApiJsonHelper.extractStringNamed(SchedulerJobApiConstants.displayNameParamName, element); - baseDataValidator.reset().parameter(SchedulerJobApiConstants.displayNameParamName).value(displayName).notBlank(); - } - - if (this.fromApiJsonHelper.parameterExists(SchedulerJobApiConstants.cronExpressionParamName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String cronExpression = this.fromApiJsonHelper.extractStringNamed(SchedulerJobApiConstants.cronExpressionParamName, - element); - baseDataValidator.reset().parameter(SchedulerJobApiConstants.cronExpressionParamName).value(cronExpression).notBlank() - .validateCronExpression(); - } - if (this.fromApiJsonHelper.parameterExists(SchedulerJobApiConstants.jobActiveStatusParamName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String status = this.fromApiJsonHelper.extractStringNamed(SchedulerJobApiConstants.jobActiveStatusParamName, element); - baseDataValidator.reset().parameter(SchedulerJobApiConstants.jobActiveStatusParamName).value(status).notBlank() - .validateForBooleanValue(); - } - - if (!atLeastOneParameterPassedForUpdate) { - final Object forceError = null; - baseDataValidator.reset().anyOfNotNull(forceError); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateRequest.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateRequest.java new file mode 100644 index 00000000000..eee6b3d6782 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateRequest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.infrastructure.jobs.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import java.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; +import org.quartz.CronExpression; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class JobUpdateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long jobId; + + private String displayName; + private String cronExpression; + private Boolean active; + + @JsonIgnore + @AssertTrue(message = "{org.apache.fineract.infrastructure.jobs.update.at-least-one-field}") + public boolean isAtLeastOneFieldPresent() { + return displayName != null || cronExpression != null || active != null; + } + + @JsonIgnore + @AssertTrue(message = "{org.apache.fineract.infrastructure.jobs.cron-expression.invalid}") + public boolean isCronExpressionValid() { + return cronExpression == null || CronExpression.isValidExpression(cronExpression.trim()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateResponse.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateResponse.java new file mode 100644 index 00000000000..bc1816c2135 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobUpdateResponse.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.infrastructure.jobs.data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class JobUpdateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long resourceId; // the jobId + private Map changes; // what actually changed — kept for backward compatibility +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/JobUpdateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/JobUpdateCommandHandler.java new file mode 100644 index 00000000000..3c41a65f695 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/JobUpdateCommandHandler.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.infrastructure.jobs.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.infrastructure.jobs.data.JobUpdateRequest; +import org.apache.fineract.infrastructure.jobs.data.JobUpdateResponse; +import org.apache.fineract.infrastructure.jobs.service.JobRegisterService; +import org.apache.fineract.infrastructure.jobs.service.SchedularWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JobUpdateCommandHandler implements CommandHandler { + + private final SchedularWritePlatformService schedularWritePlatformService; + private final JobRegisterService jobRegisterService; + + @Retry(name = "commandJobUpdate", fallbackMethod = "fallback") + @Override + @Transactional + public JobUpdateResponse handle(Command command) { + JobUpdateResponse response = schedularWritePlatformService.updateJobDetail(command.getPayload()); + + Map changes = response.getChanges(); + if (changes != null && (changes.containsKey("cronExpression") || changes.containsKey("active"))) { + jobRegisterService.rescheduleJob(command.getPayload().getJobId()); + } + return response; + } + + @Override + public JobUpdateResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/UpdateJobDetailCommandhandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/UpdateJobDetailCommandhandler.java deleted file mode 100755 index 9cc9af8c95f..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/UpdateJobDetailCommandhandler.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.jobs.handler; - -import org.apache.fineract.commands.annotation.CommandType; -import org.apache.fineract.commands.handler.NewCommandSourceHandler; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.jobs.service.SchedularWritePlatformService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -@CommandType(entity = "SCHEDULER", action = "UPDATE") -public class UpdateJobDetailCommandhandler implements NewCommandSourceHandler { - - private final SchedularWritePlatformService schedularWritePlatformService; - - @Autowired - public UpdateJobDetailCommandhandler(final SchedularWritePlatformService schedularWritePlatformService) { - this.schedularWritePlatformService = schedularWritePlatformService; - } - - @Override - public CommandProcessingResult processCommand(final JsonCommand command) { - return this.schedularWritePlatformService.updateJobDetail(command.entityId(), command); - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java index a832c7311be..eb5b42e3185 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java @@ -27,6 +27,7 @@ import java.util.Properties; import java.util.Set; import java.util.TimeZone; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; @@ -51,7 +52,6 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.configuration.JobLocator; import org.springframework.batch.core.launch.NoSuchJobException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; @@ -63,37 +63,24 @@ * Service class to create and load batch jobs to Scheduler using {@link SchedulerFactoryBean} * ,{@link MethodInvokingJobDetailFactoryBean} and {@link CronTriggerFactoryBean} */ +@RequiredArgsConstructor @Service @Slf4j public class JobRegisterServiceImpl implements JobRegisterService, ApplicationListener { private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: "; - @Autowired - private SchedularWritePlatformService schedularWritePlatformService; - - @Autowired - private SchedulerJobListener schedulerJobListener; - - @Autowired - private SchedulerTriggerListener globalSchedulerTriggerListener; - private static final HashMap SCHEDULERS = new HashMap<>(4); - @Autowired - private FineractProperties fineractProperties; - - @Autowired - private JobLocator jobLocator; - - @Autowired - private JobStarter jobStarter; - - @Autowired - private JobParameterDataParser dataParser; - - @Autowired - private JobNameService jobNameService; + private final SchedularWritePlatformService schedularWritePlatformService; + private final SchedulerJobListener schedulerJobListener; + private final SchedulerTriggerListener globalSchedulerTriggerListener; + private final FineractProperties fineractProperties; + private final JobLocator jobLocator; + private final JobStarter jobStarter; + private final JobParameterDataParser dataParser; + private final JobNameService jobNameService; + private final ScheduledJobReadService scheduledJobReadService; private static final String JOB_STARTER_METHOD_NAME = "run"; @@ -154,7 +141,7 @@ public void rescheduleJob(final ScheduledJobDetail scheduledJobDetail) { @Override public void pauseScheduler() { - final SchedulerDetail schedulerDetail = this.schedularWritePlatformService.retriveSchedulerDetail(); + final SchedulerDetail schedulerDetail = this.scheduledJobReadService.retrieveSchedulerDetail(); if (!schedulerDetail.isSuspended()) { schedulerDetail.setSuspended(true); this.schedularWritePlatformService.updateSchedulerDetail(schedulerDetail); @@ -163,7 +150,7 @@ public void pauseScheduler() { @Override public void startScheduler() { - final SchedulerDetail schedulerDetail = this.schedularWritePlatformService.retriveSchedulerDetail(); + final SchedulerDetail schedulerDetail = this.scheduledJobReadService.retrieveSchedulerDetail(); if (schedulerDetail.isSuspended()) { schedulerDetail.setSuspended(false); this.schedularWritePlatformService.updateSchedulerDetail(schedulerDetail); @@ -202,7 +189,7 @@ public void startScheduler() { @Override public void rescheduleJob(final Long jobId) { - final ScheduledJobDetail scheduledJobDetail = this.schedularWritePlatformService.findByJobId(jobId); + final ScheduledJobDetail scheduledJobDetail = this.scheduledJobReadService.findByJobId(jobId); final String nodeIdStored = scheduledJobDetail.getNodeId().toString(); if (nodeIdStored.equals(fineractProperties.getNodeId()) || nodeIdStored.equals("0")) { rescheduleJob(scheduledJobDetail); @@ -216,7 +203,7 @@ public void rescheduleJob(final Long jobId) { @Override public void executeJobWithParameters(final Long jobId, String jobParametersJson) { Set jobParameterDTOSet = dataParser.parseExecution(jobParametersJson); - final ScheduledJobDetail scheduledJobDetail = this.schedularWritePlatformService.findByJobId(jobId); + final ScheduledJobDetail scheduledJobDetail = this.scheduledJobReadService.findByJobId(jobId); if (scheduledJobDetail == null) { throw new JobNotFoundException(String.valueOf(jobId)); } @@ -233,7 +220,7 @@ public void executeJobWithParameters(final Long jobId, String jobParametersJson) @Override public boolean isSchedulerRunning() { - return !this.schedularWritePlatformService.retriveSchedulerDetail().isSuspended(); + return !this.scheduledJobReadService.retrieveSchedulerDetail().isSuspended(); } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java index adb90ec41dd..fb4a3cf332c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java @@ -48,6 +48,7 @@ public class JobSchedulerServiceImpl implements ApplicationListener retrieveAllJobs(final String nodeId) { return this.scheduledJobDetailsRepository.findAllJobs(Integer.parseInt(nodeId)); @@ -91,8 +80,7 @@ public Long fetchMaxVersionBy(final String jobKey) { return version; } - @Override - public ScheduledJobDetail findByJobId(final Long jobId) { + private ScheduledJobDetail findByJobId(final Long jobId) { return this.scheduledJobDetailsRepository.findByJobId(jobId); } @@ -102,7 +90,6 @@ public void updateSchedulerDetail(final SchedulerDetail schedulerDetail) { this.schedulerDetailRepository.save(schedulerDetail); } - @Override public SchedulerDetail retriveSchedulerDetail() { SchedulerDetail schedulerDetail = null; final List schedulerDetailList = this.schedulerDetailRepository.findAll(); @@ -114,22 +101,32 @@ public SchedulerDetail retriveSchedulerDetail() { @Transactional @Override - public CommandProcessingResult updateJobDetail(final Long jobId, final JsonCommand command) { - this.dataValidator.validateForUpdate(command.json()); - final ScheduledJobDetail scheduledJobDetail = findByJobId(jobId); - if (scheduledJobDetail == null) { - throw new JobNotFoundException(String.valueOf(jobId)); + public JobUpdateResponse updateJobDetail(JobUpdateRequest request) { + ScheduledJobDetail job = findByJobId(request.getJobId()); + if (job == null) { + throw new JobNotFoundException(String.valueOf(request.getJobId())); } - final Map changes = scheduledJobDetail.update(command); + + Map changes = new LinkedHashMap<>(); + + if (request.getDisplayName() != null && !request.getDisplayName().trim().equals(job.getJobDisplayName())) { + job.setJobDisplayName(StringUtils.defaultIfEmpty(request.getDisplayName().trim(), null)); + changes.put("displayName", request.getDisplayName().trim()); + } + if (request.getCronExpression() != null && !request.getCronExpression().trim().equals(job.getCronExpression())) { + job.setCronExpression(StringUtils.defaultIfEmpty(request.getCronExpression().trim(), null)); + changes.put("cronExpression", request.getCronExpression().trim()); + } + if (request.getActive() != null && request.getActive() != job.isActiveSchedular()) { + job.setActiveSchedular(request.getActive()); + changes.put("active", request.getActive()); + } + if (!changes.isEmpty()) { - this.scheduledJobDetailsRepository.saveAndFlush(scheduledJobDetail); + scheduledJobDetailsRepository.saveAndFlush(job); } - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(jobId) // - .with(changes) // - .build(); + return JobUpdateResponse.builder().resourceId(job.getId()).changes(changes).build(); } @Transactional diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/ScheduledJobReadService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/ScheduledJobReadService.java new file mode 100644 index 00000000000..e9ba9eb828c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/ScheduledJobReadService.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.jobs.service; + +import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail; +import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetail; + +public interface ScheduledJobReadService { + + ScheduledJobDetail findByJobId(Long jobId); + + SchedulerDetail retrieveSchedulerDetail(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java index 40a5c283957..efa133cdd12 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java @@ -22,6 +22,7 @@ import java.sql.SQLException; import java.util.Date; import java.util.List; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.api.IdTypeResolver; import org.apache.fineract.infrastructure.core.service.Page; @@ -30,11 +31,13 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.jobs.data.JobDetailData; import org.apache.fineract.infrastructure.jobs.data.JobDetailHistoryData; +import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail; import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository; +import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetail; +import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetailRepository; import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException; import org.apache.fineract.infrastructure.jobs.exception.OperationNotAllowedException; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.lang.NonNull; @@ -44,30 +47,33 @@ @Service @Slf4j @Transactional(readOnly = true) -public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerReadService { +@RequiredArgsConstructor +public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerReadService, ScheduledJobReadService { private final JdbcTemplate jdbcTemplate; private final ColumnValidator columnValidator; private final DatabaseSpecificSQLGenerator sqlGenerator; - private final ScheduledJobDetailRepository jobDetailRepository; - + private final ScheduledJobDetailRepository jobDetailRepository; // Handles all job entity queries now + private final SchedulerDetailRepository schedulerDetailRepository; private final PaginationHelper paginationHelper; - @Autowired - public SchedulerJobRunnerReadServiceImpl(final JdbcTemplate jdbcTemplate, final ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, ScheduledJobDetailRepository jobDetailRepository, - PaginationHelper paginationHelper) { - this.jdbcTemplate = jdbcTemplate; - this.columnValidator = columnValidator; - this.sqlGenerator = sqlGenerator; - this.jobDetailRepository = jobDetailRepository; - this.paginationHelper = paginationHelper; + @Override + public ScheduledJobDetail findByJobId(final Long jobId) { + return this.jobDetailRepository.findByJobId(jobId); + } + + @Override + public SchedulerDetail retrieveSchedulerDetail() { + final List schedulerDetailList = schedulerDetailRepository.findAll(); + if (schedulerDetailList != null && !schedulerDetailList.isEmpty()) { + return schedulerDetailList.get(0); + } + return null; } @Override public List findAllJobDetails() { return jobDetailRepository.getAllData(); - } @Override diff --git a/fineract-validation/src/main/resources/ValidationMessages.properties b/fineract-validation/src/main/resources/ValidationMessages.properties index 1e4e9d56fbe..ce21b524560 100644 --- a/fineract-validation/src/main/resources/ValidationMessages.properties +++ b/fineract-validation/src/main/resources/ValidationMessages.properties @@ -134,3 +134,8 @@ org.apache.fineract.portfolio.meeting.date-format.not-null=The parameter 'dateFo org.apache.fineract.portfolio.meeting.locale.not-null=The parameter 'locale' is mandatory org.apache.fineract.portfolio.meeting.attendance.client-id.not-null=The parameter 'clientId' is mandatory org.apache.fineract.portfolio.meeting.attendance.attendance-type.not-null=The parameter 'attendanceType' is mandatory + +# infrastructure + +org.apache.fineract.infrastructure.jobs.update.at-least-one-field=At least one of displayName, cronExpression, or active must be provided +org.apache.fineract.infrastructure.jobs.cron-expression.invalid=The parameter 'cronExpression' is not a valid cron expression diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java index ee47125e35a..1c8a25fd8f7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java @@ -52,11 +52,11 @@ import org.apache.fineract.client.models.BusinessDateUpdateRequest; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.JobUpdateRequest; import org.apache.fineract.client.models.JournalEntryTransactionItem; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; -import org.apache.fineract.client.models.PutJobsJobIDRequest; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -1375,15 +1375,14 @@ public void businessDateIsCorrectForCronJob() throws InterruptedException { .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10", null)); this.schedulerJobHelper.updateSchedulerStatus(true); - this.schedulerJobHelper.updateSchedulerJob(16L, new PutJobsJobIDRequest().active(true).cronExpression("0/1 * * * * ?")); - + this.schedulerJobHelper.updateSchedulerJob(new JobUpdateRequest().jobId(16L).active(true).cronExpression("0/1 * * * * ?")); Thread.sleep(2000); GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); assertEquals(LocalDate.of(2022, 9, 5), loanDetails.getTransactions().get(1).getDate()); } finally { globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, new PutGlobalConfigurationsRequest().enabled(false)); - this.schedulerJobHelper.updateSchedulerJob(16L, new PutJobsJobIDRequest().cronExpression("0 2 0 1/1 * ? *")); + this.schedulerJobHelper.updateSchedulerJob(new JobUpdateRequest().jobId(16L).cronExpression("0 2 0 1/1 * ? *")); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java index 663bade65f0..29debe7b17c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java @@ -44,7 +44,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.fineract.client.models.GetJobsResponse; -import org.apache.fineract.client.models.PutJobsJobIDRequest; +import org.apache.fineract.client.models.JobUpdateRequest; import org.apache.fineract.client.util.Calls; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -147,8 +147,8 @@ public Map updateSchedulerJob(int jobId, final boolean active) { return response; } - public void updateSchedulerJob(long jobId, PutJobsJobIDRequest request) { - Calls.ok(FineractClientHelper.getFineractClient().jobs.updateJobDetail(jobId, request)); + public void updateSchedulerJob(JobUpdateRequest request) { + Calls.ok(FineractClientHelper.getFineractClient().jobs.updateJobDetail(request.getJobId(), request)); } // TODO: Rewrite to use fineract-client instead!