All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gorm.tools.problem.ProblemHandler.groovy Maven / Gradle / Ivy

The newest version!
/*
* Copyright 2021 Yak.Works - Licensed under the Apache License, Version 2.0 (the "License")
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
package gorm.tools.problem

import groovy.json.JsonException
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import org.codehaus.groovy.runtime.StackTraceUtils
import org.hibernate.QueryTimeoutException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.MessageSourceResolvable
import org.springframework.dao.DataAccessException
import org.springframework.dao.DataAccessResourceFailureException
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.validation.Errors
import org.springframework.validation.ObjectError

import gorm.tools.repository.errors.EmptyErrors
import yakworks.api.ApiStatus
import yakworks.api.HttpStatus
import yakworks.api.problem.GenericProblem
import yakworks.api.problem.Problem
import yakworks.api.problem.ThrowableProblem
import yakworks.api.problem.UnexpectedProblem
import yakworks.api.problem.data.DataProblem
import yakworks.api.problem.data.DataProblemCodes
import yakworks.i18n.icu.ICUMessageSource
import yakworks.message.MsgServiceRegistry

/**
 * Service to prepare ApiError / ApiValidationError for given a given exception
 *
 * @author Joshua Burnett (@basejump)
 * @since 7.0.8
 */
@Slf4j
@CompileStatic
class ProblemHandler {

    @Autowired ICUMessageSource messageSource

    static {
        //setup default class filtering for making stack trace less noisy
        stackTraceUtilsDefaultFilters()
    }

    GenericProblem handleException(Class entityClass, Throwable e) {
        handleException(e, entityClass.simpleName)
    }

    /**
     * Prepares Problem for given entity and exception
     * - Problem(status:422) for ValidationException
     * - Problem(status:400) for DataAccessException
     * - Problem(status:404) for NotFoundException
     * - Problem(status:500) for other exceptions
     *
     * @param simpleName used for validation conversion
     * @param Exception e
     * @return ApiError
     */
    GenericProblem handleException(Throwable e, String simpleName = null) {
        // default error status code is 422
        ApiStatus status400 = HttpStatus.BAD_REQUEST
        ApiStatus status404 = HttpStatus.NOT_FOUND
        ApiStatus status422 = HttpStatus.UNPROCESSABLE_ENTITY

        if (e instanceof ValidationProblem.Exception) {
            def valProblem = e.getValidationProblem()
            if (valProblem.errors instanceof EmptyErrors) {
                //this is some other exception wrapped in validation exception
                valProblem.detail(e.cause?.message)
            }
            //translate the errors
            if(!valProblem.violations && valProblem.errors?.hasErrors()){
                //we do this late, not done when created with RepoExceptionSupport
                valProblem.violations(ValidationProblem.transateErrorsToViolations(valProblem.errors))
            }
            return valProblem
        }
        else if (e instanceof ThrowableProblem) {
            return (GenericProblem) e.problem
        }
        else if (e instanceof GenericProblem) {
            return (GenericProblem) e
        }
        else if (e instanceof grails.validation.ValidationException
            || e instanceof org.grails.datastore.mapping.validation.ValidationException) {
            return buildFromErrorException(e, simpleName)
        } else if (e instanceof IllegalArgumentException) {
            //We use this all over to double as a validation error, Validate.notNull for example.
            return Problem.of('error.illegalArgument').status(status400).detail(e.message)
        }
        else if(isQueryTimeout(e)) {
            return DataProblem.of("error.query.timeout")
        }
        else if (e instanceof DataAccessException) {
            return buildFromDataAccessException(e)
        }
        else if (e instanceof HttpMessageNotReadableException || e instanceof JsonException) {
            //this happens if request contains bad data / malformed json. we dont want to log stacktraces for these as they are expected
            return DataProblem.of(e)
        }
        else if(e instanceof AssertionError) {
            return DataProblem.of(e)
        }
        else {
            return handleUnexpected(e)
        }
    }

    GenericProblem handleUnexpected(Throwable e){
        log.error("UNEXPECTED Internal Server Error\n${e.message}", deepSanitize(e))
        if (e instanceof GenericProblem) {
            return (GenericProblem) e
        }
        else if (e instanceof ThrowableProblem) {
            return (GenericProblem) e.problem
        }
        else if (e instanceof NullPointerException) {
            //deal with the dreaded null pointer
            //Check if there's stacktrace, in certain cases stacktrace is coming up empty, which is causing Arrayoutofbound ex - see #2712
            String stackLine1 = e.stackTrace ? "at ${e.stackTrace[0].toString()}" : ""
            return new UnexpectedProblem().cause(e).detail("NullPointerException $stackLine1")
        }
        else {
            return new UnexpectedProblem().cause(e).detail(e.message)
        }
    }

    //XXX for OptimisticLockingFailureException there are times when its valid I think
    // but then times when its our processes (autocash for example). How do we parse that out?
    // OptimisticLockingFailureException is a DataAccessException so it hits the else below
    // and we always log it out as error.
    static DataProblem buildFromDataAccessException(DataAccessException e) {
        // Root of the hierarchy of data access exceptions
        if(isUniqueIndexViolation((DataAccessException) e)){
            return DataProblemCodes.UniqueConstraint.of(e)
        }
        else if(isForeignKeyViolation((DataAccessException) e)){
            return DataProblemCodes.ReferenceKey.of(e)
        }
        else {
            //For now turn to warn in case we want to turn it off.
            String rootMessage = e.rootCause?.getMessage()
            String msgInfo = "===  message: ${e.message} \n === rootMessage: ${rootMessage} "

            log.error("MAYBE UNEXPECTED? Data Access Exception ${msgInfo}", deepSanitize(e))
            return DataProblem.of(e)
        }
    }

    ValidationProblem buildFromErrorException(Throwable valEx, String entityName = null) {
        Errors ers = valEx['errors'] as Errors
        def valProb = ValidationProblem.of(valEx).errors(ers)
        if(entityName) valProb.name(entityName)
        return valProb.violations(ValidationProblem.transateErrorsToViolations(ers))
    }

    static String getMsg(MessageSourceResolvable msr) {
        //FIXME this should be generalized somehwere?
        try {
            //cast so we can use the getMessage(MessageSourceResolvable resolvable), which works
            ICUMessageSource msgService = MsgServiceRegistry.service as ICUMessageSource//get static msgService that should have been set in icu4j plugin
            return msgService.getMessage(msr)
        }
        catch (e) {
            return msr.defaultMessage
        }
    }

    /**
     * returns true if the exception is a psql query timeout exception
     */
    static boolean isQueryTimeout(Throwable ex) {
        //Criteria/Mango throws QueryTimeoutException, jdbcTemplate throws DataAccessResourceFailureException
        return (ex instanceof QueryTimeoutException) ||
            (ex instanceof DataAccessResourceFailureException && ex.message.contains('canceling statement due to statement timeout"'))
    }

    //Unique index unique constraint or primary key violation
    @SuppressWarnings('BracesForIfElse')
    static String isUniqueIndexViolation(DataAccessException dax) {
        if(!dax.rootCause) return null
        String rootMessage = dax.rootCause.message
        if (rootMessage.contains("Unique index or primary key violation") || //mysql and H2
            rootMessage.contains("Duplicate entry") || //mysql
            rootMessage.contains("Violation of UNIQUE KEY constraint") || //sql server
            rootMessage.contains("unique constraint")) {
            return rootMessage
        } else {
            return null
        }
    }

    static String isForeignKeyViolation(DataAccessException dax) {
        if (!dax.rootCause || !(dax instanceof DataIntegrityViolationException)) return null
        String rootMessage = dax.rootCause.message.toLowerCase()
        //postgres and H2 - if its DataIntegrityViolationException and contains keyword 'foreign key' thn its fk violation
        if (rootMessage.contains("foreign key")) {
            return rootMessage
        } else {
            return null
        }
    }

    /**
     * Broken pipe exception happens when client has closed the socket and server tries to write/send any response byte on the output stream.
     * Server Can write nothing to output stream once we encounter Broken pipe exception
     */
    static boolean isBrokenPipe(Throwable ex) {
        return ex.message && ex.message.toLowerCase().contains("broken pipe")
    }

    //Legacy from ValidationException
    static String formatErrors(Errors errors, String msg) {
        String ls = System.getProperty("line.separator");
        StringBuilder b = new StringBuilder();
        if (msg != null) {
            b.append(msg).append(" : ") //.append(ls);
        }

        for (ObjectError error : errors.getAllErrors()) {
            b.append(ls)
                .append(" - ")
                .append(error)
                .append(ls);
        }
        return b.toString();
    }

    public static Throwable deepSanitize(Throwable t) {
        StackTraceUtils.deepSanitize(t)
    }

    //setup default class filtering for making stack trace less noisy
    @SuppressWarnings(['BooleanMethodReturnsNull'])
    static void stackTraceUtilsDefaultFilters(){
        StackTraceUtils.addClassTest { String className ->
            for (String groovyPackage : (NOISY_PACKAGES + NOISY_TEST_PACKAGES)) {
                if (className.startsWith(groovyPackage)) {
                    return false
                }
            }
            return null
        }
    }

    //the list of packages to summarize up so logging trace is not so noisy. only logs one line if multiples start with these
    public static List NOISY_PACKAGES = [
        'jdk.internal.reflect.NativeMethodAccessorImpl',
        'jdk.internal.reflect.DelegatingMethodAccessorImpl',
        'jdk.internal.reflect.GeneratedMethodAccessor',
        'org.springframework.web.filter.OncePerRequestFilter',
        'org.springframework.web.filter.CharacterEncodingFilter',
        'org.springframework.web.filter.DelegatingFilterProxy',
        'org.springframework.security.web',
        'org.grails.core.DefaultGrailsControllerClass',
        'org.grails.web.servlet.mvc.GrailsWebRequestFilter',
        'org.grails.web.filters.HiddenHttpMethodFilter',
        'org.grails.datastore.mapping.reflect.FieldEntityAccess',
        'org.apache.catalina.core',
        'org.apache.tomcat.websocket.server.WsFilter',
        'org.apache.tomcat.util.net',
        'org.apache.tomcat.util.threads',
        'org.apache.coyote'
    ];

    public static List NOISY_TEST_PACKAGES = [
        'jdk.internal.reflect.NativeConstructorAccessorImpl',
        'org.spockframework.runtime',
        'org.spockframework.util.ReflectionUtil',
        'org.spockframework.junit4.ExceptionAdapterInterceptor',
        'org.junit.platform.engine.support.hierarchical',
        'org.junit.platform.launcher.core.EngineExecutionOrchestrator',
        //'org.gradle',

    ];

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy