aboutsummaryrefslogtreecommitdiffstats
path: root/aai-core/src/main/java/org/onap/aai/prevalidation/ValidationService.java
blob: c66511f0ed9ef1bad7f5cdbc51f5667ce4668fe0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/**
 * ============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.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Arrays;
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 javax.annotation.PostConstruct;

import org.apache.http.conn.ConnectTimeoutException;
import org.onap.aai.exceptions.AAIException;
import org.onap.aai.introspection.Introspector;
import org.onap.aai.rest.ueb.NotificationEvent;
import org.onap.aai.restclient.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * <b>ValidationService</b> 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 {

    /**
     * Error indicating that the service trying to connect is down
     */
    static final String CONNECTION_REFUSED_STRING =
            "Connection refused to the validation microservice due to service unreachable";

    /**
     * Error indicating that the server is unable to reach the port
     * Could be server related connectivity issue
     */
    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}";

    /**
     * Error indicating that the request exceeded the allowed time
     *
     * Note: This means that the service could be active its
     * just taking some time to process our request
     */
    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 String ENTITY_TYPE = "entity-type";
    private static final String ACTION = "action";
    private static final String SOURCE_NAME = "source-name";

    private static final String DELETE = "DELETE";

    private static final Logger LOGGER = LoggerFactory.getLogger(ValidationService.class);

    private final RestClient validationRestClient;

    private final String appName;

    private final Set<String> validationNodeTypes;

    private List<Pattern> exclusionList;

    private final Gson gson;

    @Autowired
    public ValidationService(@Qualifier("validationRestClient") RestClient validationRestClient,
            @Value("${spring.application.name}") String appName,
            @Value("${validation.service.node-types}") String validationNodes,
            @Value("${validation.service.exclusion-regexes}") String exclusionRegexes) {
        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.gson = new Gson();
        LOGGER.info("Successfully initialized the pre validation service");
    }

    @PostConstruct
    public void initialize() throws AAIException {

        Map<String, String> httpHeaders = new HashMap<>();

        httpHeaders.put("X-FromAppId", appName);
        httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
        httpHeaders.put("Content-Type", "application/json");

        ResponseEntity<String> healthCheckResponse = null;

        try {

            healthCheckResponse =
                    validationRestClient.execute(VALIDATION_HEALTH_ENDPOINT, HttpMethod.GET, httpHeaders, null);

        } 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<NotificationEvent> notificationEvents) throws AAIException {

        if (notificationEvents == null || notificationEvents.isEmpty()) {
            return;
        }

        {
            // 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);
            Introspector eventHeader = notification.getEventHeader();
            if (eventHeader != null) {
                String source = eventHeader.getValue(SOURCE_NAME);
                for (Pattern pattern : exclusionList) {
                    if (pattern.matcher(source).matches()) {
                        return;
                    }
                }
            }

        }

        for (NotificationEvent event : notificationEvents) {

            Introspector 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;
            }

            String entityType = eventHeader.getValue(ENTITY_TYPE);
            String action = eventHeader.getValue(ACTION);

            /**
             * Skipping the delete events for now
             * Note: Might revisit this later when validation supports DELETE events
             */
            if (DELETE.equalsIgnoreCase(action)) {
                continue;
            }

            if (this.shouldValidate(entityType)) {
                List<String> violations = this.preValidate(event.getNotificationEvent());
                if (!violations.isEmpty()) {
                    AAIException aaiException = new AAIException("AAI_4019");
                    aaiException.getTemplateVars().addAll(violations);
                    throw aaiException;
                }
            }
        }
    }

    List<String> preValidate(String body) throws AAIException {

        Map<String, String> httpHeaders = new HashMap<>();

        httpHeaders.put("X-FromAppId", appName);
        httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
        httpHeaders.put("Content-Type", "application/json");

        List<String> violations = new ArrayList<>();
        ResponseEntity responseEntity;
        try {

            responseEntity = validationRestClient.execute(VALIDATION_ENDPOINT, HttpMethod.POST, httpHeaders, body);

            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 = null;
                try {
                    validation = gson.fromJson(responseBody.toString(), Validation.class);
                } catch (JsonSyntaxException jsonException) {
                    LOGGER.warn("Unable to convert the response body {}", jsonException.getMessage());
                }

                if (validation == null) {
                    LOGGER.debug("Validation Service following status code {} with body {}",
                            responseEntity.getStatusCodeValue(), responseEntity.getBody());
                } else {
                    violations.addAll(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 SocketTimeoutException) {
                LOGGER.error(REQUEST_TIMEOUT_STRING, e.getCause());
            } else if (e.getCause() instanceof ConnectException) {
                LOGGER.error(CONNECTION_REFUSED_STRING, e.getCause());
            } else if (e.getCause() instanceof ConnectTimeoutException) {
                LOGGER.error(CONNECTION_TIMEOUT_STRING, e.getCause());
            } else {
                LOGGER.error("Unknown exception thrown please investigate", e.getCause());
            }
        }
        return violations;
    }

    boolean isSuccess(ResponseEntity responseEntity) {
        return responseEntity != null && responseEntity.getStatusCode().is2xxSuccessful();
    }

    List<String> extractViolations(Validation validation) {

        List<String> errorMessages = new ArrayList<>();

        if (validation == null) {
            return errorMessages;
        }

        List<Violation> violations = validation.getViolations();

        if (violations != null && !violations.isEmpty()) {
            for (Violation violation : validation.getViolations()) {
                LOGGER.info(violation.getErrorMessage());
                errorMessages.add(violation.getErrorMessage());
            }
        }

        return errorMessages;
    }
}