FINERACT-2119: Address jobs by name

This commit is contained in:
Marta Jankovics
2024-08-28 16:46:20 +02:00
committed by Adam Saghy
parent eef6a8da5e
commit 3aaa2ffe9c
27 changed files with 761 additions and 185 deletions
@@ -54,6 +54,7 @@ public final class SchedulerJobApiConstants {
public static final String COMMAND_START_SCHEDULER = "start";
public static final String COMMAND = "command";
public static final String JOB_ID = "jobId";
public static final String SHORT_NAME_PARAM = "short-name";
public static final String JOB_RUN_HISTORY = "runhistory";
public static final String SCHEDULER_STATUS_PATH = "scheduler";
@@ -18,6 +18,11 @@
*/
package org.apache.fineract.infrastructure.jobs.api;
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS;
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.JOB_HISTORY_RESPONSE_DATA_PARAMETERS;
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME;
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.SHORT_NAME_PARAM;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -27,6 +32,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -41,12 +47,14 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -88,10 +96,10 @@ public class SchedulerJobApiResource {
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class)))) })
public String retrieveAll(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
final List<JobDetailData> jobDetailDatas = this.schedulerJobRunnerReadService.findAllJobDeatils();
this.context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
final List<JobDetailData> jobDetailDatas = this.schedulerJobRunnerReadService.findAllJobDetails();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, jobDetailDatas, SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
return this.toApiJsonSerializer.serialize(settings, jobDetailDatas, JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
}
@GET
@@ -101,10 +109,19 @@ public class SchedulerJobApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class))) })
public String retrieveOne(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
final JobDetailData jobDetailData = this.schedulerJobRunnerReadService.retrieveOne(jobId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, jobDetailData, SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
return retrieveOne(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), uriInfo);
}
@GET
@Path(SHORT_NAME_PARAM + "/{shortName}")
@Operation(summary = "Retrieve a Job", description = "Returns the details of a Job bu shortName.\n" + "\n" + "Example Requests:\n"
+ "\n" + "jobs/short-name/SA_PINT")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class))) })
public String retrieveByShortName(
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
@Context final UriInfo uriInfo) {
return retrieveOne(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, uriInfo);
}
@GET
@@ -118,16 +135,22 @@ public class SchedulerJobApiResource {
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
.sortOrder(sortOrder).build();
final Page<JobDetailHistoryData> jobhistoryDetailData = this.schedulerJobRunnerReadService.retrieveJobHistory(jobId,
searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.jobHistoryToApiJsonSerializer.serialize(settings, jobhistoryDetailData,
SchedulerJobApiConstants.JOB_HISTORY_RESPONSE_DATA_PARAMETERS);
return retrieveHistory(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), offset, limit, orderBy, sortOrder, uriInfo);
}
@GET
@Path(SHORT_NAME_PARAM + "/{shortName}/" + SchedulerJobApiConstants.JOB_RUN_HISTORY)
@Operation(summary = "Retrieve Job Run History", description = "Example Requests:\n" + "\n"
+ "jobs/short-name/SA_PINT/runhistory?offset=0&limit=200")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsJobIDJobRunHistoryResponse.class))) })
public String retrieveHistoryByShortName(@Context final UriInfo uriInfo,
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
return retrieveHistory(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, offset, limit, orderBy, sortOrder, uriInfo);
}
@POST
@@ -138,17 +161,78 @@ public class SchedulerJobApiResource {
public Response executeJob(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
@QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam,
@Parameter(hidden = true) final String jsonRequestBody) {
// check the logged in user have permissions to execute scheduler jobs
return executeJob(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), commandParam, jsonRequestBody);
}
@POST
@Path(SHORT_NAME_PARAM + "/{shortName}")
@Operation(summary = "Run a Job", description = "Manually Execute Specific Job.")
@RequestBody(content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.ExecuteJobRequest.class)))
@ApiResponses({ @ApiResponse(responseCode = "200", description = "POST: jobs/short-name/SA_PINT?command=executeJob") })
public Response executeJobByShortName(
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
@QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam,
@Parameter(hidden = true) final String jsonRequestBody) {
return executeJob(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, commandParam, jsonRequestBody);
}
@PUT
@Path("{" + SchedulerJobApiConstants.JOB_ID + "}")
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
@ApiResponses({ @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);
}
@PUT
@Path(SHORT_NAME_PARAM + "/{shortName}")
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String updateJobDetailByShortName(
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
@Parameter(hidden = true) final String jsonRequestBody) {
return updateJobDetail(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, jsonRequestBody);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
private String retrieveOne(@NotNull IdTypeResolver.IdType idType, String identifier, UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
final JobDetailData jobDetailData = schedulerJobRunnerReadService.retrieveOne(idType, identifier);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, jobDetailData, JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
}
private String retrieveHistory(@NotNull IdTypeResolver.IdType idType, String identifier, Integer offset, Integer limit, String orderBy,
String sortOrder, UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
.sortOrder(sortOrder).build();
final Page<JobDetailHistoryData> jobHistoryData = schedulerJobRunnerReadService.retrieveJobHistory(idType, identifier,
searchParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return jobHistoryToApiJsonSerializer.serialize(settings, jobHistoryData, JOB_HISTORY_RESPONSE_DATA_PARAMETERS);
}
private Response executeJob(@NotNull IdTypeResolver.IdType idType, String identifier, String commandParam, String jsonRequestBody) {
// check the logged-in user have permissions to execute scheduler jobs
Response response;
if (fineractProperties.getMode().isBatchManagerEnabled()) {
final boolean hasNotPermission = this.context.authenticatedUser().hasNotPermissionForAnyOf("ALL_FUNCTIONS",
"EXECUTEJOB_SCHEDULER");
final boolean hasNotPermission = context.authenticatedUser().hasNotPermissionForAnyOf("ALL_FUNCTIONS", "EXECUTEJOB_SCHEDULER");
if (hasNotPermission) {
final String authorizationMessage = "User has no authority to execute scheduler jobs";
throw new NoAuthorizationException(authorizationMessage);
}
response = Response.status(400).build();
if (is(commandParam, SchedulerJobApiConstants.COMMAND_EXECUTE_JOB)) {
Long jobId = schedulerJobRunnerReadService.retrieveId(idType, identifier);
jobRegisterService.executeJobWithParameters(jobId, jsonRequestBody);
response = Response.status(202).build();
} else {
@@ -161,14 +245,8 @@ public class SchedulerJobApiResource {
return response;
}
@PUT
@Path("{" + SchedulerJobApiConstants.JOB_ID + "}")
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String updateJobDetail(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
@Parameter(hidden = true) final String jsonRequestBody) {
private String updateJobDetail(@NotNull IdTypeResolver.IdType idType, String identifier, String jsonRequestBody) {
Long jobId = schedulerJobRunnerReadService.retrieveId(idType, identifier);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.updateJobDetail(jobId) //
.withJson(jsonRequestBody) //
@@ -180,8 +258,4 @@ public class SchedulerJobApiResource {
}
return this.toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
}
@@ -44,6 +44,8 @@ final class SchedulerJobApiResourceSwagger {
public Long jobId;
@Schema(example = "Update loan Summary")
public String displayName;
@Schema(example = "LA_USUM")
public String shortName;
@Schema(example = "")
public Date nextRunTime;
@Schema(example = "")
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.persistence.UniqueConstraint;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -36,7 +37,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants;
@Entity
@Table(name = "job")
@Table(name = "job", uniqueConstraints = { @UniqueConstraint(columnNames = { "short_name" }, name = "job_short_name_key") })
@Getter
@Setter
@NoArgsConstructor
@@ -97,6 +98,9 @@ public class ScheduledJobDetail extends AbstractPersistableCustom<Long> {
@Column(name = "is_misfired")
private boolean triggerMisfired;
@Column(name = "short_name", nullable = false)
private String shortName;
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(9);
@@ -20,6 +20,8 @@ package org.apache.fineract.infrastructure.jobs.domain;
import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.infrastructure.jobs.data.JobDetailData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
@@ -47,4 +49,22 @@ public interface ScheduledJobDetailRepository
ScheduledJobDetail findByJobName(String jobName);
String GET_DATA = "select new org.apache.fineract.infrastructure.jobs.data.JobDetailData(j.id, j.jobDisplayName, j.shortName, j.nextRunTime, "
+ "j.errorLog, j.cronExpression, j.activeSchedular, j.currentlyRunning, "
+ "jh.version, jh.startTime, jh.endTime, jh.status, jh.errorMessage, jh.triggerType, jh.errorLog) "
+ "from ScheduledJobDetail j left join ScheduledJobRunHistory jh on jh.scheduledJobDetail = j and j.previousRunStartTime = jh.startTime ";
@Query(GET_DATA + "where j.id = :jobId")
JobDetailData getDataById(@Param("jobId") Long jobId);
@Query(GET_DATA + "where j.shortName = :shortName")
JobDetailData getDataByShortName(@Param("shortName") String shortName);
@Query(GET_DATA + "order by j.id")
List<JobDetailData> getAllData();
boolean existsByShortName(String shortName);
@Query("select j.id from ScheduledJobDetail j where j.shortName = :shortName")
Optional<Long> findIdByShortName(@Param("shortName") String shortName);
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.jobs.exception;
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.dao.EmptyResultDataAccessException;
@@ -31,6 +32,10 @@ public class JobNotFoundException extends AbstractPlatformResourceNotFoundExcept
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier);
}
public JobNotFoundException(IdTypeResolver.IdType idType, String jobId) {
super("error.msg.sheduler.job.id.invalid", String.format("Job with %s: %s cannot be found", idType, jobId), idType, jobId);
}
public JobNotFoundException(final String identifier, NoSuchJobException e) {
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier, e);
}
@@ -18,22 +18,24 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
import jakarta.validation.constraints.NotNull;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
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.ScheduledJobDetailRepository;
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.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@@ -47,59 +49,62 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
private final JdbcTemplate jdbcTemplate;
private final ColumnValidator columnValidator;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final ScheduledJobDetailRepository jobDetailRepository;
private final PaginationHelper paginationHelper;
@Autowired
public SchedulerJobRunnerReadServiceImpl(final JdbcTemplate jdbcTemplate, final ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
DatabaseSpecificSQLGenerator sqlGenerator, ScheduledJobDetailRepository jobDetailRepository,
PaginationHelper paginationHelper) {
this.jdbcTemplate = jdbcTemplate;
this.columnValidator = columnValidator;
this.sqlGenerator = sqlGenerator;
this.jobDetailRepository = jobDetailRepository;
this.paginationHelper = paginationHelper;
}
@Override
public List<JobDetailData> findAllJobDeatils() {
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
final String sql = detailMapper.schema();
final List<JobDetailData> JobDeatils = this.jdbcTemplate.query(sql, detailMapper, new Object[] {});
return JobDeatils;
public List<JobDetailData> findAllJobDetails() {
return jobDetailRepository.getAllData();
}
@Override
public JobDetailData retrieveOne(final Long jobId) {
try {
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
final String sql = detailMapper.schema() + " where job.id=?";
return this.jdbcTemplate.queryForObject(sql, detailMapper, new Object[] { jobId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new JobNotFoundException(String.valueOf(jobId), e);
public JobDetailData retrieveOne(@NotNull IdTypeResolver.IdType idType, String identifier) {
JobDetailData jobDetail = switch (idType) {
case ID -> jobDetailRepository.getDataById(Long.valueOf(identifier));
case SHORT_NAME -> jobDetailRepository.getDataByShortName(identifier);
default -> null;
};
if (jobDetail == null) {
throw new JobNotFoundException(idType, identifier);
}
return jobDetail;
}
@Override
public JobDetailData retrieveOneByName(String jobName) {
try {
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
final String sql = detailMapper.schema() + " where job.name=?";
return this.jdbcTemplate.queryForObject(sql, detailMapper, new Object[] { jobName }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new JobNotFoundException(jobName, e);
}
}
@Override
public Page<JobDetailHistoryData> retrieveJobHistory(final Long jobId, final SearchParameters searchParameters) {
if (!isJobExist(jobId)) {
throw new JobNotFoundException(String.valueOf(jobId));
public Page<JobDetailHistoryData> retrieveJobHistory(@NotNull IdTypeResolver.IdType idType, String identifier,
SearchParameters searchParameters) {
if (!isJobExist(idType, identifier)) {
throw new JobNotFoundException(idType, identifier);
}
final JobHistoryMapper jobHistoryMapper = new JobHistoryMapper(sqlGenerator);
final StringBuilder sqlBuilder = new StringBuilder(200);
sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " ");
sqlBuilder.append(jobHistoryMapper.schema());
sqlBuilder.append(" where job.id=?");
final StringBuilder sqlBuilder = new StringBuilder("select " + sqlGenerator.calcFoundRows() + " ").append(jobHistoryMapper.schema())
.append(" where job.");
Object idParam;
switch (idType) {
case ID -> {
sqlBuilder.append("id");
idParam = Long.valueOf(identifier);
}
case SHORT_NAME -> {
sqlBuilder.append("short_name");
idParam = identifier;
}
default -> throw new JobNotFoundException(idType, identifier);
}
sqlBuilder.append(" = ?");
if (searchParameters.hasOrderBy()) {
sqlBuilder.append(" order by ").append(searchParameters.getOrderBy());
this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy());
@@ -118,7 +123,18 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
}
}
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { jobId }, jobHistoryMapper);
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { idParam }, jobHistoryMapper);
}
@Override
@NotNull
public Long retrieveId(@NotNull IdTypeResolver.IdType idType, String identifier) {
return switch (idType) {
case ID -> Long.valueOf(identifier);
case SHORT_NAME ->
jobDetailRepository.findIdByShortName(identifier).orElseThrow(() -> new JobNotFoundException(idType, identifier));
default -> throw new JobNotFoundException(idType, identifier);
};
}
@Override
@@ -133,84 +149,27 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
return true;
}
private boolean isJobExist(final Long jobId) {
boolean isJobPresent = false;
try {
final String sql = "select count(*) from job job where job.id= ?";
final int count = this.jdbcTemplate.queryForObject(sql, Integer.class, new Object[] { jobId });
if (count == 1) {
isJobPresent = true;
}
return isJobPresent;
} catch (EmptyResultDataAccessException e) {
return isJobPresent;
}
}
private static final class JobDetailMapper implements RowMapper<JobDetailData> {
private final StringBuilder sqlBuilder;
JobDetailMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
sqlBuilder = new StringBuilder("select").append(
" job.id,job.display_name as displayName,job.next_run_time as nextRunTime,job.initializing_errorlog as initializingError,job.cron_expression as cronExpression,job.is_active as active,job.currently_running as currentlyRunning,")
.append(" runHistory.version,runHistory.start_time as lastRunStartTime,runHistory.end_time as lastRunEndTime,runHistory.")
.append(sqlGenerator.escape("status"))
.append(",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
.append(" from job job left join job_run_history runHistory ON job.id=runHistory.job_id and job.previous_run_start_time=runHistory.start_time ");
}
public String schema() {
return this.sqlBuilder.toString();
}
@Override
public JobDetailData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String displayName = rs.getString("displayName");
final Date nextRunTime = rs.getTimestamp("nextRunTime");
final String initializingError = rs.getString("initializingError");
final String cronExpression = rs.getString("cronExpression");
final boolean active = rs.getBoolean("active");
final boolean currentlyRunning = rs.getBoolean("currentlyRunning");
final Long version = rs.getLong("version");
final Date jobRunStartTime = rs.getTimestamp("lastRunStartTime");
final Date jobRunEndTime = rs.getTimestamp("lastRunEndTime");
final String status = rs.getString("status");
final String jobRunErrorMessage = rs.getString("jobRunErrorMessage");
final String triggerType = rs.getString("triggerType");
final String jobRunErrorLog = rs.getString("jobRunErrorLog");
JobDetailHistoryData lastRunHistory = null;
if (version > 0) {
lastRunHistory = new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime)
.setJobRunEndTime(jobRunEndTime).setStatus(status).setJobRunErrorMessage(jobRunErrorMessage)
.setTriggerType(triggerType).setJobRunErrorLog(jobRunErrorLog);
}
final JobDetailData jobDetail = new JobDetailData().setJobId(id).setDisplayName(displayName).setNextRunTime(nextRunTime)
.setInitializingError(initializingError).setCronExpression(cronExpression).setActive(active)
.setCurrentlyRunning(currentlyRunning).setLastRunHistory(lastRunHistory);
return jobDetail;
}
private boolean isJobExist(@NotNull IdTypeResolver.IdType idType, @NotNull String jobId) {
return switch (idType) {
case ID -> jobDetailRepository.existsById(Long.valueOf(jobId));
case SHORT_NAME -> jobDetailRepository.existsByShortName(jobId);
default -> false;
};
}
private static final class JobHistoryMapper implements RowMapper<JobDetailHistoryData> {
private final StringBuilder sqlBuilder;
private final String sql;
JobHistoryMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
sqlBuilder = new StringBuilder(200)
.append(" runHistory.version,runHistory.start_time as runStartTime,runHistory.end_time as runEndTime,runHistory."
+ sqlGenerator.escape("status")
+ ",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
.append(" from job job join job_run_history runHistory ON job.id=runHistory.job_id");
sql = " runHistory.version, runHistory.start_time as runStartTime, runHistory.end_time as runEndTime, " + "runHistory."
+ sqlGenerator.escape("status")
+ ", runHistory.error_message as jobRunErrorMessage, runHistory.trigger_type as triggerType, runHistory.error_log as jobRunErrorLog "
+ "from job job join job_run_history runHistory ON job.id=runHistory.job_id";
}
public String schema() {
return this.sqlBuilder.toString();
return sql;
}
@Override
@@ -222,12 +181,9 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
final String jobRunErrorMessage = rs.getString("jobRunErrorMessage");
final String triggerType = rs.getString("triggerType");
final String jobRunErrorLog = rs.getString("jobRunErrorLog");
final JobDetailHistoryData jobDetailHistory = new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime)
.setJobRunEndTime(jobRunEndTime).setStatus(status).setJobRunErrorMessage(jobRunErrorMessage).setTriggerType(triggerType)
return new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime).setJobRunEndTime(jobRunEndTime)
.setStatus(status).setJobRunErrorMessage(jobRunErrorMessage).setTriggerType(triggerType)
.setJobRunErrorLog(jobRunErrorLog);
return jobDetailHistory;
}
}
}
@@ -31,6 +31,10 @@
<include file="tenant-store/changelog-tenant-store.xml" relativeToChangelogFile="true" context="tenant_store_db AND !initial_switch"/>
<include file="tenant/initial-switch-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND initial_switch"/>
<include file="tenant/changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
<includeAll path="db/changelog/tenant/module" errorIfMissingOrEmpty="false" context="tenant_db AND custom_changelog" filter="org.apache.fineract.infrastructure.core.service.migration.TenantModuleRootFilter" />
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND custom_changelog" />
<!-- Add new module to the end of this modules list (to keep the existing auto-increment identifiers) -->
<include file="db/changelog/tenant/module/loan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
<include file="db/changelog/tenant/module/investor/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND !initial_switch AND custom_changelog"/>
<!-- Scripts to run after the modules were initialized -->
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
</databaseChangeLog>
@@ -164,4 +164,5 @@
<include file="parts/0142_add_accrual_activity_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0143_add_accrual_activity_posting_job.xml" relativeToChangelogFile="true" />
<include file="parts/0144_transaction_summary_with_asset_owner_report_unc_allocation_fix.xml" relativeToChangelogFile="true" />
<include file="parts/0145_job_short_name.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<include file="parts/0146_add_final_constraints.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="job">
<column name="short_name" type="VARCHAR(8)">
<constraints unique="true" nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<update tableName="job">
<column name="short_name" value="LA_USUM"/>
<where>name='Update loan Summary'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_ARAG"/>
<where>name='Update Loan Arrears Ageing'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_PIAD"/>
<where>name='Update Loan Paid In Advance'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_AHOL"/>
<where>name='Apply Holidays To Loans'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_TFFS"/>
<where>name='Transfer Fee For Loans From Savings'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_OPEN"/>
<where>name='Apply penalty to overdue loans'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_UNPA"/>
<where>name='Update Non Performing Assets'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_RINT"/>
<where>name='Recalculate Interest For Loans'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_GLPR"/>
<where>name='Generate Loan Loss Provisioning'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_AATR"/>
<where>name='Add Accrual Transactions For Loans With Income Posted As Transactions'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_ECOB"/>
<where>name='Loan COB'</where>
</update>
<update tableName="job">
<column name="short_name" value="LA_DECL"/>
<where>name='Loan Delinquency Classification'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_AANF"/>
<where>name='Apply Annual Fee For Savings'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_PINT"/>
<where>name='Post Interest For Savings'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_PDCH"/>
<where>name='Pay Due Savings Charges'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_TINT"/>
<where>name='Transfer Interest To Savings'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_MATD"/>
<where>name='Update Deposit Accounts Maturity details'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_GSCH"/>
<where>name='Generate Mandatory Savings Schedule'</where>
</update>
<update tableName="job">
<column name="short_name" value="SA_UDOR"/>
<where>name='Update Savings Dormant Accounts'</where>
</update>
<update tableName="job">
<column name="short_name" value="SH_PDIV"/>
<where>name='Post Dividends For Shares'</where>
</update>
<update tableName="job">
<column name="short_name" value="ACC_RBAL"/>
<where>name='Update Accounting Running Balances'</where>
</update>
<update tableName="job">
<column name="short_name" value="ACC_AATR"/>
<where>name='Add Accrual Transactions'</where>
</update>
<update tableName="job">
<column name="short_name" value="ACC_APTR"/>
<where>name='Add Periodic Accrual Transactions'</where>
</update>
<update tableName="job">
<column name="short_name" value="STI_EXEC"/>
<where>name='Execute Standing Instruction'</where>
</update>
<update tableName="job">
<column name="short_name" value="RMJ_EXEC"/>
<where>name='Execute Report Mailing Jobs'</where>
</update>
<update tableName="job">
<column name="short_name" value="EM_EXEC"/>
<where>name='Execute Email'</where>
</update>
<update tableName="job">
<column name="short_name" value="EM_UOUT"/>
<where>name='Update Email Outbound with campaign message'</where>
</update>
<update tableName="job">
<column name="short_name" value="SMS_UOUT"/>
<where>name='Update SMS Outbound with Campaign Message'</where>
</update>
<update tableName="job">
<column name="short_name" value="SMS_SMSG"/>
<where>name='Send Messages to SMS Gateway'</where>
</update>
<update tableName="job">
<column name="short_name" value="SMS_DRPT"/>
<where>name='Get Delivery Reports from SMS Gateway'</where>
</update>
<update tableName="job">
<column name="short_name" value="ADH_GSCH"/>
<where>name='Generate AdhocClient Schedule'</where>
</update>
<update tableName="job">
<column name="short_name" value="TBL_UDET"/>
<where>name='Update Trial Balance Details'</where>
</update>
<update tableName="job">
<column name="short_name" value="JOB_EXEC"/>
<where>name='Execute All Dirty Jobs'</where>
</update>
<update tableName="job">
<column name="short_name" value="BDT_INC1"/>
<where>name='Increase Business Date by 1 day'</where>
</update>
<update tableName="job">
<column name="short_name" value="BDT_INC1"/>
<where>name='Increase Business Date by 1 day'</where>
</update>
<update tableName="job">
<column name="short_name" value="BDT_COB1"/>
<where>name='Increase COB Date by 1 day'</where>
</update>
<update tableName="job">
<column name="short_name" value="ASE_SEND"/>
<where>name='Send Asynchronous Events'</where>
</update>
<update tableName="job">
<column name="short_name" value="EXE_PURG"/>
<where>name='Purge External Events'</where>
</update>
<update tableName="job">
<column name="short_name" value="COM_PURG"/>
<where>name='Purge Processed Commands'</where>
</update>
</changeSet>
<changeSet author="fineract" id="3">
<update tableName="job">
<column name="short_name" value="ACC_ACPO"/>
<where>name='Accrual Activity Posting'</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<addNotNullConstraint tableName="job" columnName="short_name"/>
</changeSet>
<changeSet author="fineract" id="2" context="mysql">
<addNotNullConstraint tableName="job" columnName="short_name" columnDataType="VARCHAR(8)"/>
</changeSet>
</databaseChangeLog>