blob: f6ffb431c22825403c04ce5d2a90617d88e21e81 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.fineract.infrastructure.core.exception;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import com.google.gson.Gson;
import jakarta.persistence.PersistenceException;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.naming.AuthenticationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exceptionmapper.DefaultExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Provides an Error Handler method that returns an object of type {@link ErrorInfo} to the CommandStrategy which raised
* the exception. This class uses various subclasses of RuntimeException to check the kind of exception raised and
* provide appropriate status and error codes for each one of the raised exception.
*
* @author Rishabh Shukla
* @see org.apache.fineract.batch.command.CommandStrategy
*/
@Component
@Slf4j
@AllArgsConstructor
public final class ErrorHandler {
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private enum PessimisticLockingFailureCode {
ROLLBACK("40"), // Transaction rollback
DEADLOCK("60"), // Oracle: deadlock
HY00("HY", "Lock wait timeout exceeded"), // MySql deadlock HY00
;
private final String code;
private final String msg;
PessimisticLockingFailureCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
PessimisticLockingFailureCode(String code) {
this(code, null);
}
private static Throwable match(Throwable t) {
Throwable rootCause = ExceptionUtils.getRootCause(t);
return rootCause instanceof SQLException sqle && Arrays.stream(values()).anyMatch(e -> e.matches(sqle)) ? rootCause : null;
}
private boolean matches(SQLException ex) {
return code.equals(getSqlClassCode(ex)) && (msg == null || ex.getMessage().contains(msg));
}
@Nullable
private static String getSqlClassCode(SQLException ex) {
String sqlState = ex.getSQLState();
if (sqlState == null) {
SQLException nestedEx = ex.getNextException();
if (nestedEx != null) {
sqlState = nestedEx.getSQLState();
}
}
return sqlState != null && sqlState.length() > 2 ? sqlState.substring(0, 2) : sqlState;
}
}
@Autowired
private final ApplicationContext ctx;
@Autowired
private final DefaultExceptionMapper defaultExceptionMapper;
@NotNull
public <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
Class<?> clazz = exception.getClass();
do {
Set<String> exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz)));
Set<String> fineractErrorMappers = createSet(ctx.getBeanNamesForType(FineractExceptionMapper.class));
SetUtils.SetView<String> intersection = SetUtils.intersection(exceptionMappers, fineractErrorMappers);
if (!intersection.isEmpty()) {
// noinspection unchecked
return (ExceptionMapper<T>) ctx.getBean(intersection.iterator().next());
}
if (!exceptionMappers.isEmpty()) {
// noinspection unchecked
return (ExceptionMapper<T>) ctx.getBean(exceptionMappers.iterator().next());
}
clazz = clazz.getSuperclass();
} while (!clazz.equals(Exception.class));
// noinspection unchecked
return (ExceptionMapper<T>) defaultExceptionMapper;
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public ErrorInfo handle(@NotNull RuntimeException exception) {
ExceptionMapper<RuntimeException> exceptionMapper = findMostSpecificExceptionHandler(exception);
Response response = exceptionMapper.toResponse(exception);
MultivaluedMap<String, Object> headers = response.getHeaders();
Set<Header> batchHeaders = headers == null ? null
: headers.keySet().stream().map(e -> new Header(e, response.getHeaderString(e))).collect(Collectors.toSet());
Integer errorCode = exceptionMapper instanceof FineractExceptionMapper ? ((FineractExceptionMapper) exceptionMapper).errorCode()
: null;
Object msg = response.getEntity();
return new ErrorInfo(response.getStatus(), errorCode, msg instanceof String ? (String) msg : JSON_HELPER.toJson(msg), batchHeaders);
}
public static RuntimeException getMappable(@NotNull Throwable thr) {
return getMappable(thr, null, null, null);
}
public static RuntimeException getMappable(@NotNull Throwable thr, String msgCode, String defaultMsg) {
return getMappable(thr, msgCode, defaultMsg, null);
}
public static RuntimeException getMappable(@NotNull Throwable t, String msgCode, String defaultMsg, String param,
final Object... defaultMsgArgs) {
String msg = defaultMsg == null ? t.getMessage() : defaultMsg;
String codePfx = "error.msg" + (param == null ? "" : ("." + param));
Object[] args = defaultMsgArgs == null ? new Object[] { t } : defaultMsgArgs;
Throwable cause;
if ((cause = PessimisticLockingFailureCode.match(t)) != null) {
return new PessimisticLockingFailureException(msg, cause); // deadlock
}
if (t instanceof NestedRuntimeException nre) {
cause = nre.getMostSpecificCause();
msg = defaultMsg == null ? cause.getMessage() : defaultMsg;
if (nre instanceof NonTransientDataAccessException) {
msgCode = msgCode == null ? codePfx + ".data.integrity.issue" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, args);
} else if (cause instanceof OptimisticLockException) {
return (RuntimeException) cause;
}
}
if (t instanceof ValidationException) {
msgCode = msgCode == null ? codePfx + ".validation.error" : msgCode;
return new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(msgCode, msg, param, defaultMsgArgs)));
}
if (t instanceof jakarta.persistence.OptimisticLockException) {
return (RuntimeException) t;
}
if (t instanceof PersistenceException) {
msgCode = msgCode == null ? codePfx + ".persistence.error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, args);
}
if (t instanceof AuthenticationException) {
msgCode = msgCode == null ? codePfx + ".authentication.error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, args);
}
if (t instanceof ParseException) {
msgCode = msgCode == null ? codePfx + ".parse.error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, args);
}
if (t instanceof RuntimeException re) {
return re;
}
return new RuntimeException(msg, t);
}
private static <T> Set<T> createSet(T[] array) {
if (array == null) {
return Set.of();
} else {
return Set.of(array);
}
}
public static Throwable findMostSpecificException(Exception exception) {
Throwable mostSpecificException = exception;
while (mostSpecificException.getCause() != null) {
mostSpecificException = mostSpecificException.getCause();
}
return mostSpecificException;
}
}