/**
* ============LICENSE_START=======================================================
* org.onap.aai
* ================================================================================
* Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
* ================================================================================
* Modifications Copyright © 2018 IBM.
* ================================================================================
* Licensed 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.
* ============LICENSE_END=========================================================
*/
package org.onap.aai.prevalidation;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jakarta.annotation.PostConstruct;
import org.apache.hc.client5.http.ConnectTimeoutException;
import org.onap.aai.domain.notificationEvent.NotificationEvent;
import org.onap.aai.domain.notificationEvent.NotificationEvent.EventHeader;
import org.onap.aai.exceptions.AAIException;
import org.onap.aai.restclient.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
/**
* ValidationService routes all the writes to the database
* excluding deletes for now to the validation service to verify
* that the request is an valid one before committing to the database
*/
@Service
@Profile("pre-validation")
public class ValidationService {
static final String CONNECTION_REFUSED_STRING =
"Connection refused to the validation microservice due to service unreachable";
static final String CONNECTION_TIMEOUT_STRING = "Connection timeout to the validation microservice as this could "
+ "indicate the server is unable to reach port, "
+ "please check on server by running: nc -w10 -z -v ${VALIDATION_HOST} ${VALIDATION_PORT}";
static final String REQUEST_TIMEOUT_STRING =
"Request to validation service took longer than the currently set timeout";
static final String VALIDATION_ENDPOINT = "/v1/validate";
static final String VALIDATION_HEALTH_ENDPOINT = "/v1/info";
private static final Logger LOGGER = LoggerFactory.getLogger(ValidationService.class);
private static final String DELETE = "DELETE";
private final RestClient validationRestClient;
private final String appName;
private final Set validationNodeTypes;
private final ObjectMapper mapper;
private final List exclusionList;
public ValidationService(@Qualifier("validationRestClient") RestClient validationRestClient,
@Value("${spring.application.name}") String appName,
@Value("${validation.service.node-types}") String validationNodes,
@Value("${validation.service.exclusion-regexes:#{null}}") String exclusionRegexes,
ObjectMapper mapper) {
this.validationRestClient = validationRestClient;
this.appName = appName;
this.validationNodeTypes = Arrays.stream(validationNodes.split(",")).collect(Collectors.toSet());
if (exclusionRegexes == null || exclusionRegexes.isEmpty()) {
this.exclusionList = new ArrayList<>();
} else {
this.exclusionList =
Arrays.stream(exclusionRegexes.split(",")).map(Pattern::compile).collect(Collectors.toList());
}
this.mapper = mapper;
LOGGER.info("Successfully initialized the pre validation service");
}
@PostConstruct
public void initialize() throws AAIException {
doHealthCheckRequest();
}
private void doHealthCheckRequest() throws AAIException {
Map httpHeaders = new HashMap<>();
httpHeaders.put("X-FromAppId", appName);
httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
httpHeaders.put("Content-Type", "application/json");
ResponseEntity healthCheckResponse = null;
try {
healthCheckResponse =
validationRestClient.execute(VALIDATION_HEALTH_ENDPOINT, HttpMethod.GET, httpHeaders);
} catch (Exception ex) {
AAIException validationException = new AAIException("AAI_4021", ex);
throw validationException;
}
if (!isSuccess(healthCheckResponse)) {
throw new AAIException("AAI_4021");
}
LOGGER.info("Successfully connected to the validation service endpoint");
}
public boolean shouldValidate(String nodeType) {
return this.validationNodeTypes.contains(nodeType);
}
public void validate(List notificationEvents) throws AAIException {
if (notificationEvents == null || notificationEvents.isEmpty() || isSourceExcluded(notificationEvents)) {
return;
}
for (NotificationEvent event : notificationEvents) {
EventHeader eventHeader = event.getEventHeader();
if (eventHeader == null) {
// Should I skip processing the request and let it continue
// or fail the request and cause client impact
continue;
}
/*
* Skipping the delete events for now
* Note: Might revisit this later when validation supports DELETE events
*/
if (isDelete(eventHeader)) {
continue;
}
String entityType = eventHeader.getEntityType();
if (this.shouldValidate(entityType)) {
List violations = preValidate(event);
if (!violations.isEmpty()) {
AAIException aaiException = new AAIException("AAI_4019");
aaiException.getTemplateVars().addAll(violations);
throw aaiException;
}
}
}
}
/**
* Determine if event is of type delete
*/
private boolean isDelete(EventHeader eventHeader) {
String action = eventHeader.getAction();
return DELETE.equalsIgnoreCase(action);
}
/**
* Checks the `source` attribute of the first event to determine if validation should be skipped
* @param notificationEvents
* @return
*/
private boolean isSourceExcluded(List notificationEvents) {
// Get the first notification and if the source of that notification
// is in one of the regexes then we skip sending it to validation
NotificationEvent notification = notificationEvents.get(0);
EventHeader eventHeader = notification.getEventHeader();
if (eventHeader != null) {
String source = eventHeader.getSourceName();
return exclusionList.stream().anyMatch(pattern -> pattern.matcher(source).matches());
}
return false;
}
public List preValidate(NotificationEvent notificationEvent) throws AAIException {
Map httpHeaders = new HashMap<>();
httpHeaders.put("X-FromAppId", appName);
httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
httpHeaders.put("Content-Type", "application/json");
List violations = new ArrayList<>();
ResponseEntity responseEntity;
try {
String requestBody = mapper.writeValueAsString(notificationEvent);
responseEntity = validationRestClient.execute(VALIDATION_ENDPOINT, HttpMethod.POST, httpHeaders, requestBody);
Object responseBody = responseEntity.getBody();
if (isSuccess(responseEntity)) {
LOGGER.debug("Validation Service returned following response status code {} and body {}",
responseEntity.getStatusCodeValue(), responseEntity.getBody());
} else if (responseBody != null) {
Validation validation = getValidation(responseBody);
if (validation == null) {
LOGGER.debug("Validation Service following status code {} with body {}",
responseEntity.getStatusCodeValue(), responseEntity.getBody());
} else {
violations = extractViolations(validation);
}
} else {
LOGGER.warn("Unable to convert the response body null");
}
} catch (Exception e) {
// If the exception cause is client side timeout
// then proceed as if it passed validation
// resources microservice shouldn't be blocked because of validation service
// is taking too long or if the validation service is down
// Any other exception it should block the request from passing?
if (e.getCause() instanceof ConnectTimeoutException) {
LOGGER.error(CONNECTION_TIMEOUT_STRING, e.getCause());
} else if (e.getCause() instanceof SocketTimeoutException) {
LOGGER.error(REQUEST_TIMEOUT_STRING, e.getCause());
} else if (e.getCause() instanceof ConnectException) {
LOGGER.error(CONNECTION_REFUSED_STRING, e.getCause());
} else {
LOGGER.error("Unknown exception thrown please investigate", e.getCause());
}
}
return violations;
}
private Validation getValidation(Object responseBody) {
Validation validation = null;
try {
validation = mapper.readValue(responseBody.toString(), Validation.class);
} catch (JsonProcessingException jsonException) {
LOGGER.warn("Unable to convert the response body {}", jsonException.getMessage());
}
return validation;
}
boolean isSuccess(ResponseEntity responseEntity) {
return responseEntity != null && responseEntity.getStatusCode().is2xxSuccessful();
}
public List extractViolations(Validation validation) {
if (validation == null || validation.getViolations() == null) {
return Collections.emptyList();
}
return validation.getViolations().stream()
.map(Violation::getErrorMessage)
.peek(LOGGER::info)
.collect(Collectors.toList());
}
}