Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import static java.time.temporal.ChronoUnit.DAYS;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand All @@ -36,6 +38,8 @@
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
Expand Down Expand Up @@ -442,9 +446,10 @@ public static LocalDateTime convertDateTimeStringToLocalDateTime(String dateTime
if (dateTimeStr == null || dateTimeStr.isBlank()) {
return null;
}
String dateTimeStrWithoutOffset = removeOffsetFromString(dateTimeStr);
final Locale locale = localeStr == null ? null : JsonParserHelper.localeFromString(localeStr);
DateTimeFormatter formatter = getDateFormatter(dateFormat, locale);
TemporalAccessor parsed = formatter.parse(dateTimeStr);
TemporalAccessor parsed = formatter.parse(dateTimeStrWithoutOffset);

boolean hasTime = parsed.isSupported(ChronoField.HOUR_OF_DAY) && parsed.isSupported(ChronoField.MINUTE_OF_HOUR);

Expand All @@ -462,6 +467,146 @@ public static LocalDateTime convertDateTimeStringToLocalDateTime(String dateTime
}
}

public static OffsetDateTime convertDateTimeStringToOffsetDateTime(String dateTimeStr, String dateFormat, String localeStr,
LocalTime fallbackTime, ZoneOffset defaultOffset) {
if (dateTimeStr == null || dateTimeStr.isBlank()) {
return null;
}
String dateTimeStrWithoutOffset = removeOffsetFromString(dateTimeStr);
ZoneOffset offset = extractOffsetFromString(dateTimeStr, defaultOffset);
LocalDateTime localDateTime = convertDateTimeStringToLocalDateTime(dateTimeStrWithoutOffset, dateFormat, localeStr, fallbackTime);
if (localDateTime == null) {
return null;
}
return OffsetDateTime.of(localDateTime, offset);
}

public static OffsetDateTime convertDateTimeStringToOffsetDateTime(String dateTimeStr, String dateFormat, String localeStr,
LocalTime fallbackTime, String timeZone) {
if (dateTimeStr == null || dateTimeStr.isBlank()) {
return null;
}
String dateTimeStrWithoutOffset = removeOffsetFromString(dateTimeStr);
LocalDateTime localDateTime = convertDateTimeStringToLocalDateTime(dateTimeStrWithoutOffset, dateFormat, localeStr, fallbackTime);
if (localDateTime == null) {
return null;
}
ZoneOffset inlineOffset = extractOffsetFromStringOrNull(dateTimeStr);
if (inlineOffset != null) {
return OffsetDateTime.of(localDateTime, inlineOffset);
}
ZoneOffset defaultOffset = resolveOffset(timeZone, localDateTime);
return OffsetDateTime.of(localDateTime, defaultOffset);
}

public static ZoneOffset resolveOffset(String timeZone) {
return resolveOffset(timeZone, null);
}

public static ZoneOffset resolveOffset(String timeZone, LocalDateTime dateTime) {
if (timeZone == null || timeZone.isBlank()) {
return ZoneOffset.UTC;
}
try {
return ZoneOffset.of(timeZone);
} catch (DateTimeException e) {
try {
ZoneId zoneId = ZoneId.of(timeZone);
Instant instant = dateTime != null ? dateTime.atZone(zoneId).toInstant() : Instant.now();
return zoneId.getRules().getOffset(instant);
} catch (DateTimeException ex) {
final List<ApiParameterError> errors = List.of(ApiParameterError.parameterError("validation.msg.invalid.timezone",
"The parameter timeZone (" + timeZone + ") is invalid", "timeZone", timeZone));
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors,
ex);
}
}
}

private static ZoneOffset extractOffsetFromStringOrNull(String dateTimeStr) {
int offsetIndex = findOffsetIndex(dateTimeStr);
if (offsetIndex < 0) {
return null;
}
String offsetStr = dateTimeStr.substring(offsetIndex);
try {
return parseOffset(offsetStr);
} catch (DateTimeException e) {
final List<ApiParameterError> errors = List.of(ApiParameterError.parameterError("validation.msg.invalid.offset",
"The inline offset (" + offsetStr + ") is invalid", "offset", offsetStr));
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e);
}
}

private static final Pattern OFFSET_PATTERN = Pattern
.compile("(?<=[:\\s]\\d{2}|\\s)(Z|[+-][\\w:]+)$|(?<=\\d{2}:\\d{2}:\\d{2})(Z|[+-][\\w:]+)$", Pattern.CASE_INSENSITIVE);

private static int findOffsetIndex(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
Matcher matcher = OFFSET_PATTERN.matcher(str);
if (matcher.find()) {
return matcher.start();
}
return -1;
}

private static ZoneOffset parseOffset(String offsetStr) {
if (offsetStr == null || offsetStr.isBlank()) {
return ZoneOffset.UTC;
}
offsetStr = offsetStr.trim();
if ("Z".equalsIgnoreCase(offsetStr)) {
return ZoneOffset.UTC;
}
char sign = offsetStr.charAt(0);
if (sign != '+' && sign != '-') {
throw new DateTimeException("Invalid offset format: " + offsetStr);
}
String numPart = offsetStr.substring(1);
if (numPart.contains(":")) {
return ZoneOffset.of(offsetStr);
}
if (!numPart.matches("\\d+")) {
throw new DateTimeException("Invalid offset format: " + offsetStr);
}
int hours;
int minutes = 0;
if (numPart.length() <= 2) {
hours = Integer.parseInt(numPart);
} else if (numPart.length() == 4) {
hours = Integer.parseInt(numPart.substring(0, 2));
minutes = Integer.parseInt(numPart.substring(2, 4));
} else {
return ZoneOffset.of(offsetStr);
}
return ZoneOffset.ofHoursMinutes(sign == '-' ? -hours : hours, sign == '-' ? -minutes : minutes);
}

private static ZoneOffset extractOffsetFromString(String dateTimeStr, ZoneOffset defaultOffset) {
int offsetIndex = findOffsetIndex(dateTimeStr);
if (offsetIndex < 0) {
return defaultOffset;
}
String offsetStr = dateTimeStr.substring(offsetIndex);
try {
return parseOffset(offsetStr);
} catch (DateTimeException e) {
final List<ApiParameterError> errors = List.of(ApiParameterError.parameterError("validation.msg.invalid.offset",
"The inline offset (" + offsetStr + ") is invalid", "offset", offsetStr));
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e);
}
}

private static String removeOffsetFromString(String dateTimeStr) {
int offsetIndex = findOffsetIndex(dateTimeStr);
if (offsetIndex < 0) {
return dateTimeStr;
}
return dateTimeStr.substring(0, offsetIndex).trim();
}

/**
* Returns the earlier date. If date1 is before date2 it return date1 otherwise date2.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
Expand All @@ -39,7 +42,7 @@
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.PaginationParameters;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.utils.SQLBuilder;
import org.springframework.stereotype.Component;
Expand All @@ -59,7 +62,6 @@ public class AuditsApiResource {
private final PlatformSecurityContext context;
private final AuditReadPlatformService auditReadPlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final ToApiJsonSerializer<String> toApiJsonSerializer;

@GET
@Consumes({ MediaType.APPLICATION_JSON })
Expand All @@ -68,22 +70,20 @@ public class AuditsApiResource {
+ "\n" + "Example Requests:\n" + "\n" + "audits\n" + "\n" + "audits?fields=madeOnDate,maker,processingResult\n" + "\n"
+ "audits?makerDateTimeFrom=2013-03-25 08:00:00&makerDateTimeTo=2013-04-04 18:00:00\n" + "\n" + "audits?officeId=1\n" + "\n"
+ "audits?officeId=1&includeJson=true")
public String retrieveAuditEntries(@Context final UriInfo uriInfo, @BeanParam AuditRequest auditRequest,
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AuditData.class)))
public Page<AuditData> retrieveAuditEntries(@Context final UriInfo uriInfo, @BeanParam AuditRequest auditRequest,
@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,
@QueryParam("paged") @Parameter(description = "paged") final Boolean paged) {
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {

context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final PaginationParameters parameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final PaginationParameters parameters = PaginationParameters.builder().paged(true).limit(limit).offset(offset).orderBy(orderBy)
.sortOrder(sortOrder).build();
final SQLBuilder extraCriteria = getExtraCriteria(auditRequest);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());

return toApiJsonSerializer.serialize(parameters.isPaged()
? auditReadPlatformService.retrievePaginatedAuditEntries(extraCriteria, settings.isIncludeJson(), parameters)
: auditReadPlatformService.retrieveAuditEntries(extraCriteria, settings.isIncludeJson()));
return auditReadPlatformService.retrievePaginatedAuditEntries(extraCriteria, settings.isIncludeJson(), parameters);
}

@GET
Expand All @@ -92,6 +92,7 @@ public String retrieveAuditEntries(@Context final UriInfo uriInfo, @BeanParam Au
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an Audit Entry", description = "Example Requests:\n" + "\n" + "audits/20\n"
+ "audits/20?fields=madeOnDate,maker,processingResult")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AuditData.class)))
public AuditData retrieveAuditEntry(@PathParam("auditId") @Parameter final Long auditId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return auditReadPlatformService.retrieveAuditEntry(auditId);
Expand All @@ -104,6 +105,7 @@ public AuditData retrieveAuditEntry(@PathParam("auditId") @Parameter final Long
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Audit Search Template", description = "This is a convenience resource. It can be useful when building an Audit Search UI. \"appUsers\" are data scoped to the office/branch the requestor is associated with.\n"
+ "\n" + "Example Requests:\n" + "\n" + "audits/searchtemplate\n" + "audits/searchtemplate?fields=actionNames")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AuditSearchData.class)))
public AuditSearchData retrieveAuditSearchTemplate() {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.auditReadPlatformService.retrieveSearchTemplate("audit");
Expand All @@ -119,35 +121,35 @@ private SQLBuilder getExtraCriteria(AuditRequest auditRequest) {
extraCriteria.addNonNullCriteria("aud.resource_id = ", auditRequest.getResourceId());
extraCriteria.addNonNullCriteria("aud.maker_id = ", auditRequest.getMakerId());
extraCriteria.addNonNullCriteria("aud.checker_id = ", auditRequest.getCheckerId());
if (auditRequest.getMakerDateTimeFrom() != null) {
if (auditRequest.hasMakerDateTimeFrom()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.made_on_date >= ", auditRequest.getMakerDateTimeFrom(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.made_on_date_utc >= ", auditRequest.getMakerDateTimeFrom(),
criteria.addNonNullCriteria("aud.made_on_date_utc >= ", auditRequest.getMakerDateTimeFromOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
if (auditRequest.getMakerDateTimeTo() != null) {
if (auditRequest.hasMakerDateTimeTo()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.made_on_date <= ", auditRequest.getMakerDateTimeTo(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.made_on_date_utc <= ", auditRequest.getMakerDateTimeTo(),
criteria.addNonNullCriteria("aud.made_on_date_utc <= ", auditRequest.getMakerDateTimeToOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
if (auditRequest.getCheckerDateTimeFrom() != null) {
if (auditRequest.hasCheckerDateTimeFrom()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.checked_on_date >= ", auditRequest.getCheckerDateTimeFrom(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.checked_on_date_utc >= ", auditRequest.getCheckerDateTimeFrom(),
criteria.addNonNullCriteria("aud.checked_on_date_utc >= ", auditRequest.getCheckerDateTimeFromOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
if (auditRequest.getCheckerDateTimeTo() != null) {
if (auditRequest.hasCheckerDateTimeTo()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.checked_on_date <= ", auditRequest.getCheckerDateTimeTo(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.checked_on_date_utc <= ", auditRequest.getCheckerDateTimeTo(),
criteria.addNonNullCriteria("aud.checked_on_date_utc <= ", auditRequest.getCheckerDateTimeToOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,22 @@ private SQLBuilder getExtraCriteria(MakerCheckerRequest makerCheckerRequest) {
}
extraCriteria.addNonNullCriteria("aud.resource_id = ", makerCheckerRequest.getResourceId());
extraCriteria.addNonNullCriteria("aud.maker_id = ", makerCheckerRequest.getMakerId());
extraCriteria.addNonNullCriteria("aud.made_on_date >= ", makerCheckerRequest.getMakerDateTimeFrom());
extraCriteria.addNonNullCriteria("aud.made_on_date <= ", makerCheckerRequest.getMakerDateTimeTo());
if (makerCheckerRequest.hasMakerDateTimeFrom()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.made_on_date >= ", makerCheckerRequest.getMakerDateTimeFromLocal(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.made_on_date_utc >= ", makerCheckerRequest.getMakerDateTimeFromOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
if (makerCheckerRequest.hasMakerDateTimeTo()) {
extraCriteria.addSubOperation((SQLBuilder criteria) -> {
criteria.addNonNullCriteria("aud.made_on_date <= ", makerCheckerRequest.getMakerDateTimeToLocal(),
SQLBuilder.WhereLogicalOperator.NONE);
criteria.addNonNullCriteria("aud.made_on_date_utc <= ", makerCheckerRequest.getMakerDateTimeToOffset(),
SQLBuilder.WhereLogicalOperator.OR);
});
}
extraCriteria.addNonNullCriteria("aud.office_id = ", makerCheckerRequest.getOfficeId());
extraCriteria.addNonNullCriteria("aud.group_id = ", makerCheckerRequest.getGroupId());
extraCriteria.addNonNullCriteria("aud.client_id = ", makerCheckerRequest.getClientId());
Expand Down
Loading
Loading