ValidationService.java
package cf.maybelambda.httpvalidator.springboot.service;
import cf.maybelambda.httpvalidator.springboot.model.ValidationTask;
import cf.maybelambda.httpvalidator.springboot.persistence.XMLValidationTaskDao;
import cf.maybelambda.httpvalidator.springboot.util.HttpSendOutcomeWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Service;
import javax.management.modelmbean.XMLParseException;
import java.io.FileNotFoundException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.rmi.ConnectIOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import static cf.maybelambda.httpvalidator.springboot.HTTPValidatorWebApp.RUN_SCHEDULE_PROPERTY;
import static cf.maybelambda.httpvalidator.springboot.controller.AppInfoController.START_TIME_KEY;
import static cf.maybelambda.httpvalidator.springboot.controller.AppInfoController.TASKS_FAILED_KEY;
import static cf.maybelambda.httpvalidator.springboot.controller.AppInfoController.TASKS_OK_KEY;
import static cf.maybelambda.httpvalidator.springboot.controller.AppInfoController.TASKS_TOTAL_KEY;
import static cf.maybelambda.httpvalidator.springboot.controller.AppInfoController.TIME_ELAPSED_KEY;
import static java.net.http.HttpRequest.BodyPublishers.ofString;
import static java.util.Objects.nonNull;
import static javax.swing.text.html.FormSubmitEvent.MethodType.POST;
/**
* Service for handling validation tasks, executing scheduled validations,
* and managing validation results.
*/
@Service
public class ValidationService {
public static final String HEADER_KEY_VALUE_DELIMITER = "|";
private final Duration TIMEOUT_SECONDS = Duration.ofSeconds(10);
private Duration lrTimeElapsed;
private String lrStartDateTime;
private int[] lrTaskCounts;
private HttpClient client;
private static Logger logger = LoggerFactory.getLogger(ValidationService.class);
@Autowired
private EmailNotificationService notificationService;
@Autowired
private XMLValidationTaskDao taskReader;
@Autowired
private Environment env;
@Autowired
private ObjectMapper mapper;
/**
* Constructor to initialize the HTTP client with default connection-timeout and follow-redirects settings.
*/
public ValidationService() {
this.client = HttpClient.newBuilder()
.connectTimeout(TIMEOUT_SECONDS)
.followRedirects(HttpClient.Redirect.ALWAYS).build();
}
/**
* Executes validation tasks periodically based on a cron schedule.
* Retrieves tasks, sends HTTP requests, and processes responses.
* Sends email notifications for any validation failures and
* updates information about the last run of validation tasks.
*
* @throws FileNotFoundException if the data file is not found
* @throws XMLParseException if there is an error parsing the XML file
* @throws JsonProcessingException when a validation task contains invalid JSON content
* @throws ConnectIOException if there is an error sending notification email
* @throws ExecutionException when an unhandled error occurs while processing the HTTP requests
* @throws InterruptedException when interrupted before completing all the requests
*/
@Scheduled(cron = "${" + RUN_SCHEDULE_PROPERTY + "}")
public void execValidations() throws FileNotFoundException, XMLParseException, JsonProcessingException,
ConnectIOException, ExecutionException, InterruptedException {
// Record the start date-time of the validation process
Instant start = Instant.now();
String startDT = EventListenerService.getCurrentDateTime();
List<ValidationTask> tasks = this.taskReader.getAll();
List<HttpSendOutcomeWrapper> results = this.buildAndExecuteRequests(tasks);
// Process the results and get the task counts
int[] taskCounts = this.processRequestResultsAndNotify(tasks, results);
// Update task counts and timing information of the last run
this.lrTaskCounts = taskCounts;
this.lrStartDateTime = startDT;
this.lrTimeElapsed = Duration.between(start, Instant.now());
}
/**
* Executes HTTP requests asynchronously and stores the resulting responses or exceptions.
* Builds the requests from the information in the provided tasks.
*
* @param tasks the list of validation tasks
* @return a list of HttpSendOutcomeWrapper objects containing the responses or exceptions
* @throws ExecutionException when an unhandled error occurs while processing the HTTP requests
* @throws InterruptedException when interrupted before completing all the requests
* @throws JsonProcessingException when a validation task contains invalid JSON content
*/
List<HttpSendOutcomeWrapper> buildAndExecuteRequests(List<ValidationTask> tasks) throws ExecutionException, InterruptedException, JsonProcessingException {
// Build HTTP requests from the validation tasks
List<HttpRequest> reqs = new ArrayList<>();
for (ValidationTask task : tasks) {
HttpRequest.Builder req = HttpRequest.newBuilder();
req.uri(URI.create(task.reqURL()));
task.reqHeaders().forEach(h -> req.headers(h.split(Pattern.quote(HEADER_KEY_VALUE_DELIMITER))));
req.timeout(TIMEOUT_SECONDS);
if (POST.equals(task.reqMethod())) {
req.POST(ofString(this.mapper.writeValueAsString(task.reqBody())));
}
reqs.add(req.build());
}
List<HttpSendOutcomeWrapper> results = new ArrayList<>(reqs.size());
IntStream.range(0, reqs.size()).forEach(i -> results.add(null));
// Send the requests asynchronously and store the responses or exceptions in the results list
// Use the index of each request to store the corresponding response or exception
List<CompletableFuture<Void>> futures = IntStream.range(0, reqs.size())
.mapToObj(i -> client.sendAsync(reqs.get(i), HttpResponse.BodyHandlers.ofString())
.thenAccept(res -> results.set(i, new HttpSendOutcomeWrapper(res)))
.exceptionally(e -> {
results.set(i, new HttpSendOutcomeWrapper(e));
return null;
}))
.toList();
// Wait for all requests to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
return results;
}
/**
* Processes the results of the HTTP requests, logging the outcomes.
* Sends notifications if there are any failures.
*
* @param tasks the list of validation tasks
* @param results the list of HttpSendOutcomeWrapper objects containing the responses or exceptions
* @return an array of task counts, where index 0 is the total tasks, 1 is successful tasks, and 2 is failed tasks
* @throws ConnectIOException if there is an error sending notification email
*/
int[] processRequestResultsAndNotify(List<ValidationTask> tasks, List<HttpSendOutcomeWrapper> results) throws ConnectIOException {
// Initialize task counts: [total tasks, successful tasks, failed tasks]
int[] taskCounts = new int[3];
// Set the total number of tasks
taskCounts[0] = tasks.size();
// Iterate over the results and register the outcomes in the taskCounts and the log
List<String[]> failures = new ArrayList<>();
for (int i = 0; i < results.size(); i++) {
ValidationTask task = tasks.get(i);
HttpSendOutcomeWrapper res = results.get(i);
String logmsg = "VALIDATION ";
if (res.isWholeResponse() && task.isValid(res.getStatusCode(), res.getBody())) {
logmsg += "OK";
taskCounts[1]++;
} else {
failures.add(new String[]{task.reqURL(), String.valueOf(res.getStatusCode()), res.getBody()});
logmsg += "FAILURE";
taskCounts[2]++;
}
logger.info(logmsg + " for: " + task.reqURL());
}
// Send notification if there are any failures
if (!failures.isEmpty()) {
this.notificationService.sendVTaskErrorsNotification(failures);
}
return taskCounts;
}
/**
* Retrieves information about the last run of validation tasks.
*
* @return A map containing start time, time elapsed, total tasks, successful tasks and failed tasks.
*/
public Map<String, String> getLastRunInfo() {
Map<String, String> res = new HashMap<>();
if (nonNull(this.lrTimeElapsed)) {
res.put(START_TIME_KEY, this.lrStartDateTime);
res.put(TIME_ELAPSED_KEY, String.valueOf(this.lrTimeElapsed.getSeconds()));
res.put(TASKS_TOTAL_KEY, String.valueOf(this.lrTaskCounts[0]));
res.put(TASKS_OK_KEY, String.valueOf(this.lrTaskCounts[1]));
res.put(TASKS_FAILED_KEY, String.valueOf(this.lrTaskCounts[2]));
}
return res;
}
/**
* Checks if the configuration is valid by validating the cron expression currently in use.
*
* @return true if the configuration is valid, false otherwise
*/
public boolean isValidConfig() {
return this.isValidCronExpression(this.env.getProperty(RUN_SCHEDULE_PROPERTY));
}
/**
* Validates a given cron expression.
*
* @param cronExpr Cron expression to validate
* @return true if the cron expression is valid, false otherwise
*/
public boolean isValidCronExpression(String cronExpr) {
boolean ans = true;
if (!"-".equals(cronExpr)) {
try {
CronExpression.parse(cronExpr);
} catch (IllegalArgumentException e) {
ans = false;
}
}
return ans;
}
/**
* Sets the HTTP client. Used for testing purposes.
*
* @param client HTTP client
*/
void setClient(HttpClient client) { this.client = client; }
/**
* Sets the email notification service. Used for testing purposes.
*
* @param service Email notification service
*/
void setNotificationService(EmailNotificationService service) { this.notificationService = service; }
/**
* Sets the validation task reader service. Used for testing purposes.
*
* @param taskReader XML validation task DAO
*/
void setTaskReader(XMLValidationTaskDao taskReader) { this.taskReader = taskReader; }
/**
* Sets the logger. Used for testing purposes.
*
* @param logger Logger
*/
void setLogger(Logger logger) { ValidationService.logger = logger; }
/**
* Sets the environment object. Used for testing purposes.
*
* @param env Environment
*/
void setEnv(Environment env) { this.env = env; }
/**
* Sets the object mapper; for testing purposes.
*
* @param mapper The ObjectMapper to set.
*/
void setObjectMapper(ObjectMapper mapper) { this.mapper = mapper; }
}