Refactor ErrorLogHelper
[aai/aai-common.git] / aai-els-onap-logging / src / main / java / org / onap / aai / logging / ErrorLogHelper.java
1 /**
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2023 Deutsche Telekom SA.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *    http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.onap.aai.logging;
23
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.stream.Collectors;
34 import java.util.Properties;
35 import java.util.Map.Entry;
36
37 import javax.ws.rs.core.MediaType;
38
39 import org.apache.commons.lang3.StringUtils;
40 import org.onap.aai.domain.errorResponse.ErrorMessage;
41 import org.onap.aai.domain.errorResponse.ExceptionType;
42 import org.onap.aai.domain.errorResponse.Fault;
43 import org.onap.aai.domain.errorResponse.Info;
44 import org.onap.aai.exceptions.AAIException;
45 import org.onap.aai.util.AAIConstants;
46 import org.onap.logging.filter.base.Constants;
47 import org.onap.logging.filter.base.MDCSetup;
48 import org.onap.logging.ref.slf4j.ONAPLogConstants;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.slf4j.MDC;
52
53 import com.fasterxml.jackson.core.JsonProcessingException;
54 import com.fasterxml.jackson.databind.ObjectMapper;
55 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
56
57 /**
58  *
59  * This classes loads the application error properties file
60  * and provides a method that returns an ErrorObject
61  *
62  */
63
64 public class ErrorLogHelper {
65     private static final Logger LOGGER = LoggerFactory.getLogger(ErrorLogHelper.class);
66     private static final HashMap<String, ErrorObject> ERROR_OBJECTS = new HashMap<String, ErrorObject>();
67     private static final ObjectMapper objectMapper = new ObjectMapper();
68     private static final XmlMapper xmlMapper = new XmlMapper();
69
70     static {
71         try {
72             loadProperties();
73         } catch (IOException e) {
74             throw new RuntimeException("Failed to load error.properties file", e);
75         } catch (ErrorObjectFormatException e) {
76             throw new RuntimeException("Failed to parse error.properties file", e);
77         }
78     }
79
80     /**
81      * Load properties.
82      * 
83      * @throws IOException the exception
84      * @throws ErrorObjectFormatException
85      */
86     public static void loadProperties() throws IOException, ErrorObjectFormatException {
87         final String filePath = AAIConstants.AAI_HOME_ETC_APP_PROPERTIES + "error.properties";
88         final InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("error.properties");
89         final Properties properties = new Properties();
90
91         try (final FileInputStream fileInputStream = new FileInputStream(filePath)) {
92             LOGGER.info("Found the error.properties in the following location: {}",
93                     AAIConstants.AAI_HOME_ETC_APP_PROPERTIES);
94             properties.load(fileInputStream);
95         } catch (Exception ex) {
96             LOGGER.info("Unable to find the error.properties from filesystem so using file in jar");
97             if (inputStream != null) {
98                 properties.load(inputStream);
99             } else {
100                 LOGGER.error("Expected to find the error.properties in the jar but unable to find it");
101             }
102         }
103
104         for (Entry<Object, Object> entry : properties.entrySet()) {
105             final String key = (String) entry.getKey();
106             final String value = (String) entry.getValue();
107             final String[] errorProperties = value.split(":");
108
109             if (errorProperties.length < 7)
110                 throw new ErrorObjectFormatException();
111
112             final ErrorObject errorObject = new ErrorObject();
113
114             errorObject.setDisposition(errorProperties[0].trim());
115             errorObject.setCategory(errorProperties[1].trim());
116             errorObject.setSeverity(errorProperties[2].trim());
117             errorObject.setErrorCode(errorProperties[3].trim());
118             errorObject.setHTTPResponseCode(errorProperties[4].trim());
119             errorObject.setRESTErrorCode(errorProperties[5].trim());
120             errorObject.setErrorText(errorProperties[6].trim());
121             if (errorProperties.length > 7) {
122                 errorObject.setAaiElsErrorCode(errorProperties[7].trim());
123             }
124
125             ERROR_OBJECTS.put(key, errorObject);
126         }
127     }
128
129     /**
130      * Logs a known A&AI exception (i.e. one that can be found in error.properties)
131      *
132      * @param code for the error in the error.properties file
133      * @throws IOException
134      * @throws ErrorObjectNotFoundException
135      */
136     public static ErrorObject getErrorObject(String code) {
137
138         if (code == null)
139             throw new IllegalArgumentException("Key cannot be null");
140
141         final ErrorObject errorObject = ERROR_OBJECTS.get(code);
142
143         if (errorObject == null) {
144             LOGGER.warn("Unknown AAIException with code=" + code + ".  Using default AAIException");
145             return ERROR_OBJECTS.get(AAIException.DEFAULT_EXCEPTION_CODE);
146         }
147
148         return errorObject;
149     }
150
151     /**
152      * Determines whether category is policy or not. If policy (1), this is a POL error, else it's a SVC error.
153      * The AAIRESTException may contain a different ErrorObject than that created with the REST error key.
154      * This allows lower level exception detail to be returned to the client to help troubleshoot the problem.
155      * If no error object is embedded in the AAIException, one will be created using the error object from the
156      * AAIException.
157      *
158      * @param aaiException must have a restError value whose numeric value must match what should be returned in the REST API
159      * @param variables optional list of variables to flesh out text in error string
160      * @return appropriately formatted JSON response per the REST API spec.
161      * @throws IOException
162      * @deprecated
163      */
164     public static String getRESTAPIErrorResponse(AAIException aaiException, ArrayList<String> variables) {
165         List<MediaType> acceptHeaders = Collections.singletonList(MediaType.APPLICATION_JSON_TYPE);
166
167         return getRESTAPIErrorResponse(acceptHeaders, aaiException, variables);
168     }
169
170     /**
171      * Determines whether category is policy or not. If policy (1), this is a POL error, else it's a SVC error.
172      * The AAIRESTException may contain a different ErrorObject than that created with the REST error key.
173      * This allows lower level exception detail to be returned to the client to help troubleshoot the problem.
174      * If no error object is embedded in the AAIException, one will be created using the error object from the
175      * AAIException.
176      * 
177      * @param aaiException
178      * @param variables
179      * @return
180      */
181     public static Fault getErrorResponse(AAIException aaiException,
182             ArrayList<String> variables) {
183         final ErrorObject restErrorObject = getRestErrorObject(aaiException);
184         final String text = createText(restErrorObject);
185         final int placeholderCount = StringUtils.countMatches(restErrorObject.getErrorText(), "%");
186         variables = checkAndEnrichVariables(aaiException, variables, placeholderCount);
187         
188         if (aaiException.getErrorObject().getCategory().equals("1")) {
189             return createPolicyFault(aaiException, text, variables);
190         } else {
191             return createServiceFault(aaiException, text, variables);
192         }
193     }
194
195     /**
196      *
197      * @param acceptHeaders the accept headers orig
198      * @param aaiException must have a restError value whose numeric value must match what should be returned in the REST API
199      * @param variables optional list of variables to flesh out text in error string
200      * @return appropriately formatted JSON response per the REST API spec.
201      * @deprecated in favor of {@link #getErrorResponse(AAIException, ArrayList)}
202      */
203     @Deprecated
204     public static String getRESTAPIErrorResponse(List<MediaType> acceptHeaders, AAIException aaiException,
205             ArrayList<String> variables) {
206
207         List<MediaType> validAcceptHeaders = new ArrayList<MediaType>();
208         // we might have an exception but no accept header, so we'll set default to JSON
209         boolean foundValidAcceptHeader = false;
210         for (MediaType mediaType : acceptHeaders) {
211             if (MediaType.APPLICATION_XML_TYPE.isCompatible(mediaType) || MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
212                 validAcceptHeaders.add(mediaType);
213                 foundValidAcceptHeader = true;
214             }
215         }
216         if (foundValidAcceptHeader == false) {
217             // override the exception, client needs to set an appropriate Accept header
218             aaiException = new AAIException("AAI_4014");
219             validAcceptHeaders.add(MediaType.APPLICATION_JSON_TYPE);
220         }
221
222         MediaType mediaType = validAcceptHeaders.stream()
223             .filter(MediaType.APPLICATION_JSON_TYPE::isCompatible)
224             .findAny()
225             .orElse(MediaType.APPLICATION_XML_TYPE);
226
227         Fault fault = getErrorResponse(aaiException, variables);
228         try {
229             return MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)
230                 ? objectMapper.writeValueAsString(fault)
231                 : xmlMapper.writeValueAsString(fault);
232         } catch (JsonProcessingException ex) {
233             LOGGER.error(
234                 "We were unable to create a rest exception to return on an API because of a parsing error "
235                                     + ex.getMessage());
236         }
237         return null;
238     }
239
240     private static String createText(ErrorObject restErrorObject) {
241         final StringBuilder text = new StringBuilder();
242         text.append(restErrorObject.getErrorText());
243
244         // We want to always append the (msg=%n) (ec=%n+1) to the text, but have to find value of n
245         // This assumes that the variables in the ArrayList, which might be more than are needed to flesh out the
246         // error, are ordered based on the error string.
247         int placeholderCount = StringUtils.countMatches(restErrorObject.getErrorText(), "%");
248         text.append(" (msg=%").append(placeholderCount + 1).append(") (ec=%").append(placeholderCount + 2).append(")");
249         return text.toString();
250     }
251
252     /**
253      * Gets the RESTAPI error response with logging.
254      *
255      * @param acceptHeaders the accept headers orig
256      * @param aaiException the are
257      * @param variables the variables
258      */
259     public static String getRESTAPIErrorResponseWithLogging(List<MediaType> acceptHeaders, AAIException aaiException,
260             ArrayList<String> variables) {
261         logException(aaiException);
262         return ErrorLogHelper.getRESTAPIErrorResponse(acceptHeaders, aaiException, variables);
263     }
264
265     /**
266      * Gets the RESTAPI info response.
267      *
268      * @param acceptHeaders this param is ignored
269      * @param aaiExceptionsMap the map of AAIException
270      * @return the RESTAPI info response
271      * @deprecated {@link ErrorLogHelper#}
272      * @deprecated in favor of {@link #getRestApiInfoResponse(HashMap)}
273      */
274     @Deprecated
275     public static Object getRESTAPIInfoResponse(ArrayList<MediaType> acceptHeaders,
276             Map<AAIException, ArrayList<String>> aaiExceptionsMap) {
277         return (Object) getRestApiInfoResponse(aaiExceptionsMap);
278     }
279
280     /**
281      * Gets the RESTAPI info response.
282      *
283      * @param acceptHeaders the accept headers
284      * @param aaiExceptionsMap the are list
285      * @return the RESTAPI info response
286      */
287     public static Info getRestApiInfoResponse(
288             Map<AAIException, ArrayList<String>> aaiExceptionsMap) {
289         List<ErrorMessage> errorMessages = aaiExceptionsMap.entrySet().stream()
290                 .map(entry -> createResponseMessage(entry))
291                 .filter(Objects::nonNull)
292                 .collect(Collectors.toList());
293         return new Info(errorMessages);
294     }
295
296     private static ErrorMessage createResponseMessage(Entry<AAIException, ArrayList<String>> entry) {
297         AAIException aaiException = entry.getKey();
298         ArrayList<String> variables = entry.getValue();
299
300         ErrorObject restErrorObject = getRestErrorObject(aaiException);
301         final String text = createText(restErrorObject);
302         final int placeholderCount = StringUtils.countMatches(aaiException.getErrorObject().getErrorText(), "%");
303         variables = checkAndEnrichVariables(aaiException, variables, placeholderCount);
304
305         try {
306             return ErrorMessage.builder()
307                 .messageId("INF" + aaiException.getErrorObject().getRESTErrorCode())
308                 .text(text)
309                 .variables(variables)
310                 .build();
311         } catch (Exception ex) {
312             LOGGER.error("We were unable to create a rest exception to return on an API because of a parsing error "
313                     + ex.getMessage());
314             return null;
315         }
316     }
317
318     /**
319      * Determines whether category is policy or not. If policy (1), this is a POL error, else it's a SVC error.
320      * The AAIRESTException may contain a different ErrorObject than that created with the REST error key.
321      * This allows lower level exception detail to be returned to the client to help troubleshoot the problem.
322      * If no error object is embedded in the AAIException, one will be created using the error object from the
323      * AAIException.
324      *
325      * @param aaiException must have a restError value whose numeric value must match what should be returned in the REST API
326      * @param variables optional list of variables to flesh out text in error string
327      * @return appropriately formatted JSON response per the REST API spec.
328      */
329     public static String getRESTAPIPolicyErrorResponseXML(AAIException aaiException, ArrayList<String> variables) {
330         return getRESTAPIErrorResponse(Collections.singletonList(MediaType.APPLICATION_XML_TYPE), aaiException, variables);
331     }
332
333     public static void logException(AAIException aaiException) {
334         final ErrorObject errorObject = aaiException.getErrorObject();
335         /*
336          * String severityCode = errorObject.getSeverityCode(errorObject.getSeverity());
337          * 
338          * Severify should be left empty per Logging Specification 2019.11
339          * if (!StringUtils.isEmpty(severityCode)) {
340          * int sevCode = Integer.parseInt(severityCode);
341          * if (sevCode > 0 && sevCode <= 3) {
342          * LoggingContext.severity(sevCode);
343          * }
344          * }
345          */
346         String stackTrace = "";
347         try {
348             stackTrace = LogFormatTools.getStackTop(aaiException);
349         } catch (Exception a) {
350             // ignore
351         }
352         final String errorMessage = new StringBuilder().append(errorObject.getErrorText()).append(":")
353                 .append(errorObject.getRESTErrorCode()).append(":").append(errorObject.getHTTPResponseCode())
354                 .append(":").append(aaiException.getMessage()).toString().replaceAll("\\n", "^");
355
356         MDCSetup mdcSetup = new MDCSetup();
357         mdcSetup.setResponseStatusCode(errorObject.getHTTPResponseCode().getStatusCode());
358         mdcSetup.setErrorCode(Integer.parseInt(errorObject.getAaiElsErrorCode()));
359         String serviceName = MDC.get(ONAPLogConstants.MDCs.SERVICE_NAME);
360         if (serviceName == null || serviceName.isEmpty()) {
361             MDC.put(ONAPLogConstants.MDCs.SERVICE_NAME, Constants.DefaultValues.UNKNOWN);
362         }
363         MDC.put(ONAPLogConstants.MDCs.ERROR_DESC, errorMessage);
364         final String details =
365                 new StringBuilder().append(errorObject.getErrorCodeString()).append(" ").append(stackTrace).toString();
366
367         if (errorObject.getSeverity().equalsIgnoreCase("WARN"))
368             LOGGER.warn(details);
369         else if (errorObject.getSeverity().equalsIgnoreCase("ERROR"))
370             LOGGER.error(details);
371         else if (errorObject.getSeverity().equalsIgnoreCase("FATAL"))
372             LOGGER.error(details);
373         else if (errorObject.getSeverity().equals("INFO"))
374             LOGGER.info(details);
375     }
376
377     public static void logError(String code) {
378         logError(code, "");
379     }
380
381     public static void logError(String code, String message) {
382         logException(new AAIException(code, message));
383     }
384
385     private static ErrorObject getRestErrorObject(AAIException aaiException) {
386         final int restErrorCode = Integer.parseInt(aaiException.getErrorObject().getRESTErrorCode());
387         return ErrorLogHelper.getErrorObject("AAI_" + restErrorCode);
388     }
389
390     public static Fault createPolicyFault(AAIException aaiException, String text, List<String> variables) {
391         return createFault(aaiException, text, variables, ExceptionType.POLICY);
392     }
393     public static Fault createServiceFault(AAIException aaiException, String text, List<String> variables) {
394         return createFault(aaiException, text, variables, ExceptionType.SERVICE);
395     }
396     private static Fault createFault(AAIException aaiException, String text, List<String> variables, ExceptionType exceptionType) {
397         String typePrefix = ExceptionType.POLICY.equals(exceptionType) ? "POL" : "SVC";
398         Map<ExceptionType, ErrorMessage> requestError = Collections.singletonMap(exceptionType, 
399             ErrorMessage.builder()
400                 .messageId(typePrefix+ aaiException.getErrorObject().getRESTErrorCode())
401                 .text(text)
402                 .variables(variables)
403                 .build());
404         return new Fault(requestError);
405     }
406
407     private static ArrayList<String> checkAndEnrichVariables(AAIException aaiException, ArrayList<String> variables, int placeholderCount) {
408         final ErrorObject errorObject = aaiException.getErrorObject();
409         if (variables == null) {
410             variables = new ArrayList<String>();
411         }
412
413         // int placeholderCount = StringUtils.countMatches(errorObject.getErrorText(), "%");
414         if (variables.size() < placeholderCount) {
415             ErrorLogHelper.logError("AAI_4011", "data missing for rest error");
416             while (variables.size() < placeholderCount) {
417                 variables.add("null");
418             }
419         }
420
421         // This will put the error code and error text into the right positions
422         if (aaiException.getMessage() == null || aaiException.getMessage().length() == 0) {
423             variables.add(placeholderCount++, errorObject.getErrorText());
424         } else {
425             variables.add(placeholderCount++, errorObject.getErrorText() + ":" + aaiException.getMessage());
426         }
427         variables.add(placeholderCount, errorObject.getErrorCodeString());
428         return variables;
429     }
430 }