├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src └── main │ ├── java │ └── com │ │ └── ksoot │ │ └── problem │ │ ├── core │ │ ├── AbstractThrowableProblem.java │ │ ├── ApplicationException.java │ │ ├── ApplicationProblem.java │ │ ├── ContentNegotiation.java │ │ ├── DefaultProblem.java │ │ ├── ErrorResponseBuilder.java │ │ ├── ErrorType.java │ │ ├── Exceptional.java │ │ ├── FallbackContentNegotiationStrategy.java │ │ ├── GeneralErrorKey.java │ │ ├── Lists.java │ │ ├── MediaTypes.java │ │ ├── MultiProblem.java │ │ ├── Problem.java │ │ ├── ProblemConstant.java │ │ ├── ProblemSupport.java │ │ ├── ProblemUtils.java │ │ ├── Problems.java │ │ ├── StackTraceProcessor.java │ │ └── ThrowableProblem.java │ │ ├── jackson │ │ ├── AbstractThrowableProblemMixIn.java │ │ ├── ExceptionalMixin.java │ │ ├── HttpMethodDeserializer.java │ │ ├── HttpMethodSerializer.java │ │ ├── HttpStatusDeserializer.java │ │ ├── HttpStatusSerializer.java │ │ ├── ProblemMixIn.java │ │ └── ProblemModule.java │ │ └── spring │ │ ├── advice │ │ ├── AdviceTrait.java │ │ ├── application │ │ │ ├── ApplicationAdviceTraits.java │ │ │ ├── ApplicationExceptionAdviceTrait.java │ │ │ ├── ApplicationMultiProblemAdviceTrait.java │ │ │ └── ApplicationProblemAdviceTrait.java │ │ ├── dao │ │ │ ├── AbstractDaoExceptionHandler.java │ │ │ ├── BaseDataIntegrityAdvice.java │ │ │ ├── ConstraintNameResolver.java │ │ │ ├── DBType.java │ │ │ ├── DaoAdviceTraits.java │ │ │ ├── DataIntegrityViolationAdviceTrait.java │ │ │ ├── Database.java │ │ │ ├── DuplicateKeyExceptionAdviceTrait.java │ │ │ ├── MongoConstraintNameResolver.java │ │ │ ├── MysqlConstraintNameResolver.java │ │ │ ├── OracleConstraintNameResolver.java │ │ │ ├── PostgresConstraintNameResolver.java │ │ │ └── SQLServerConstraintNameResolver.java │ │ ├── general │ │ │ ├── GeneralAdviceTraits.java │ │ │ ├── ProblemAdviceTrait.java │ │ │ ├── ThrowableAdviceTrait.java │ │ │ └── UnsupportedOperationAdviceTrait.java │ │ ├── http │ │ │ ├── BaseNotAcceptableAdviceTrait.java │ │ │ ├── HttpAdviceTraits.java │ │ │ ├── HttpMediaTypeNotAcceptableAdviceTrait.java │ │ │ ├── HttpMediaTypeNotSupportedExceptionAdviceTrait.java │ │ │ ├── HttpRequestMethodNotSupportedAdviceTrait.java │ │ │ ├── MethodNotAllowedAdviceTrait.java │ │ │ ├── NotAcceptableStatusAdviceTrait.java │ │ │ ├── ResponseStatusAdviceTrait.java │ │ │ └── UnsupportedMediaTypeStatusAdviceTrait.java │ │ ├── io │ │ │ ├── DataBufferLimitExceptionAdviceTrait.java │ │ │ ├── IOAdviceTraits.java │ │ │ ├── MaxUploadSizeExceededExceptionAdviceTrait.java │ │ │ ├── MessageNotReadableAdviceTrait.java │ │ │ └── MultipartAdviceTrait.java │ │ ├── network │ │ │ ├── CircuitBreakerOpenAdviceTrait.java │ │ │ └── NetworkAdviceTraits.java │ │ ├── routing │ │ │ ├── MissingRequestHeaderAdviceTrait.java │ │ │ ├── MissingServletRequestParameterAdviceTrait.java │ │ │ ├── MissingServletRequestPartAdviceTrait.java │ │ │ ├── NoHandlerFoundAdviceTrait.java │ │ │ ├── RoutingAdviceTraits.java │ │ │ └── ServletRequestBindingAdviceTrait.java │ │ ├── security │ │ │ ├── AccessDeniedAdviceTrait.java │ │ │ ├── AuthenticationAdviceTrait.java │ │ │ ├── InsufficientAuthenticationAdviceTrait.java │ │ │ ├── ProblemAccessDeniedHandler.java │ │ │ ├── ProblemAuthenticationEntryPoint.java │ │ │ ├── ProblemServerAccessDeniedHandler.java │ │ │ ├── ProblemServerAuthenticationEntryPoint.java │ │ │ └── SecurityAdviceTraits.java │ │ ├── validation │ │ │ ├── BaseBindingResultHandlingAdviceTrait.java │ │ │ ├── BaseValidationAdviceTrait.java │ │ │ ├── BindAdviceTrait.java │ │ │ ├── ConstraintViolationAdviceTrait.java │ │ │ ├── MethodArgumentNotValidAdviceTrait.java │ │ │ ├── MethodArgumentTypeMismatchAdviceTrait.java │ │ │ ├── OpenApiValidationAdviceTrait.java │ │ │ ├── TypeMismatchAdviceTrait.java │ │ │ ├── ValidationAdviceTraits.java │ │ │ └── ViolationVM.java │ │ ├── web │ │ │ └── ProblemHandlingWeb.java │ │ └── webflux │ │ │ ├── ProblemHandlingWebflux.java │ │ │ ├── SpringWebfluxProblemResponseUtils.java │ │ │ └── WebExchangeBindAdviceTrait.java │ │ ├── boot │ │ └── autoconfigure │ │ │ ├── DaoAdviceEnabled.java │ │ │ ├── ORMAdviceEnabled.java │ │ │ ├── ORMUrlAvailable.java │ │ │ ├── OpenAPIValidationAdviceEnabled.java │ │ │ ├── OpenApiConfigsEnabled.java │ │ │ ├── ProblemDaoConfiguration.java │ │ │ ├── ProblemJacksonConfiguration.java │ │ │ ├── ProblemJacksonEnabled.java │ │ │ ├── SecurityAdviceEnabled.java │ │ │ ├── web │ │ │ ├── OpenApiValidationExceptionHandler.java │ │ │ ├── PathConfigurableOpenApiValidationFilter.java │ │ │ ├── ProblemWebAutoConfiguration.java │ │ │ ├── SpringWebErrorResponseBuilder.java │ │ │ ├── WebDaoExceptionHandler.java │ │ │ ├── WebExceptionHandler.java │ │ │ └── WebSecurityExceptionHandler.java │ │ │ └── webflux │ │ │ ├── ProblemWebfluxAutoConfiguration.java │ │ │ ├── SpringWebfluxErrorResponseBuilder.java │ │ │ ├── WebFluxDaoExceptionHandler.java │ │ │ ├── WebFluxExceptionHandler.java │ │ │ └── WebFluxSecurityExceptionHandler.java │ │ └── config │ │ ├── ProblemBeanRegistry.java │ │ ├── ProblemConfigException.java │ │ ├── ProblemMessageProvider.java │ │ ├── ProblemMessageProviderConfig.java │ │ ├── ProblemMessageSourceResolver.java │ │ └── ProblemProperties.java │ └── resources │ ├── META-INF │ ├── spring-configuration-metadata.json │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── i18n │ └── problems.properties └── tooling ├── checkstyle-suppressions.xml ├── checkstyle.xml └── eclipse-code-formatter.xml /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | **/*.log 3 | *.log 4 | 5 | ### Maven ### 6 | target/ 7 | bin/ 8 | !.mvn/wrapper/maven-wrapper.jar 9 | !**/src/main/**/target/ 10 | !**/src/test/**/target/ 11 | 12 | ### Gradle ### 13 | .gradle 14 | build/ 15 | !gradle/wrapper/gradle-wrapper.jar 16 | !**/src/main/**/build/ 17 | !**/src/test/**/build/ 18 | 19 | ### STS ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### IntelliJ IDEA ### 32 | **/.DS_Store 33 | .DS_Store 34 | .idea 35 | *.iws 36 | *.iml 37 | *.ipr 38 | out/ 39 | !**/src/main/**/out/ 40 | !**/src/test/**/out/ 41 | 42 | ### NetBeans ### 43 | /nbproject/private/ 44 | /nbbuild/ 45 | /dist/ 46 | /nbdist/ 47 | /.nb-gradle/ 48 | 49 | ### VS Code ### 50 | .vscode/ 51 | /settings.xml 52 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2023 Ksoot Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/AbstractThrowableProblem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import jakarta.annotation.Nullable; 4 | import java.util.Collections; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | public abstract class AbstractThrowableProblem extends ThrowableProblem { 10 | 11 | private static final long serialVersionUID = 7657146691407810390L; 12 | 13 | private final String code; 14 | private final String title; 15 | private final String detail; 16 | private final Map parameters; 17 | 18 | protected AbstractThrowableProblem() { 19 | this(null, null); 20 | } 21 | 22 | protected AbstractThrowableProblem(final String code, final String title) { 23 | this(code, title, null); 24 | } 25 | 26 | protected AbstractThrowableProblem(final String code, final String title, final String detail) { 27 | this(code, title, detail, null); 28 | } 29 | 30 | protected AbstractThrowableProblem( 31 | final String code, 32 | final String title, 33 | final String detail, 34 | @Nullable final ThrowableProblem cause) { 35 | this(code, title, detail, cause, null); 36 | } 37 | 38 | protected AbstractThrowableProblem( 39 | final String code, 40 | final String title, 41 | final String detail, 42 | @Nullable final ThrowableProblem cause, 43 | @Nullable final Map parameters) { 44 | super(cause); 45 | this.code = code; 46 | this.title = title; 47 | this.detail = detail; 48 | this.parameters = Optional.ofNullable(parameters).orElseGet(LinkedHashMap::new); 49 | } 50 | 51 | @Override 52 | public String getCode() { 53 | return this.code; 54 | } 55 | 56 | @Override 57 | public String getTitle() { 58 | return this.title; 59 | } 60 | 61 | @Override 62 | public String getDetail() { 63 | return this.detail; 64 | } 65 | 66 | @Override 67 | public Map getParameters() { 68 | return Collections.unmodifiableMap(this.parameters); 69 | } 70 | 71 | /** 72 | * This is required to workaround missing support for {@link 73 | * com.fasterxml.jackson.annotation.JsonAnySetter} on constructors annotated with {@link 74 | * com.fasterxml.jackson.annotation.JsonCreator}. 75 | * 76 | * @param key the custom key 77 | * @param value the custom value 78 | * @see Jackson Issue 562 79 | */ 80 | void set(final String key, final Object value) { 81 | this.parameters.put(key, value); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ApplicationException.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import jakarta.annotation.Nullable; 4 | import java.util.Map; 5 | import lombok.Getter; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.util.Assert; 8 | 9 | @Getter 10 | @SuppressWarnings("serial") 11 | public final class ApplicationException extends Exception implements ProblemSupport { 12 | 13 | private final HttpStatus status; 14 | private final String errorKey; 15 | private final String defaultDetail; 16 | private final Object[] detailArgs; 17 | 18 | private final ThrowableProblem cause; 19 | private final Map parameters; 20 | 21 | private final Problem problem; 22 | 23 | private ApplicationException( 24 | final String message, 25 | final HttpStatus status, 26 | final Problem problem, 27 | final String errorKey, 28 | @Nullable final String defaultDetail, 29 | @Nullable final Object[] detailArgs, 30 | @Nullable final ThrowableProblem cause, 31 | @Nullable final Map parameters) { 32 | super(message, cause); 33 | this.status = status; 34 | this.errorKey = errorKey; 35 | this.problem = problem; 36 | this.defaultDetail = defaultDetail; 37 | this.detailArgs = detailArgs; 38 | this.cause = cause; 39 | this.parameters = parameters; 40 | } 41 | 42 | public static ApplicationException of( 43 | final HttpStatus status, 44 | final String errorKey, 45 | @Nullable final String defaultDetail, 46 | @Nullable final Object[] detailArgs, 47 | @Nullable final ThrowableProblem cause, 48 | @Nullable final Map parameters) { 49 | Assert.hasText(errorKey, "'errorKey' must not be null or empty"); 50 | return new ApplicationException( 51 | ProblemUtils.toMessage(errorKey, defaultDetail, null, cause), 52 | status, 53 | null, 54 | errorKey, 55 | defaultDetail, 56 | detailArgs, 57 | cause, 58 | parameters); 59 | } 60 | 61 | public static ApplicationException of(final HttpStatus status, final Problem problem) { 62 | Assert.notNull(problem, "'problem' must not be null"); 63 | return new ApplicationException( 64 | ProblemUtils.toMessage(null, null, problem, null), 65 | status, 66 | problem, 67 | null, 68 | null, 69 | null, 70 | null, 71 | null); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ApplicationProblem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import jakarta.annotation.Nullable; 4 | import java.util.Map; 5 | import lombok.Getter; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.util.Assert; 8 | 9 | @Getter 10 | @SuppressWarnings("serial") 11 | public final class ApplicationProblem extends RuntimeException implements ProblemSupport { 12 | 13 | private final HttpStatus status; 14 | private final String errorKey; 15 | private final String defaultDetail; 16 | private final Object[] detailArgs; 17 | 18 | private final ThrowableProblem cause; 19 | private final Map parameters; 20 | 21 | private final Problem problem; 22 | 23 | private ApplicationProblem( 24 | final String message, 25 | final HttpStatus status, 26 | final Problem problem, 27 | final String errorKey, 28 | @Nullable final String defaultDetail, 29 | @Nullable final Object[] detailArgs, 30 | @Nullable final ThrowableProblem cause, 31 | @Nullable final Map parameters) { 32 | super(message, cause); 33 | Assert.notNull(status, "'status' must not be null"); 34 | this.status = status; 35 | this.errorKey = errorKey; 36 | this.problem = problem; 37 | this.defaultDetail = defaultDetail; 38 | this.detailArgs = detailArgs; 39 | this.cause = cause; 40 | this.parameters = parameters; 41 | } 42 | 43 | public static ApplicationProblem of( 44 | final HttpStatus status, 45 | final String errorKey, 46 | @Nullable final String defaultDetail, 47 | @Nullable final Object[] detailArgs, 48 | @Nullable final ThrowableProblem cause, 49 | @Nullable final Map parameters) { 50 | Assert.hasText(errorKey, "'errorKey' must not be null or empty"); 51 | return new ApplicationProblem( 52 | ProblemUtils.toMessage(errorKey, defaultDetail, null, cause), 53 | status, 54 | null, 55 | errorKey, 56 | defaultDetail, 57 | detailArgs, 58 | cause, 59 | parameters); 60 | } 61 | 62 | public static ApplicationProblem of(final HttpStatus status, final String errorKey) { 63 | Assert.hasText(errorKey, "'errorKey' must not be null or empty"); 64 | return new ApplicationProblem( 65 | ProblemUtils.toMessage(errorKey, null, null, null), 66 | status, 67 | null, 68 | errorKey, 69 | null, 70 | null, 71 | null, 72 | null); 73 | } 74 | 75 | public static ApplicationProblem of(final HttpStatus status, final Problem problem) { 76 | Assert.notNull(problem, "'problem' must not be null"); 77 | return new ApplicationProblem( 78 | ProblemUtils.toMessage(null, null, problem, null), 79 | status, 80 | problem, 81 | null, 82 | problem.getDetail(), 83 | null, 84 | null, 85 | null); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ContentNegotiation.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import org.springframework.web.accept.ContentNegotiationStrategy; 4 | import org.springframework.web.accept.HeaderContentNegotiationStrategy; 5 | 6 | class ContentNegotiation { 7 | 8 | static final ContentNegotiationStrategy DEFAULT = 9 | new FallbackContentNegotiationStrategy(new HeaderContentNegotiationStrategy()); 10 | 11 | private ContentNegotiation() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/DefaultProblem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import jakarta.annotation.Nullable; 4 | import java.util.Map; 5 | 6 | public final class DefaultProblem extends AbstractThrowableProblem { 7 | 8 | private static final long serialVersionUID = -6866968751952328910L; 9 | 10 | // TODO needed for jackson 11 | DefaultProblem( 12 | final String code, 13 | final String title, 14 | final String detail, 15 | @Nullable final ThrowableProblem cause) { 16 | super(code, title, detail, cause); 17 | } 18 | 19 | DefaultProblem( 20 | final String code, 21 | final String title, 22 | final String detail, 23 | @Nullable final ThrowableProblem cause, 24 | @Nullable final Map parameters) { 25 | super(code, title, detail, cause, parameters); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ErrorResponseBuilder.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CAUSE_KEY; 4 | import static com.ksoot.problem.core.ProblemConstant.CODE_KEY; 5 | import static com.ksoot.problem.core.ProblemConstant.METHOD_KEY; 6 | import static com.ksoot.problem.core.ProblemConstant.TIMESTAMP_KEY; 7 | import static org.springframework.http.MediaType.APPLICATION_JSON; 8 | 9 | import com.ksoot.problem.spring.config.ProblemBeanRegistry; 10 | import java.net.URI; 11 | import java.time.OffsetDateTime; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | import org.apache.commons.collections4.MapUtils; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.springframework.http.HttpHeaders; 18 | import org.springframework.http.HttpMethod; 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.http.ProblemDetail; 22 | import org.springframework.web.accept.ContentNegotiationStrategy; 23 | import org.springframework.web.accept.HeaderContentNegotiationStrategy; 24 | 25 | public interface ErrorResponseBuilder { 26 | 27 | ContentNegotiationStrategy DEFAULT_CONTENT_NEGOTIATION_STRATEGY = 28 | new FallbackContentNegotiationStrategy(new HeaderContentNegotiationStrategy()); 29 | 30 | static Optional getProblemMediaType(final List mediaTypes) { 31 | for (final MediaType mediaType : mediaTypes) { 32 | if (mediaType.includes(APPLICATION_JSON) || mediaType.includes(MediaTypes.PROBLEM)) { 33 | return Optional.of(MediaTypes.PROBLEM); 34 | } else if (mediaType.includes(MediaTypes.X_PROBLEM)) { 35 | return Optional.of(MediaTypes.X_PROBLEM); 36 | } 37 | } 38 | 39 | return Optional.empty(); 40 | } 41 | 42 | R buildResponse( 43 | final Throwable throwable, 44 | final T request, 45 | final HttpStatus status, 46 | final HttpHeaders headers, 47 | final Problem problem); 48 | 49 | default ProblemDetail createProblemDetail( 50 | final T request, final HttpStatus status, final Problem problem) { 51 | ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, problem.getDetail()); 52 | problemDetail.setTitle(problem.getTitle()); 53 | problemDetail.setInstance(requestUri(request)); 54 | if (StringUtils.isNotBlank(ProblemBeanRegistry.problemProperties().getTypeUrl())) { 55 | URI type = 56 | URI.create( 57 | ProblemBeanRegistry.problemProperties().getTypeUrl() + "#" + problem.getCode()); 58 | problemDetail.setType(type); 59 | } 60 | problemDetail.setProperty(METHOD_KEY, requestMethod(request)); 61 | problemDetail.setProperty(TIMESTAMP_KEY, OffsetDateTime.now()); 62 | problemDetail.setProperty(CODE_KEY, problem.getCode()); 63 | 64 | if (MapUtils.isNotEmpty(problem.getParameters())) { 65 | problem.getParameters().forEach((k, v) -> problemDetail.setProperty(k, v)); 66 | } 67 | if (Objects.nonNull(problem.getCause())) { 68 | problemDetail.setProperty(CAUSE_KEY, problem.getCause()); 69 | } 70 | return problemDetail; 71 | } 72 | 73 | default URI requestUri(final T request) { 74 | throw new UnsupportedOperationException( 75 | "Method need to be implemented in implementation class"); 76 | } 77 | 78 | default HttpMethod requestMethod(final T request) { 79 | throw new UnsupportedOperationException( 80 | "Method need to be implemented in implementation class"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ErrorType.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public interface ErrorType { 6 | 7 | String getErrorKey(); 8 | 9 | String getDefaultDetail(); 10 | 11 | HttpStatus getStatus(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/Exceptional.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | /** 4 | * An extension of the {@link Problem} interface for problems that extend {@link Exception}. Since 5 | * {@link Exception} is a concrete type any class can only extend one exception type. {@link 6 | * ThrowableProblem} is one choice, but we don't want to force people to extend from this but choose 7 | * their own super class. For this they can implement this interface and get the same handling as 8 | * {@link ThrowableProblem} for free. A common use case would be: 9 | * 10 | *
{@code
11 |  * public final class OutOfStockException extends BusinessException implements Exceptional
12 |  * }
13 | * 14 | * @see Exception 15 | * @see Problem 16 | * @see ThrowableProblem 17 | */ 18 | public interface Exceptional extends Problem { 19 | 20 | ThrowableProblem getCause(); 21 | 22 | default Exception propagate() throws Exception { 23 | throw propagateAs(Exception.class); 24 | } 25 | 26 | default X propagateAs(final Class type) throws X { 27 | throw type.cast(this); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/FallbackContentNegotiationStrategy.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import javax.annotation.Nonnull; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 8 | import org.springframework.web.accept.ContentNegotiationStrategy; 9 | import org.springframework.web.context.request.NativeWebRequest; 10 | 11 | /** 12 | * Mimics the new default behavior as of Spring 5 that {@link 13 | * ContentNegotiationStrategy#resolveMediaTypes(NativeWebRequest)} returns {@link MediaType#ALL} as 14 | * a fallback compared to an empty list. 15 | */ 16 | final class FallbackContentNegotiationStrategy implements ContentNegotiationStrategy { 17 | 18 | private final List all = Collections.singletonList(MediaType.ALL); 19 | private final ContentNegotiationStrategy delegate; 20 | 21 | FallbackContentNegotiationStrategy(final ContentNegotiationStrategy delegate) { 22 | this.delegate = delegate; 23 | } 24 | 25 | @Override 26 | @Nonnull 27 | public List resolveMediaTypes(final NativeWebRequest request) 28 | throws HttpMediaTypeNotAcceptableException { 29 | final List mediaTypes = delegate.resolveMediaTypes(request); 30 | 31 | if (mediaTypes.isEmpty()) { 32 | return all; 33 | } 34 | 35 | return mediaTypes; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/GeneralErrorKey.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | public class GeneralErrorKey { 4 | 5 | public static final String INTERNAL_SERVER_ERROR = "internal.server.error"; 6 | 7 | public static final String DATA_INTEGRITY_VIOLATION = "data.integrity.violation"; 8 | 9 | public static final String CONSTRAINT_VIOLATION = "constraint.violation"; 10 | 11 | public static final String MISSING_SERVLET_REQUEST_PARAMETER = "missing.request.parameter"; 12 | 13 | public static final String MISSING_SERVLET_REQUEST_PART = "missing.request.part"; 14 | 15 | public static final String NO_HANDLER_FOUND = "no.handler.found"; 16 | 17 | public static final String MISSING_REQUEST_HEADER = "missing.request.header"; 18 | 19 | public static final String OPEN_API_VIOLATION = "openapi.violation"; 20 | 21 | public static final String TYPE_MISMATCH = "type.mismatch"; 22 | 23 | public static final String INVALID_FORMAT = "invalid.format"; 24 | 25 | public static final String NOT_FOUND = "not.found"; 26 | 27 | public static final String REQUEST_METHOD_NOT_SUPPORTED = "request.method.not.supported"; 28 | 29 | public static final String METHOD_NOT_ALLOWED = "method.not.allowed"; 30 | 31 | public static final String MEDIA_TYPE_NOT_ACCEPTABLE = "media.type.not.acceptable"; 32 | 33 | public static final String MEDIA_TYPE_NOT_SUPPORTED = "media.type.not.supported"; 34 | 35 | public static final String SECURITY_UNAUTHORIZED = "security.unauthorized"; 36 | 37 | public static final String SECURITY_ACCESS_DENIED = "security.access.denied"; 38 | 39 | public static final String MULTIPLE_ERRORS = "multiple.errors"; 40 | 41 | private GeneralErrorKey() { 42 | throw new IllegalStateException("Just a constants container, not supposed to be instantiated"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/Lists.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import java.util.List; 4 | 5 | final class Lists { 6 | 7 | private Lists() {} 8 | 9 | /** 10 | * Returns the length of the longest trailing partial sublist of the target list within the 11 | * specified source list, or 0 if there is no such occurrence. More formally, returns the length 12 | * i such that {@code source.subList(source.size() - i, 13 | * source.size()).equals(target.subList(target.size() - i, target.size()))}, or 0 if there is no 14 | * such index. 15 | * 16 | * @param source the list in which to search for the longest trailing partial sublist of 17 | * target. 18 | * @param target the list to search for as a trailing partial sublist of source. 19 | * @return the length of the last occurrence of trailing partial sublist the specified target list 20 | * within the specified source list, or 0 if there is no such occurrence. 21 | * @since 1.4 22 | */ 23 | static int lengthOfTrailingPartialSubList(final List source, final List target) { 24 | final int s = source.size() - 1; 25 | final int t = target.size() - 1; 26 | int l = 0; 27 | 28 | while (l <= s && l <= t && source.get(s - l).equals(target.get(t - l))) { 29 | l++; 30 | } 31 | 32 | return l; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/MediaTypes.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import org.springframework.http.MediaType; 4 | 5 | public final class MediaTypes { 6 | 7 | public static final String PROBLEM_VALUE = "application/problem+json"; 8 | public static final MediaType PROBLEM = MediaType.parseMediaType(PROBLEM_VALUE); 9 | 10 | public static final String X_PROBLEM_VALUE = "application/x.problem+json"; 11 | public static final MediaType X_PROBLEM = MediaType.parseMediaType(X_PROBLEM_VALUE); 12 | 13 | @Deprecated static final String WILDCARD_JSON_VALUE = "application/*+json"; 14 | 15 | @Deprecated static final MediaType WILDCARD_JSON = MediaType.parseMediaType(WILDCARD_JSON_VALUE); 16 | 17 | private MediaTypes() {} 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/MultiProblem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import lombok.Getter; 7 | import org.apache.commons.collections4.CollectionUtils; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.util.Assert; 10 | 11 | @Getter 12 | @SuppressWarnings("serial") 13 | public class MultiProblem extends RuntimeException { 14 | 15 | private final HttpStatus status; 16 | 17 | private final List errors; 18 | 19 | private MultiProblem(final HttpStatus status, final List problems) { 20 | super(status.getReasonPhrase()); 21 | Assert.isTrue(CollectionUtils.isNotEmpty(problems), "'problems' must not be null or empty"); 22 | Assert.noNullElements(problems, "'problems' must not contain null"); 23 | this.status = status; 24 | this.errors = new ArrayList<>(problems); 25 | } 26 | 27 | private MultiProblem(final List exceptions, final HttpStatus status) { 28 | super(status.getReasonPhrase()); 29 | Assert.isTrue(CollectionUtils.isNotEmpty(exceptions), "'exceptions' must not be null or empty"); 30 | Assert.noNullElements(exceptions, "'exceptions' must not contain null"); 31 | this.status = status; 32 | this.errors = new ArrayList<>(exceptions); 33 | } 34 | 35 | public static MultiProblem ofExceptions( 36 | final HttpStatus status, final List exceptions) { 37 | Assert.notNull(status, "'status' must not be null"); 38 | return new MultiProblem(exceptions, status); 39 | } 40 | 41 | public static MultiProblem ofExceptions(final List exceptions) { 42 | return ofExceptions(HttpStatus.MULTI_STATUS, exceptions); 43 | } 44 | 45 | public static MultiProblem ofProblems(final HttpStatus status, final List problems) { 46 | Assert.notNull(status, "'status' must not be null"); 47 | return new MultiProblem(status, problems); 48 | } 49 | 50 | public static MultiProblem ofProblems(final List problems) { 51 | return ofProblems(HttpStatus.MULTI_STATUS, problems); 52 | } 53 | 54 | public MultiProblem add(final Throwable exception) { 55 | Assert.notNull(exception, "'exception' must not be null"); 56 | this.errors.add(exception); 57 | return this; 58 | } 59 | 60 | public MultiProblem add(final Problem problem) { 61 | Assert.notNull(problem, "'problem' must not be null"); 62 | this.errors.add(problem); 63 | return this; 64 | } 65 | 66 | public List getErrors() { 67 | return Collections.unmodifiableList(this.errors); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/Problem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import jakarta.annotation.Nullable; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.Set; 13 | import java.util.stream.Stream; 14 | import org.apache.commons.collections4.MapUtils; 15 | import org.springframework.util.Assert; 16 | 17 | /** 18 | * {@link Problem} instances are required to be immutable. 19 | * 20 | * @see RFC 7807: Problem Details for HTTP APIs 21 | */ 22 | public interface Problem { 23 | 24 | String getCode(); 25 | 26 | /** 27 | * A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to 28 | * occurrence of the problem, except for purposes of localisation. 29 | * 30 | * @return a short, human-readable summary of this problem 31 | */ 32 | String getTitle(); 33 | 34 | /** 35 | * A human readable explanation specific to this occurrence of the problem. 36 | * 37 | * @return A human readable explanation of this problem 38 | */ 39 | String getDetail(); 40 | 41 | ThrowableProblem getCause(); 42 | 43 | /** 44 | * Optional, additional attributes of the problem. Implementations can choose to ignore this in 45 | * favor of concrete, typed fields. 46 | * 47 | * @return additional parameters 48 | */ 49 | default Map getParameters() { 50 | return Collections.emptyMap(); 51 | } 52 | 53 | static String toString(final Problem problem) { 54 | final Stream parts = 55 | Stream.concat( 56 | Stream.of(problem.getCode(), problem.getTitle(), problem.getDetail()), 57 | problem.getParameters().entrySet().stream().map(Map.Entry::toString)) 58 | .filter(Objects::nonNull); 59 | 60 | return problem.getCode() + "{" + parts.collect(joining(", ")) + "}"; 61 | } 62 | 63 | // ------------- Builder --------------- 64 | interface DetailBuilder { 65 | CauseBuilder detail(final String detail); 66 | } 67 | 68 | interface CauseBuilder extends ParameterBuilder { 69 | ParameterBuilder cause(@Nullable Throwable cause); 70 | } 71 | 72 | interface ParameterBuilder extends ParametersBuilder { 73 | ParameterBuilder parameter(final String key, final Object value); 74 | } 75 | 76 | interface ParametersBuilder extends org.apache.commons.lang3.builder.Builder { 77 | org.apache.commons.lang3.builder.Builder parameters( 78 | @Nullable final Map parameters); 79 | } 80 | 81 | class ProblemBuilder implements DetailBuilder, CauseBuilder { 82 | 83 | private static final Set RESERVED_PROPERTIES = 84 | new HashSet<>(Arrays.asList("code", "title", "detail", "cause")); 85 | private String code; 86 | private String title; 87 | private String detail; 88 | private ThrowableProblem cause; 89 | private final Map parameters = new LinkedHashMap<>(); 90 | 91 | ProblemBuilder(final String code, final String title) { 92 | this(code, title, null); 93 | } 94 | 95 | ProblemBuilder(final String code, final String title, final String detail) { 96 | Assert.hasText(code, "'code' must not be null or empty"); 97 | Assert.hasText(title, "'title' must not be null or empty"); 98 | this.code = code; 99 | this.title = title; 100 | this.detail = detail; 101 | } 102 | 103 | @Override 104 | public CauseBuilder detail(final String detail) { 105 | this.detail = detail; 106 | return this; 107 | } 108 | 109 | @Override 110 | public ParameterBuilder cause(@Nullable final Throwable cause) { 111 | this.cause = Objects.nonNull(cause) ? ProblemUtils.toProblem(cause) : null; 112 | return this; 113 | } 114 | 115 | @Override 116 | public ParameterBuilder parameter(final String key, final Object value) { 117 | Assert.hasText(key, "'key' must not be null or empty"); 118 | Assert.isTrue(!RESERVED_PROPERTIES.contains(key), "Property " + key + " is reserved"); 119 | this.parameters.put(key, value); 120 | return this; 121 | } 122 | 123 | @Override 124 | public org.apache.commons.lang3.builder.Builder parameters( 125 | @Nullable final Map parameters) { 126 | if (MapUtils.isNotEmpty(parameters)) { 127 | parameters.entrySet().stream() 128 | .forEach(entry -> parameter(entry.getKey(), entry.getValue())); 129 | } 130 | return this; 131 | } 132 | 133 | @Override 134 | public ThrowableProblem build() { 135 | return new DefaultProblem(this.code, this.title, this.detail, this.cause, this.parameters); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ProblemConstant.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | public class ProblemConstant { 4 | 5 | public static final String CODE_KEY = "code"; 6 | public static final String METHOD_KEY = "method"; 7 | public static final String TIMESTAMP_KEY = "timestamp"; 8 | public static final String STACKTRACE_KEY = "statcktrace"; 9 | public static final String CAUSE_KEY = "cause"; 10 | public static final String ERRORS_KEY = "errors"; 11 | public static final String VIOLATIONS_KEY = "violations"; 12 | public static final String DOT = "."; 13 | 14 | public static final String CODE_CODE_PREFIX = "code."; 15 | public static final String TITLE_CODE_PREFIX = "title."; 16 | public static final String DETAIL_CODE_PREFIX = "detail."; 17 | public static final String STATUS_CODE_PREFIX = "status."; 18 | 19 | public static final String CODE_RESOLVER = "codeResolver"; 20 | public static final String TITLE_RESOLVER = "titleResolver"; 21 | public static final String DETAIL_RESOLVER = "detailResolver"; 22 | 23 | public static final String STATUS_RESOLVER = "statusResolver"; 24 | 25 | public static final String PROPERTY_PATH_KEY = "propertyPath"; 26 | 27 | public static final String CONSTRAINT_VIOLATION_CODE_CODE_PREFIX = 28 | CODE_CODE_PREFIX + GeneralErrorKey.CONSTRAINT_VIOLATION; 29 | public static final String CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX = 30 | TITLE_CODE_PREFIX + GeneralErrorKey.CONSTRAINT_VIOLATION; 31 | public static final String CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX = 32 | DETAIL_CODE_PREFIX + GeneralErrorKey.CONSTRAINT_VIOLATION; 33 | 34 | public static final String CONSTRAINT_VIOLATION_DEFAULT_MESSAGE = "Constraints violated"; 35 | public static final String DB_CONSTRAINT_VIOLATION_DEFAULT_MESSAGE = 36 | "Database constraints violated"; 37 | 38 | private ProblemConstant() { 39 | throw new IllegalStateException("Just a constants container, not supposed to be instantiated"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ProblemSupport.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import java.util.Map; 4 | import org.springframework.http.HttpStatus; 5 | 6 | public interface ProblemSupport { 7 | 8 | HttpStatus getStatus(); 9 | 10 | String getErrorKey(); 11 | 12 | Problem getProblem(); 13 | 14 | String getDefaultDetail(); 15 | 16 | Object[] getDetailArgs(); 17 | 18 | ThrowableProblem getCause(); 19 | 20 | Map getParameters(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ProblemUtils.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import static java.util.Arrays.asList; 4 | import static java.util.stream.Collectors.joining; 5 | import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; 6 | 7 | import com.ksoot.problem.spring.config.ProblemBeanRegistry; 8 | import jakarta.annotation.Nullable; 9 | import java.text.CharacterIterator; 10 | import java.text.StringCharacterIterator; 11 | import java.util.Objects; 12 | import java.util.Optional; 13 | import java.util.stream.Stream; 14 | import lombok.experimental.UtilityClass; 15 | import org.apache.commons.lang3.exception.ExceptionUtils; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.HttpStatusCode; 18 | import org.springframework.web.bind.annotation.ResponseStatus; 19 | 20 | /** 21 | * @author Rajveer Singh 22 | */ 23 | @UtilityClass 24 | public class ProblemUtils { 25 | 26 | public static String statusCode(final HttpStatusCode httpStatus) { 27 | return String.valueOf(httpStatus.value()); 28 | } 29 | 30 | public static ThrowableProblem toProblem(final Throwable throwable) { 31 | final HttpStatus status = resolveStatus(throwable); 32 | ThrowableProblem problem = Problems.newInstance(status).detail(throwable.getMessage()).build(); 33 | problem.setStackTrace(createStackTrace(throwable)); 34 | return problem; 35 | } 36 | 37 | public static HttpStatus resolveStatus(final Throwable throwable) { 38 | return Optional.ofNullable(ProblemUtils.resolveResponseStatus(throwable)) 39 | .map(ResponseStatus::value) 40 | .orElse(HttpStatus.INTERNAL_SERVER_ERROR); 41 | } 42 | 43 | public static ResponseStatus resolveResponseStatus(final Throwable type) { 44 | @Nullable 45 | final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class); 46 | return candidate == null && type.getCause() != null 47 | ? resolveResponseStatus(type.getCause()) 48 | : candidate; 49 | } 50 | 51 | public static StackTraceElement[] createStackTrace(final Throwable throwable) { 52 | final Throwable cause = throwable.getCause(); 53 | if (cause == null || !ProblemBeanRegistry.problemProperties().isCauseChainsEnabled()) { 54 | return throwable.getStackTrace(); 55 | } else { 56 | 57 | final StackTraceElement[] next = cause.getStackTrace(); 58 | final StackTraceElement[] current = throwable.getStackTrace(); 59 | 60 | final int length = 61 | current.length - Lists.lengthOfTrailingPartialSubList(asList(next), asList(current)); 62 | final StackTraceElement[] stackTrace = new StackTraceElement[length]; 63 | System.arraycopy(current, 0, stackTrace, 0, length); 64 | return stackTrace; 65 | } 66 | } 67 | 68 | public static String getStackTrace(final Throwable exception) { 69 | String stacktrace = ExceptionUtils.getStackTrace(exception); 70 | StringBuilder escapedStacktrace = new StringBuilder(stacktrace.length() + 100); 71 | StringCharacterIterator scitr = new StringCharacterIterator(stacktrace); 72 | 73 | char current = scitr.first(); 74 | // DONE = \\uffff (not a character) 75 | String lastAppend = null; 76 | while (current != CharacterIterator.DONE) { 77 | if (current == '\t' || current == '\r' || current == '\n') { 78 | if (!" ".equals(lastAppend)) { 79 | escapedStacktrace.append(" "); 80 | lastAppend = " "; 81 | } 82 | } else { 83 | // nothing matched - just text as it is. 84 | escapedStacktrace.append(current); 85 | lastAppend = "" + current; 86 | } 87 | current = scitr.next(); 88 | } 89 | return escapedStacktrace.toString(); 90 | } 91 | 92 | public static String toMessage( 93 | @Nullable final String errorKey, 94 | @Nullable final String defaultDetail, 95 | @Nullable final Problem problem, 96 | @Nullable final Problem cause) { 97 | final Stream parts = 98 | Stream.of( 99 | errorKey, 100 | defaultDetail, 101 | Objects.nonNull(problem) ? Problem.toString(problem) : null, 102 | Objects.nonNull(cause) ? Problem.toString(cause) : null) 103 | .filter(Objects::nonNull); 104 | 105 | return parts.collect(joining(", ")); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/StackTraceProcessor.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import static java.util.ServiceLoader.load; 4 | import static java.util.stream.StreamSupport.stream; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * @see java.util.ServiceLoader 10 | */ 11 | public interface StackTraceProcessor { 12 | 13 | StackTraceProcessor DEFAULT = elements -> elements; 14 | StackTraceProcessor COMPOUND = 15 | stream(load(StackTraceProcessor.class).spliterator(), false) 16 | .reduce(DEFAULT, (first, second) -> elements -> second.process(first.process(elements))); 17 | 18 | Collection process(final Collection elements); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/core/ThrowableProblem.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.core; 2 | 3 | import static java.util.Arrays.asList; 4 | import static java.util.stream.Collectors.joining; 5 | 6 | import jakarta.annotation.Nullable; 7 | import java.util.Collection; 8 | import java.util.Objects; 9 | import java.util.stream.Stream; 10 | 11 | /** {@link Problem} instances are required to be immutable. */ 12 | public abstract class ThrowableProblem extends RuntimeException implements Problem, Exceptional { 13 | 14 | private static final long serialVersionUID = 2893667887362253159L; 15 | 16 | protected ThrowableProblem() { 17 | this(null); 18 | } 19 | 20 | protected ThrowableProblem(@Nullable final ThrowableProblem cause) { 21 | super(cause); 22 | final Collection stackTrace = 23 | StackTraceProcessor.COMPOUND.process(asList(getStackTrace())); 24 | setStackTrace(stackTrace.toArray(new StackTraceElement[0])); 25 | } 26 | 27 | @Override 28 | public String getDetail() { 29 | return Stream.of(getCode(), getTitle()).filter(Objects::nonNull).collect(joining(": ")); 30 | } 31 | 32 | @Override 33 | public ThrowableProblem getCause() { 34 | // cast is safe, since the only way to set this is our constructor 35 | return (ThrowableProblem) super.getCause(); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return Problem.toString(this); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/AbstractThrowableProblemMixIn.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnySetter; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.ksoot.problem.core.AbstractThrowableProblem; 7 | import com.ksoot.problem.core.ThrowableProblem; 8 | 9 | abstract class AbstractThrowableProblemMixIn { 10 | 11 | @SuppressWarnings("serial") 12 | @JsonCreator 13 | AbstractThrowableProblemMixIn( 14 | @JsonProperty("code") final String code, 15 | @JsonProperty("title") final String title, 16 | @JsonProperty("detail") final String detail, 17 | @JsonProperty("cause") final ThrowableProblem cause) { 18 | // this is just here to see whether "our" constructor matches the real one 19 | throw new AbstractThrowableProblem(code, title, detail, cause) {}; 20 | } 21 | 22 | @JsonAnySetter 23 | abstract void set(final String key, final Object value); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/ExceptionalMixin.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | interface ExceptionalMixin { 8 | 9 | @JsonIgnore 10 | String getLocalizedMessage(); 11 | 12 | @JsonIgnore 13 | String getMessage(); 14 | 15 | @JsonIgnore 16 | StackTraceElement[] getStackTrace(); 17 | 18 | @JsonIgnore 19 | Throwable[] getSuppressed(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/HttpMethodDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import java.io.IOException; 7 | import org.springframework.http.HttpMethod; 8 | 9 | final class HttpMethodDeserializer extends JsonDeserializer { 10 | 11 | HttpMethodDeserializer() {} 12 | 13 | @Override 14 | public HttpMethod deserialize(final JsonParser json, final DeserializationContext context) 15 | throws IOException { 16 | final String method = json.getValueAsString(); 17 | return HttpMethod.valueOf(method); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/HttpMethodSerializer.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.JsonSerializer; 5 | import com.fasterxml.jackson.databind.SerializerProvider; 6 | import java.io.IOException; 7 | import org.springframework.http.HttpMethod; 8 | 9 | final class HttpMethodSerializer extends JsonSerializer { 10 | 11 | @Override 12 | public void serialize( 13 | final HttpMethod method, final JsonGenerator json, final SerializerProvider serializers) 14 | throws IOException { 15 | json.writeString(method.name()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/HttpStatusDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import java.io.IOException; 7 | import java.util.Map; 8 | import org.springframework.http.HttpStatusCode; 9 | 10 | final class HttpStatusDeserializer extends JsonDeserializer { 11 | 12 | private final Map index; 13 | 14 | HttpStatusDeserializer(final Map index) { 15 | this.index = index; 16 | } 17 | 18 | @Override 19 | public HttpStatusCode deserialize(final JsonParser json, final DeserializationContext context) 20 | throws IOException { 21 | final int statusCode = json.getIntValue(); 22 | final HttpStatusCode status = index.get(statusCode); 23 | // return status == null ? new DefaultHttpStatusCode(statusCode) : status; 24 | return status; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/HttpStatusSerializer.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.JsonSerializer; 5 | import com.fasterxml.jackson.databind.SerializerProvider; 6 | import java.io.IOException; 7 | import org.springframework.http.HttpStatusCode; 8 | 9 | final class HttpStatusSerializer extends JsonSerializer { 10 | 11 | @Override 12 | public void serialize( 13 | final HttpStatusCode status, final JsonGenerator json, final SerializerProvider serializers) 14 | throws IOException { 15 | json.writeNumber(status.value()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/ProblemMixIn.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; 4 | 5 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 9 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 10 | import com.ksoot.problem.core.DefaultProblem; 11 | import com.ksoot.problem.core.Problem; 12 | import java.util.Map; 13 | 14 | @JsonTypeInfo( 15 | use = JsonTypeInfo.Id.NAME, 16 | include = JsonTypeInfo.As.EXISTING_PROPERTY, 17 | property = "type", 18 | defaultImpl = DefaultProblem.class, 19 | visible = true) 20 | @JsonInclude(NON_EMPTY) 21 | @JsonPropertyOrder({"code", "title", "details"}) 22 | interface ProblemMixIn extends Problem { 23 | 24 | @JsonProperty("code") 25 | @Override 26 | String getCode(); 27 | 28 | @JsonProperty("title") 29 | @Override 30 | String getTitle(); 31 | 32 | @JsonProperty("detail") 33 | @Override 34 | String getDetail(); 35 | 36 | @JsonAnyGetter 37 | @Override 38 | Map getParameters(); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/jackson/ProblemModule.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.jackson; 2 | 3 | import com.fasterxml.jackson.core.Version; 4 | import com.fasterxml.jackson.core.util.VersionUtil; 5 | import com.fasterxml.jackson.databind.Module; 6 | import com.fasterxml.jackson.databind.module.SimpleModule; 7 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 8 | import com.ksoot.problem.core.DefaultProblem; 9 | import com.ksoot.problem.core.Exceptional; 10 | import com.ksoot.problem.core.Problem; 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import org.springframework.http.HttpMethod; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.HttpStatusCode; 17 | 18 | public final class ProblemModule extends Module { 19 | 20 | private final Map statuses; 21 | 22 | /** 23 | * TODO document 24 | * 25 | * @see HttpStatus 26 | */ 27 | public ProblemModule() { 28 | this(HttpStatus.class); 29 | } 30 | 31 | /** 32 | * TODO document 33 | * 34 | * @param generic enum type 35 | * @param types status type enums 36 | * @throws IllegalArgumentException if there are duplicate status codes across all status types 37 | */ 38 | @SafeVarargs 39 | public & HttpStatusCode> ProblemModule(final Class... types) 40 | throws IllegalArgumentException { 41 | 42 | this(buildIndex(types)); 43 | } 44 | 45 | private ProblemModule(final Map statuses) { 46 | this.statuses = statuses; 47 | } 48 | 49 | @SafeVarargs 50 | private static & HttpStatusCode> Map buildIndex( 51 | final Class... types) { 52 | final Map index = new HashMap<>(); 53 | 54 | for (final Class type : types) { 55 | for (final E status : type.getEnumConstants()) { 56 | // Skip depricated status "Checkpoint" 57 | if (((HttpStatus) status).getReasonPhrase().equalsIgnoreCase("Checkpoint")) { 58 | continue; 59 | } 60 | index.put(status.value(), status); 61 | } 62 | } 63 | 64 | return Collections.unmodifiableMap(index); 65 | } 66 | 67 | @Override 68 | public String getModuleName() { 69 | return ProblemModule.class.getSimpleName(); 70 | } 71 | 72 | @Override 73 | public Version version() { 74 | return VersionUtil.versionFor(ProblemModule.class); 75 | } 76 | 77 | private Class mixinClass() { 78 | return ExceptionalMixin.class; 79 | } 80 | 81 | @Override 82 | public void setupModule(final SetupContext context) { 83 | final SimpleModule module = new SimpleModule(); 84 | 85 | module.setMixInAnnotation(Exceptional.class, mixinClass()); 86 | 87 | module.setMixInAnnotation(DefaultProblem.class, AbstractThrowableProblemMixIn.class); 88 | module.setMixInAnnotation(Problem.class, ProblemMixIn.class); 89 | 90 | module.addSerializer(HttpStatusCode.class, new HttpStatusSerializer()); 91 | module.addDeserializer(HttpStatusCode.class, new HttpStatusDeserializer(this.statuses)); 92 | 93 | module.addSerializer(HttpMethod.class, new HttpMethodSerializer()); 94 | module.addDeserializer(HttpMethod.class, new HttpMethodDeserializer()); 95 | 96 | module.addSerializer(StackTraceElement.class, new ToStringSerializer()); 97 | 98 | module.setupModule(context); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/application/ApplicationAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.application; 2 | 3 | public interface ApplicationAdviceTraits 4 | extends ApplicationProblemAdviceTrait, 5 | ApplicationExceptionAdviceTrait, 6 | ApplicationMultiProblemAdviceTrait {} 7 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/application/ApplicationExceptionAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.application; 2 | 3 | import com.ksoot.problem.core.ApplicationException; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import java.util.Collections; 8 | import java.util.Optional; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | 13 | /** 14 | * @see AdviceTrait 15 | */ 16 | public interface ApplicationExceptionAdviceTrait extends AdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleApplicationException(final ApplicationException exception, final T request) { 20 | HttpStatus status = exception.getStatus(); 21 | if (StringUtils.isNotBlank(exception.getErrorKey())) { 22 | String errorKey = exception.getErrorKey(); 23 | String detailCode = ProblemConstant.DETAIL_CODE_PREFIX + errorKey; 24 | 25 | Problem problem = 26 | toProblem( 27 | exception, 28 | exception.getStatus(), 29 | errorKey, 30 | Optional.ofNullable(exception.getDefaultDetail()).orElse(detailCode), 31 | exception.getDetailArgs(), 32 | Optional.ofNullable(exception.getParameters()).orElse(Collections.emptyMap())); 33 | 34 | return toResponse(exception, request, status, problem); 35 | } else { 36 | return toResponse(exception, request, exception.getStatus(), exception.getProblem()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/application/ApplicationMultiProblemAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.application; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CODE_CODE_PREFIX; 4 | import static com.ksoot.problem.core.ProblemConstant.DETAIL_CODE_PREFIX; 5 | import static com.ksoot.problem.core.ProblemConstant.ERRORS_KEY; 6 | import static com.ksoot.problem.core.ProblemConstant.TITLE_CODE_PREFIX; 7 | 8 | import com.google.common.collect.Lists; 9 | import com.ksoot.problem.core.GeneralErrorKey; 10 | import com.ksoot.problem.core.MultiProblem; 11 | import com.ksoot.problem.core.Problem; 12 | import com.ksoot.problem.core.ProblemConstant; 13 | import com.ksoot.problem.core.ProblemSupport; 14 | import com.ksoot.problem.spring.advice.AdviceTrait; 15 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 16 | import java.util.Collections; 17 | import java.util.LinkedHashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Objects; 21 | import java.util.Optional; 22 | import org.apache.commons.collections4.CollectionUtils; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.web.bind.annotation.ExceptionHandler; 25 | 26 | /** 27 | * @see AdviceTrait 28 | */ 29 | public interface ApplicationMultiProblemAdviceTrait extends AdviceTrait { 30 | 31 | @ExceptionHandler 32 | default R handleMultiProblem(final MultiProblem exception, final T request) { 33 | List problems = Lists.newArrayList(); 34 | 35 | if (CollectionUtils.isNotEmpty(exception.getErrors())) { 36 | problems.addAll( 37 | exception.getErrors().stream() 38 | .map( 39 | ex -> { 40 | if (ex instanceof Problem problem) { 41 | return problem; 42 | } else if (ex instanceof ProblemSupport problemSupport) { 43 | if (Objects.nonNull(problemSupport.getProblem())) { 44 | return problemSupport.getProblem(); 45 | } else { 46 | String errorKey = problemSupport.getErrorKey(); 47 | String detailCode = ProblemConstant.DETAIL_CODE_PREFIX + errorKey; 48 | 49 | return toProblem( 50 | (Throwable) ex, 51 | problemSupport.getStatus(), 52 | errorKey, 53 | Optional.ofNullable(problemSupport.getDefaultDetail()) 54 | .orElse(detailCode), 55 | problemSupport.getDetailArgs(), 56 | Optional.ofNullable(problemSupport.getParameters()) 57 | .orElse(Collections.emptyMap())); 58 | } 59 | } else if (ex instanceof Throwable throwable) { 60 | return toProblem( 61 | throwable, 62 | GeneralErrorKey.INTERNAL_SERVER_ERROR, 63 | HttpStatus.INTERNAL_SERVER_ERROR); 64 | } else { 65 | throw new IllegalStateException( 66 | "MultiProblem contain illegal instance: " + ex); 67 | } 68 | }) 69 | .toList()); 70 | } 71 | Map parameters = new LinkedHashMap<>(problems.size() + 5); 72 | parameters.put(ERRORS_KEY, problems); 73 | 74 | HttpStatus status = exception.getStatus(); 75 | ProblemMessageSourceResolver codeResolver = 76 | ProblemMessageSourceResolver.of( 77 | CODE_CODE_PREFIX + GeneralErrorKey.MULTIPLE_ERRORS, status.value()); 78 | ProblemMessageSourceResolver titleResolver = 79 | ProblemMessageSourceResolver.of( 80 | TITLE_CODE_PREFIX + GeneralErrorKey.MULTIPLE_ERRORS, status.getReasonPhrase()); 81 | ProblemMessageSourceResolver detailResolver = 82 | ProblemMessageSourceResolver.of( 83 | DETAIL_CODE_PREFIX + GeneralErrorKey.MULTIPLE_ERRORS, exception.getMessage()); 84 | 85 | Problem problem = toProblem(exception, codeResolver, titleResolver, detailResolver, parameters); 86 | return toResponse(exception, request, exception.getStatus(), problem); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/application/ApplicationProblemAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.application; 2 | 3 | import com.ksoot.problem.core.ApplicationProblem; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import java.util.Collections; 8 | import java.util.Optional; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | 13 | /** 14 | * @see AdviceTrait 15 | */ 16 | public interface ApplicationProblemAdviceTrait extends AdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleApplicationProblem(final ApplicationProblem exception, final T request) { 20 | HttpStatus status = exception.getStatus(); 21 | if (StringUtils.isNotBlank(exception.getErrorKey())) { 22 | String errorKey = exception.getErrorKey(); 23 | String detailCode = ProblemConstant.DETAIL_CODE_PREFIX + errorKey; 24 | 25 | Problem problem = 26 | toProblem( 27 | exception, 28 | exception.getStatus(), 29 | errorKey, 30 | Optional.ofNullable(exception.getDefaultDetail()).orElse(detailCode), 31 | exception.getDetailArgs(), 32 | Optional.ofNullable(exception.getParameters()).orElse(Collections.emptyMap())); 33 | 34 | return toResponse(exception, request, status, problem); 35 | } else { 36 | return toResponse(exception, request, exception.getStatus(), exception.getProblem()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/AbstractDaoExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import com.ksoot.problem.spring.config.ProblemConfigException; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.function.Function; 8 | import java.util.stream.Collectors; 9 | import org.apache.commons.collections4.CollectionUtils; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.core.env.Environment; 12 | 13 | public abstract class AbstractDaoExceptionHandler implements DaoAdviceTraits { 14 | 15 | protected final Map constraintNameResolvers; 16 | 17 | protected final Database database; 18 | 19 | protected AbstractDaoExceptionHandler( 20 | final List constraintNameResolvers, final Environment env) { 21 | if (CollectionUtils.isNotEmpty(constraintNameResolvers)) { 22 | this.constraintNameResolvers = 23 | constraintNameResolvers.stream() 24 | .collect(Collectors.toMap(ConstraintNameResolver::dbType, Function.identity())); 25 | } else { 26 | this.constraintNameResolvers = Collections.EMPTY_MAP; 27 | } 28 | 29 | String dbPlatform = env.getProperty("spring.jpa.database"); 30 | if (this.constraintNameResolvers.containsKey(DBType.POSTGRESQL) 31 | || this.constraintNameResolvers.containsKey(DBType.SQL_SERVER) 32 | || this.constraintNameResolvers.containsKey(DBType.MYSQL) 33 | || this.constraintNameResolvers.containsKey(DBType.ORACLE)) { 34 | if (StringUtils.isEmpty(dbPlatform)) { 35 | throw new ProblemConfigException( 36 | "Property \"spring.jpa.database\" not found. Please specify database plateform in configurations"); 37 | } else { 38 | this.database = Database.valueOf(dbPlatform); 39 | } 40 | } else { 41 | this.database = null; 42 | } 43 | } 44 | 45 | @Override 46 | public String resolveConstraintName(final String exceptionMessage) { 47 | if (exceptionMessage.contains("WriteError")) { // MongoDB constraint violation 48 | return this.constraintNameResolvers 49 | .get(DBType.MONGO_DB) 50 | .resolveConstraintName(exceptionMessage); 51 | } else { 52 | switch (this.database) { 53 | case SQL_SERVER: 54 | return this.constraintNameResolvers 55 | .get(DBType.SQL_SERVER) 56 | .resolveConstraintName(exceptionMessage); 57 | case POSTGRESQL: 58 | return this.constraintNameResolvers 59 | .get(DBType.POSTGRESQL) 60 | .resolveConstraintName(exceptionMessage); 61 | case MYSQL: 62 | return this.constraintNameResolvers 63 | .get(DBType.MYSQL) 64 | .resolveConstraintName(exceptionMessage); 65 | case ORACLE: 66 | return this.constraintNameResolvers 67 | .get(DBType.ORACLE) 68 | .resolveConstraintName(exceptionMessage); 69 | // TODO: Add more cases for other databases constraint name resolver 70 | // implementations 71 | default: 72 | throw new IllegalStateException( 73 | "constraintNameResolver bean could not be found, " 74 | + "add ConstraintNameResolver implementation for: " 75 | + this.database); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/BaseDataIntegrityAdvice.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | 5 | public interface BaseDataIntegrityAdvice extends AdviceTrait { 6 | 7 | String resolveConstraintName(final String exceptionMessage); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/ConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import org.springframework.core.Ordered; 4 | 5 | /** 6 | * @author Rajveer Singh 7 | */ 8 | public interface ConstraintNameResolver extends Ordered { 9 | 10 | String resolveConstraintName(final String exceptionMessage); 11 | 12 | DBType dbType(); 13 | 14 | // TODO: Provide implementation for the target database 15 | // DB2, DERBY, H2, HANA, HSQL, INFORMIX, MYSQL, ORACLE, POSTGRESQL, SQL_SERVER, SYBASE 16 | 17 | default int getOrder() { 18 | return 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/DBType.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | public enum DBType { 4 | SQL_SERVER, 5 | POSTGRESQL, 6 | MYSQL, 7 | ORACLE, 8 | MONGO_DB; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/DaoAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | public interface DaoAdviceTraits 4 | extends DataIntegrityViolationAdviceTrait, DuplicateKeyExceptionAdviceTrait {} 5 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/DataIntegrityViolationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 7 | import org.springframework.dao.DataIntegrityViolationException; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | 11 | /** 12 | * @see HttpStatus#INTERNAL_SERVER_ERROR 13 | */ 14 | public interface DataIntegrityViolationAdviceTrait extends BaseDataIntegrityAdvice { 15 | 16 | @ExceptionHandler 17 | default R handleDataIntegrityViolationException( 18 | final DataIntegrityViolationException exception, final T request) { 19 | 20 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 21 | 22 | String exceptionMessage = exception.getMostSpecificCause().getMessage().trim(); 23 | String constraintName = resolveConstraintName(exceptionMessage); 24 | 25 | String codeCode = 26 | ProblemConstant.CODE_CODE_PREFIX 27 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 28 | + ProblemConstant.DOT 29 | + constraintName; 30 | String titleCode = 31 | ProblemConstant.TITLE_CODE_PREFIX 32 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 33 | + ProblemConstant.DOT 34 | + constraintName; 35 | String detailCode = 36 | ProblemConstant.DETAIL_CODE_PREFIX 37 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 38 | + ProblemConstant.DOT 39 | + constraintName; 40 | 41 | Problem problem = 42 | toProblem( 43 | exception, 44 | ProblemMessageSourceResolver.of(codeCode, status.value()), 45 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 46 | ProblemMessageSourceResolver.of( 47 | detailCode, ProblemConstant.DB_CONSTRAINT_VIOLATION_DEFAULT_MESSAGE)); 48 | 49 | return toResponse(exception, request, status, problem); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/Database.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | public enum Database { 4 | DEFAULT, 5 | DB2, 6 | DERBY, 7 | H2, 8 | HANA, 9 | HSQL, 10 | INFORMIX, 11 | MYSQL, 12 | ORACLE, 13 | POSTGRESQL, 14 | SQL_SERVER, 15 | SYBASE 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/DuplicateKeyExceptionAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 7 | import org.springframework.dao.DuplicateKeyException; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | 11 | /** 12 | * @see HttpStatus#INTERNAL_SERVER_ERROR 13 | */ 14 | public interface DuplicateKeyExceptionAdviceTrait extends BaseDataIntegrityAdvice { 15 | 16 | @ExceptionHandler 17 | default R handleDuplicateKeyException(final DuplicateKeyException exception, final T request) { 18 | 19 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 20 | 21 | String exceptionMessage = exception.getMostSpecificCause().getMessage().trim(); 22 | String constraintName = resolveConstraintName(exceptionMessage); 23 | 24 | String codeCode = 25 | ProblemConstant.CODE_CODE_PREFIX 26 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 27 | + ProblemConstant.DOT 28 | + constraintName; 29 | String titleCode = 30 | ProblemConstant.TITLE_CODE_PREFIX 31 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 32 | + ProblemConstant.DOT 33 | + constraintName; 34 | String messageCode = 35 | ProblemConstant.DETAIL_CODE_PREFIX 36 | + GeneralErrorKey.DATA_INTEGRITY_VIOLATION 37 | + ProblemConstant.DOT 38 | + constraintName; 39 | 40 | Problem problem = 41 | toProblem( 42 | exception, 43 | ProblemMessageSourceResolver.of(codeCode, status.value()), 44 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 45 | ProblemMessageSourceResolver.of( 46 | messageCode, ProblemConstant.DB_CONSTRAINT_VIOLATION_DEFAULT_MESSAGE)); 47 | 48 | return toResponse(exception, request, status, problem); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/MongoConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | import com.ksoot.problem.core.ProblemConstant; 4 | 5 | /** 6 | * @author Rajveer Singh 7 | */ 8 | public class MongoConstraintNameResolver implements ConstraintNameResolver { 9 | 10 | /* 11 | * (non-Javadoc) 12 | * 13 | * @see com.ksoot.framework.common.spring.config.error.dbconstraint. 14 | * ConstraintNameResolver# resolveConstraintName(org.springframework.dao. 15 | * DataIntegrityViolationException) 16 | */ 17 | @Override 18 | public String resolveConstraintName(final String exceptionMessage) { 19 | String exMessage = exceptionMessage.trim(); 20 | try { 21 | String temp = exMessage.substring(exMessage.indexOf("collection: ") + 12); 22 | String collectionName = temp.substring(temp.indexOf(".") + 1, temp.indexOf(" index: ")); 23 | temp = exMessage.substring(exMessage.indexOf("index: ") + 7); 24 | String indexName = temp.substring(0, temp.indexOf(" ")); 25 | return collectionName + ProblemConstant.DOT + indexName; 26 | } catch (final Exception e) { 27 | // Ignored on purpose 28 | } 29 | return "mongo.duplicate.key"; 30 | } 31 | 32 | @Override 33 | public DBType dbType() { 34 | return DBType.MONGO_DB; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/MysqlConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | /** 4 | * @author Rajveer Singh 5 | */ 6 | public class MysqlConstraintNameResolver implements ConstraintNameResolver { 7 | 8 | /* 9 | * (non-Javadoc) 10 | * 11 | * @see com.ksoot.framework.common.spring.config.error.db.ConstraintNameResolver# 12 | * resolveConstraintName(org.springframework.dao. DataIntegrityViolationException) 13 | */ 14 | @Override 15 | public String resolveConstraintName(final String exceptionMessage) { 16 | // TODO Auto-generated method stub 17 | throw new UnsupportedOperationException("Auto-generated method stub"); 18 | } 19 | 20 | @Override 21 | public DBType dbType() { 22 | return DBType.MYSQL; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/OracleConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | /** 4 | * @author Rajveer Singh 5 | */ 6 | public class OracleConstraintNameResolver implements ConstraintNameResolver { 7 | 8 | /* 9 | * (non-Javadoc) 10 | * 11 | * @see com.ksoot.framework.common.spring.config.error.db.ConstraintNameResolver# 12 | * resolveConstraintName(org.springframework.dao. DataIntegrityViolationException) 13 | */ 14 | @Override 15 | public String resolveConstraintName(final String exceptionMessage) { 16 | // TODO Auto-generated method stub 17 | throw new UnsupportedOperationException("Auto-generated method stub"); 18 | } 19 | 20 | @Override 21 | public DBType dbType() { 22 | return DBType.ORACLE; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/PostgresConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | /** 4 | * @author Rajveer Singh 5 | */ 6 | public class PostgresConstraintNameResolver implements ConstraintNameResolver { 7 | 8 | /* 9 | * (non-Javadoc) 10 | * 11 | * @see com.ksoot.framework.common.spring.config.error.dbconstraint. 12 | * ConstraintNameResolver# resolveConstraintName(org.springframework.dao. 13 | * DataIntegrityViolationException) 14 | */ 15 | @Override 16 | public String resolveConstraintName(final String exceptionMessage) { 17 | String exMessage = exceptionMessage.trim(); 18 | try { 19 | exMessage = exMessage.substring(exMessage.indexOf("constraint") + 12); 20 | return exMessage.substring(0, exMessage.indexOf("\"")); 21 | } catch (final Exception e) { 22 | // Ignored on purpose 23 | } 24 | return "postgres.duplicate.key"; 25 | } 26 | 27 | @Override 28 | public DBType dbType() { 29 | return DBType.POSTGRESQL; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/dao/SQLServerConstraintNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.dao; 2 | 3 | /** 4 | * @author Rajveer Singh 5 | */ 6 | public class SQLServerConstraintNameResolver implements ConstraintNameResolver { 7 | 8 | /* 9 | * (non-Javadoc) 10 | * 11 | * @see com.ksoot.framework.common.spring.config.error.DBConstraintNameResolver# 12 | * resolveConstraintName(org.springframework.dao. DataIntegrityViolationException) 13 | */ 14 | @Override 15 | public String resolveConstraintName(final String exceptionMessage) { 16 | try { 17 | return exceptionMessage.substring( 18 | exceptionMessage.indexOf("\"") + 1, exceptionMessage.lastIndexOf("\"")); 19 | } catch (final Exception e) { 20 | // Ignored on purpose 21 | } 22 | return "sqlserver.duplicate.key"; 23 | } 24 | 25 | @Override 26 | public DBType dbType() { 27 | return DBType.SQL_SERVER; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/general/GeneralAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.general; 2 | 3 | public interface GeneralAdviceTraits 4 | extends ProblemAdviceTrait, 5 | ThrowableAdviceTrait, 6 | UnsupportedOperationAdviceTrait {} 7 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/general/ProblemAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.general; 2 | 3 | import com.ksoot.problem.core.DefaultProblem; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ThrowableProblem; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | /** 11 | * @see Problem 12 | * @see ThrowableProblem 13 | */ 14 | public interface ProblemAdviceTrait extends AdviceTrait { 15 | 16 | @ExceptionHandler 17 | default R handleProblem(final ThrowableProblem problem, final T request) { 18 | return toResponse(problem, request); 19 | } 20 | 21 | @ExceptionHandler 22 | default R handleProblem(final DefaultProblem problem, final T request) { 23 | return toResponse(problem, request, HttpStatus.INTERNAL_SERVER_ERROR, problem); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/general/ThrowableAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.general; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.spring.advice.AdviceTrait; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | /** 10 | * @see Throwable 11 | * @see Exception 12 | */ 13 | public interface ThrowableAdviceTrait extends AdviceTrait { 14 | 15 | @ExceptionHandler 16 | default R handleThrowable(final Throwable throwable, final T request) { 17 | HttpStatus status = resolveStatus(throwable); 18 | Problem problem = toProblem(throwable, GeneralErrorKey.INTERNAL_SERVER_ERROR, status); 19 | return toResponse(throwable, request, status, problem); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/general/UnsupportedOperationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.general; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.spring.advice.AdviceTrait; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | /** 10 | * @see UnsupportedOperationException 11 | * @see HttpStatus#NOT_IMPLEMENTED 12 | */ 13 | public interface UnsupportedOperationAdviceTrait extends AdviceTrait { 14 | 15 | @ExceptionHandler 16 | default R handleUnsupportedOperation( 17 | final UnsupportedOperationException exception, final T request) { 18 | HttpStatus status = HttpStatus.NOT_IMPLEMENTED; 19 | Problem problem = toProblem(exception, GeneralErrorKey.INTERNAL_SERVER_ERROR, status); 20 | return toResponse(exception, request, status, problem); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/BaseNotAcceptableAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.DETAIL_CODE_PREFIX; 4 | 5 | import com.ksoot.problem.core.GeneralErrorKey; 6 | import com.ksoot.problem.core.Problem; 7 | import com.ksoot.problem.spring.advice.AdviceTrait; 8 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 9 | import java.util.List; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.util.MimeTypeUtils; 14 | 15 | public interface BaseNotAcceptableAdviceTrait extends AdviceTrait { 16 | 17 | default R processMediaTypeNotSupportedException( 18 | final List supportedMediaTypes, 19 | final MediaType causeMediaType, 20 | final Exception exception, 21 | final T request) { 22 | final HttpHeaders headers = new HttpHeaders(); 23 | headers.setAccept(supportedMediaTypes); 24 | Problem problem = 25 | toProblem( 26 | exception, 27 | HttpStatus.UNSUPPORTED_MEDIA_TYPE, 28 | ProblemMessageSourceResolver.of( 29 | DETAIL_CODE_PREFIX + GeneralErrorKey.MEDIA_TYPE_NOT_SUPPORTED, 30 | "Media Type: {0} Not Acceptable, Supported Media Types are: {1}", 31 | new Object[] {causeMediaType, MimeTypeUtils.toString(supportedMediaTypes)})); 32 | return buildResponse(exception, request, HttpStatus.UNSUPPORTED_MEDIA_TYPE, headers, problem); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/HttpAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | public interface HttpAdviceTraits 4 | extends HttpMediaTypeNotAcceptableAdviceTrait, 5 | HttpMediaTypeNotSupportedExceptionAdviceTrait, 6 | UnsupportedMediaTypeStatusAdviceTrait, 7 | HttpRequestMethodNotSupportedAdviceTrait, 8 | MethodNotAllowedAdviceTrait, 9 | NotAcceptableStatusAdviceTrait, 10 | ResponseStatusAdviceTrait {} 11 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/HttpMediaTypeNotAcceptableAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import java.util.List; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.util.MimeTypeUtils; 12 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | 15 | /** 16 | * @see HttpMediaTypeNotAcceptableException 17 | * @see HttpStatus#NOT_ACCEPTABLE 18 | */ 19 | public interface HttpMediaTypeNotAcceptableAdviceTrait extends AdviceTrait { 20 | 21 | @ExceptionHandler 22 | default R handleMediaTypeNotAcceptable( 23 | final HttpMediaTypeNotAcceptableException exception, final T request) { 24 | List supportedMediaTypes = exception.getSupportedMediaTypes(); 25 | Problem problem = 26 | toProblem( 27 | exception, 28 | HttpStatus.NOT_ACCEPTABLE, 29 | ProblemMessageSourceResolver.of( 30 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.MEDIA_TYPE_NOT_ACCEPTABLE, 31 | "Media Type Not Acceptable, except: {0}", 32 | new Object[] {MimeTypeUtils.toString(supportedMediaTypes)})); 33 | return toResponse(exception, request, HttpStatus.NOT_ACCEPTABLE, problem); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/HttpMediaTypeNotSupportedExceptionAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.HttpMediaTypeNotSupportedException; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | 7 | /** 8 | * @see HttpMediaTypeNotSupportedException 9 | * @see HttpStatus#UNSUPPORTED_MEDIA_TYPE 10 | */ 11 | public interface HttpMediaTypeNotSupportedExceptionAdviceTrait 12 | extends BaseNotAcceptableAdviceTrait { 13 | 14 | @ExceptionHandler 15 | default R handleHttpMediaTypeNotSupportedException( 16 | final HttpMediaTypeNotSupportedException exception, final T request) { 17 | return processMediaTypeNotSupportedException( 18 | exception.getSupportedMediaTypes(), exception.getContentType(), exception, request); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/HttpRequestMethodNotSupportedAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.ksoot.problem.core.GeneralErrorKey; 6 | import com.ksoot.problem.core.Problem; 7 | import com.ksoot.problem.core.ProblemConstant; 8 | import com.ksoot.problem.spring.advice.AdviceTrait; 9 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 10 | import jakarta.annotation.Nullable; 11 | import org.apache.commons.lang3.ArrayUtils; 12 | import org.springframework.http.HttpHeaders; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.HttpRequestMethodNotSupportedException; 15 | import org.springframework.web.bind.annotation.ExceptionHandler; 16 | 17 | public interface HttpRequestMethodNotSupportedAdviceTrait extends AdviceTrait { 18 | 19 | @ExceptionHandler 20 | default R handleRequestMethodNotSupportedException( 21 | final HttpRequestMethodNotSupportedException exception, final T request) { 22 | @Nullable final String[] methods = exception.getSupportedMethods(); 23 | String requestedMethod = exception.getMethod(); 24 | String allowedMethods = ArrayUtils.isEmpty(methods) ? "None" : String.join(",", methods); 25 | Problem problem = 26 | toProblem( 27 | exception, 28 | HttpStatus.METHOD_NOT_ALLOWED, 29 | ProblemMessageSourceResolver.of( 30 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.REQUEST_METHOD_NOT_SUPPORTED, 31 | "Requested Method: {0} not allowed, allowed methods are: {1}", 32 | new Object[] {requestedMethod, allowedMethods})); 33 | 34 | final HttpHeaders headers = new HttpHeaders(); 35 | headers.setAllow(requireNonNull(exception.getSupportedHttpMethods())); 36 | return buildResponse(exception, request, HttpStatus.METHOD_NOT_ALLOWED, headers, problem); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/MethodNotAllowedAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.ksoot.problem.core.GeneralErrorKey; 6 | import com.ksoot.problem.core.Problem; 7 | import com.ksoot.problem.core.ProblemConstant; 8 | import com.ksoot.problem.spring.advice.AdviceTrait; 9 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 10 | import jakarta.annotation.Nullable; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | import org.apache.commons.collections4.CollectionUtils; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.http.HttpMethod; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | import org.springframework.web.server.MethodNotAllowedException; 19 | 20 | public interface MethodNotAllowedAdviceTrait extends AdviceTrait { 21 | 22 | @ExceptionHandler 23 | default R handleMethodNotAllowedException( 24 | final MethodNotAllowedException exception, final T request) { 25 | @Nullable final Set methods = exception.getSupportedMethods(); 26 | String requestedMethod = exception.getHttpMethod(); 27 | String allowedMethods = 28 | CollectionUtils.isEmpty(methods) 29 | ? "None" 30 | : methods.stream().map(HttpMethod::name).collect(Collectors.joining(",")); 31 | Problem problem = 32 | toProblem( 33 | exception, 34 | HttpStatus.METHOD_NOT_ALLOWED, 35 | ProblemMessageSourceResolver.of( 36 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.METHOD_NOT_ALLOWED, 37 | "Requested Method: {0} not allowed, allowed methods are: {1}", 38 | new Object[] {requestedMethod, allowedMethods})); 39 | 40 | final HttpHeaders headers = new HttpHeaders(); 41 | headers.setAllow(requireNonNull(methods)); 42 | return buildResponse(exception, request, HttpStatus.METHOD_NOT_ALLOWED, headers, problem); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/NotAcceptableStatusAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.server.NotAcceptableStatusException; 8 | 9 | /** 10 | * @see NotAcceptableStatusException 11 | * @see HttpStatus#NOT_ACCEPTABLE 12 | */ 13 | public interface NotAcceptableStatusAdviceTrait extends BaseNotAcceptableAdviceTrait { 14 | 15 | @ExceptionHandler 16 | default R handleMediaTypeNotAcceptable( 17 | final NotAcceptableStatusException exception, final T request) { 18 | HttpStatus status = HttpStatus.NOT_ACCEPTABLE; 19 | Problem problem = toProblem(exception, GeneralErrorKey.INTERNAL_SERVER_ERROR, status); 20 | return toResponse(exception, request, status, problem); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/ResponseStatusAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.server.ResponseStatusException; 7 | 8 | /** 9 | * @see ResponseStatusException 10 | */ 11 | public interface ResponseStatusAdviceTrait 12 | extends AdviceTrait /* SpringAdviceTrait */ { 13 | 14 | @ExceptionHandler 15 | default R handleResponseStatusException( 16 | final ResponseStatusException exception, final T request) { 17 | return toResponse(exception, request, HttpStatus.valueOf(exception.getStatusCode().value())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/http/UnsupportedMediaTypeStatusAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.http; 2 | 3 | import org.springframework.web.bind.annotation.ExceptionHandler; 4 | import org.springframework.web.server.UnsupportedMediaTypeStatusException; 5 | 6 | public interface UnsupportedMediaTypeStatusAdviceTrait 7 | extends BaseNotAcceptableAdviceTrait { 8 | 9 | @ExceptionHandler 10 | default R handleUnsupportedMediaTypeStatusException( 11 | final UnsupportedMediaTypeStatusException exception, final T request) { 12 | return processMediaTypeNotSupportedException( 13 | exception.getSupportedMediaTypes(), exception.getContentType(), exception, request); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/io/DataBufferLimitExceptionAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.io; 2 | 3 | import com.google.common.base.CharMatcher; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.apache.commons.lang3.ClassUtils; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.core.io.buffer.DataBufferLimitException; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.util.unit.DataSize; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | 15 | public interface DataBufferLimitExceptionAdviceTrait extends AdviceTrait { 16 | 17 | // org.springframework.core.io.buffer.DataBufferLimitException: Part exceeded the disk usage 18 | // limit of 1024 bytes 19 | 20 | @ExceptionHandler 21 | default R handleDataBufferLimitException( 22 | final DataBufferLimitException exception, final T request) { 23 | String errorKey = ClassUtils.getName(exception); 24 | String defaultMessage = exception.getMessage(); 25 | long bytes = -1; 26 | try { 27 | final String byteSizeString = CharMatcher.inRange('0', '9').retainFrom(defaultMessage); 28 | bytes = StringUtils.isNotBlank(byteSizeString) ? Long.parseLong(byteSizeString) : bytes; 29 | } catch (final Exception ex) { 30 | // Ignore on purpose 31 | } 32 | 33 | String maxFileSizeAllowed = bytes != -1 ? DataSize.ofBytes(bytes).toString() : "UNKNOWN"; 34 | 35 | String detailCode = ProblemConstant.DETAIL_CODE_PREFIX + errorKey; 36 | 37 | Problem problem = 38 | toProblem( 39 | exception, 40 | HttpStatus.BAD_REQUEST, 41 | ProblemMessageSourceResolver.of( 42 | detailCode, defaultMessage, new Object[] {maxFileSizeAllowed})); 43 | 44 | return toResponse(exception, request, HttpStatus.BAD_REQUEST, problem); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/io/IOAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.io; 2 | 3 | public interface IOAdviceTraits 4 | extends MessageNotReadableAdviceTrait, 5 | MaxUploadSizeExceededExceptionAdviceTrait, 6 | DataBufferLimitExceptionAdviceTrait, 7 | MultipartAdviceTrait {} 8 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/io/MaxUploadSizeExceededExceptionAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.io; 2 | 3 | import com.google.common.base.CharMatcher; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.apache.commons.lang3.ClassUtils; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.util.unit.DataSize; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.multipart.MaxUploadSizeExceededException; 15 | 16 | public interface MaxUploadSizeExceededExceptionAdviceTrait extends AdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleMaxUploadSizeExceededException( 20 | final MaxUploadSizeExceededException exception, final T request) { 21 | String errorKey = ClassUtils.getName(exception); 22 | String defaultMessage = exception.getMessage(); 23 | long bytes = exception.getMaxUploadSize(); 24 | if (bytes == -1 && exception.getCause() instanceof IllegalStateException e) { 25 | try { 26 | defaultMessage = e.getMessage(); 27 | final String byteSizeString = 28 | defaultMessage.substring( 29 | defaultMessage.lastIndexOf("(") + 1, defaultMessage.lastIndexOf(")")); 30 | bytes = StringUtils.isNotBlank(byteSizeString) ? Long.parseLong(byteSizeString) : bytes; 31 | } catch (final Exception ex) { 32 | // Ignore on purpose 33 | } 34 | } 35 | if (bytes == -1 36 | && exception.getMostSpecificCause() instanceof FileSizeLimitExceededException e) { 37 | try { 38 | defaultMessage = e.getMessage(); 39 | final String byteSizeString = CharMatcher.inRange('0', '9').retainFrom(defaultMessage); 40 | bytes = StringUtils.isNotBlank(byteSizeString) ? Long.parseLong(byteSizeString) : bytes; 41 | } catch (final Exception ex) { 42 | // Ignore on purpose 43 | } 44 | } 45 | String maxFileSizeAllowed = bytes != -1 ? DataSize.ofBytes(bytes).toString() : "UNKNOWN"; 46 | 47 | String detailCode = ProblemConstant.DETAIL_CODE_PREFIX + errorKey; 48 | 49 | Problem problem = 50 | toProblem( 51 | exception, 52 | HttpStatus.BAD_REQUEST, 53 | ProblemMessageSourceResolver.of( 54 | detailCode, defaultMessage, new Object[] {maxFileSizeAllowed})); 55 | 56 | return toResponse(exception, request, HttpStatus.BAD_REQUEST, problem); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/io/MessageNotReadableAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.io; 2 | 3 | import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; 4 | 5 | import com.fasterxml.jackson.databind.JsonMappingException.Reference; 6 | import com.fasterxml.jackson.databind.exc.InvalidFormatException; 7 | import com.ksoot.problem.core.GeneralErrorKey; 8 | import com.ksoot.problem.core.Problem; 9 | import com.ksoot.problem.core.ProblemConstant; 10 | import com.ksoot.problem.spring.advice.AdviceTrait; 11 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import org.apache.commons.lang3.ClassUtils; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.converter.HttpMessageNotReadableException; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | 19 | /** 20 | * @see HttpMessageNotReadableException 21 | * @see HttpStatus#BAD_REQUEST 22 | */ 23 | public interface MessageNotReadableAdviceTrait extends AdviceTrait { 24 | 25 | @ExceptionHandler 26 | default R handleMessageNotReadableException( 27 | final HttpMessageNotReadableException exception, final T request) { 28 | if (exception.getCause() instanceof InvalidFormatException invalidFormatException) { 29 | return handleInvalidFormatException(invalidFormatException, request); 30 | } 31 | return toResponse(exception, request, HttpStatus.BAD_REQUEST); 32 | } 33 | 34 | default R handleInvalidFormatException( 35 | final InvalidFormatException invalidFormatException, final T request) { 36 | 37 | String[] errorPropertyKeys = deriveInvalidFormatExceptionErrorKeys(invalidFormatException); 38 | 39 | String[] codeCodes = 40 | Arrays.stream(errorPropertyKeys) 41 | .map(errorKey -> ProblemConstant.CODE_CODE_PREFIX + errorKey) 42 | .toArray(String[]::new); 43 | String[] titleCodes = 44 | Arrays.stream(errorPropertyKeys) 45 | .map(errorKey -> ProblemConstant.TITLE_CODE_PREFIX + errorKey) 46 | .toArray(String[]::new); 47 | String[] detailCodes = 48 | Arrays.stream(errorPropertyKeys) 49 | .map(errorKey -> ProblemConstant.DETAIL_CODE_PREFIX + errorKey) 50 | .toArray(String[]::new); 51 | 52 | Problem problem = 53 | toProblem( 54 | invalidFormatException, 55 | ProblemMessageSourceResolver.of(codeCodes, "" + HttpStatus.BAD_REQUEST.value()), 56 | ProblemMessageSourceResolver.of(titleCodes, HttpStatus.BAD_REQUEST.getReasonPhrase()), 57 | ProblemMessageSourceResolver.of(detailCodes, invalidFormatException.getMessage())); 58 | 59 | return toResponse(invalidFormatException, request, HttpStatus.BAD_REQUEST, problem); 60 | } 61 | 62 | default String[] deriveInvalidFormatExceptionErrorKeys( 63 | final InvalidFormatException invalidFormatException) { 64 | List pathRef = invalidFormatException.getPath(); 65 | if (isNotEmpty(pathRef)) { 66 | String desc = pathRef.get(0).getDescription(); 67 | String packageName = desc.contains("[") ? desc.substring(0, desc.lastIndexOf(".")) : desc; 68 | List classes = 69 | pathRef.stream() 70 | .map(Reference::getDescription) 71 | .filter(description -> description.contains("[\"")) 72 | .map( 73 | description -> 74 | description.substring( 75 | description.lastIndexOf(".") + 1, description.lastIndexOf("["))) 76 | .toList(); 77 | String classNames = String.join(".", classes); 78 | Reference ref = pathRef.get(pathRef.size() - 1); 79 | String fieldName = ref.getFieldName(); 80 | String targetType = ClassUtils.getName(invalidFormatException.getTargetType()); 81 | 82 | return new String[] { 83 | GeneralErrorKey.INVALID_FORMAT 84 | + ProblemConstant.DOT 85 | + packageName 86 | + ProblemConstant.DOT 87 | + classNames 88 | + ProblemConstant.DOT 89 | + fieldName, 90 | GeneralErrorKey.INVALID_FORMAT 91 | + ProblemConstant.DOT 92 | + classNames 93 | + ProblemConstant.DOT 94 | + fieldName, 95 | GeneralErrorKey.INVALID_FORMAT 96 | + ProblemConstant.DOT 97 | + targetType 98 | + ProblemConstant.DOT 99 | + fieldName, 100 | GeneralErrorKey.INVALID_FORMAT + ProblemConstant.DOT + fieldName 101 | }; 102 | } else { 103 | return new String[] {GeneralErrorKey.INVALID_FORMAT}; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/io/MultipartAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.io; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.spring.advice.AdviceTrait; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.multipart.MultipartException; 9 | 10 | // TODO find a better name 11 | public interface MultipartAdviceTrait extends AdviceTrait { 12 | 13 | @ExceptionHandler 14 | default R handleMultipart(final MultipartException exception, final T request) { 15 | HttpStatus status = HttpStatus.BAD_REQUEST; 16 | Problem problem = toProblem(exception, GeneralErrorKey.INTERNAL_SERVER_ERROR, status); 17 | return toResponse(exception, request, status, problem); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/network/CircuitBreakerOpenAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.network; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | import net.jodah.failsafe.CircuitBreakerOpenException; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | public interface CircuitBreakerOpenAdviceTrait extends AdviceTrait { 10 | 11 | @ExceptionHandler 12 | default R handleCircuitBreakerOpen(final CircuitBreakerOpenException exception, final T request) { 13 | final long delay = exception.getCircuitBreaker().getRemainingDelay().getSeconds(); 14 | final HttpHeaders headers = retryAfter(delay); 15 | return toResponse(exception, request, HttpStatus.SERVICE_UNAVAILABLE, headers); 16 | } 17 | 18 | default HttpHeaders retryAfter(final long delay) { 19 | final HttpHeaders headers = new HttpHeaders(); 20 | headers.add(HttpHeaders.RETRY_AFTER, String.valueOf(delay)); 21 | return headers; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/network/NetworkAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.network; 2 | 3 | public interface NetworkAdviceTraits extends CircuitBreakerOpenAdviceTrait {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/MissingRequestHeaderAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.validation.BaseValidationAdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.MissingRequestHeaderException; 10 | import org.springframework.web.bind.MissingServletRequestParameterException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | 13 | /** 14 | * @see MissingServletRequestParameterException 15 | * @see HttpStatus#BAD_REQUEST 16 | */ 17 | public interface MissingRequestHeaderAdviceTrait extends BaseValidationAdviceTrait { 18 | 19 | @ExceptionHandler 20 | default R handleMissingServletRequestParameter( 21 | final MissingRequestHeaderException exception, final T request) { 22 | String errorKey = 23 | exception.getParameter().getContainingClass().getSimpleName() 24 | + ProblemConstant.DOT 25 | + exception.getParameter().getMethod().getName() 26 | + ProblemConstant.DOT 27 | + exception.getHeaderName(); 28 | 29 | String codeCode = 30 | ProblemConstant.CODE_CODE_PREFIX 31 | + GeneralErrorKey.MISSING_REQUEST_HEADER 32 | + ProblemConstant.DOT 33 | + errorKey; 34 | String titleCode = 35 | ProblemConstant.TITLE_CODE_PREFIX 36 | + GeneralErrorKey.MISSING_REQUEST_HEADER 37 | + ProblemConstant.DOT 38 | + errorKey; 39 | String detailCode = 40 | ProblemConstant.DETAIL_CODE_PREFIX 41 | + GeneralErrorKey.MISSING_REQUEST_HEADER 42 | + ProblemConstant.DOT 43 | + errorKey; 44 | 45 | HttpStatus status = defaultConstraintViolationStatus(); 46 | 47 | Problem problem = 48 | toProblem( 49 | exception, 50 | ProblemMessageSourceResolver.of(codeCode, status.value()), 51 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 52 | ProblemMessageSourceResolver.of(detailCode, exception.getMessage())); 53 | 54 | return toResponse(exception, request, status, problem); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/MissingServletRequestParameterAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.validation.BaseValidationAdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.MissingServletRequestParameterException; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | 12 | /** 13 | * @see MissingServletRequestParameterException 14 | * @see HttpStatus#BAD_REQUEST 15 | */ 16 | public interface MissingServletRequestParameterAdviceTrait 17 | extends BaseValidationAdviceTrait { 18 | 19 | @ExceptionHandler 20 | default R handleMissingServletRequestParameter( 21 | final MissingServletRequestParameterException exception, final T request) { 22 | 23 | String errorKey = exception.getParameterName(); 24 | 25 | String codeCode = 26 | ProblemConstant.CODE_CODE_PREFIX 27 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PARAMETER 28 | + ProblemConstant.DOT 29 | + errorKey; 30 | String titleCode = 31 | ProblemConstant.TITLE_CODE_PREFIX 32 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PARAMETER 33 | + ProblemConstant.DOT 34 | + errorKey; 35 | String detailCode = 36 | ProblemConstant.DETAIL_CODE_PREFIX 37 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PARAMETER 38 | + ProblemConstant.DOT 39 | + errorKey; 40 | 41 | HttpStatus status = defaultConstraintViolationStatus(); 42 | 43 | Problem problem = 44 | toProblem( 45 | exception, 46 | ProblemMessageSourceResolver.of(codeCode, status.value()), 47 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 48 | ProblemMessageSourceResolver.of(detailCode, exception.getMessage())); 49 | 50 | return toResponse(exception, request, status, problem); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/MissingServletRequestPartAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.validation.BaseValidationAdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.multipart.support.MissingServletRequestPartException; 11 | 12 | /** 13 | * @see MissingServletRequestPartException 14 | * @see HttpStatus#BAD_REQUEST 15 | */ 16 | public interface MissingServletRequestPartAdviceTrait 17 | extends BaseValidationAdviceTrait { 18 | 19 | @ExceptionHandler 20 | default R handleMissingServletRequestPart( 21 | final MissingServletRequestPartException exception, final T request) { 22 | String errorKey = exception.getRequestPartName(); 23 | 24 | String codeCode = 25 | ProblemConstant.CODE_CODE_PREFIX 26 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PART 27 | + ProblemConstant.DOT 28 | + errorKey; 29 | String titleCode = 30 | ProblemConstant.TITLE_CODE_PREFIX 31 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PART 32 | + ProblemConstant.DOT 33 | + errorKey; 34 | String detailCode = 35 | ProblemConstant.DETAIL_CODE_PREFIX 36 | + GeneralErrorKey.MISSING_SERVLET_REQUEST_PART 37 | + ProblemConstant.DOT 38 | + errorKey; 39 | 40 | HttpStatus status = defaultConstraintViolationStatus(); 41 | 42 | Problem problem = 43 | toProblem( 44 | exception, 45 | ProblemMessageSourceResolver.of(codeCode, status.value()), 46 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 47 | ProblemMessageSourceResolver.of(detailCode, exception.getMessage())); 48 | 49 | return toResponse(exception, request, status, problem); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/NoHandlerFoundAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.servlet.DispatcherServlet; 11 | import org.springframework.web.servlet.NoHandlerFoundException; 12 | 13 | /** 14 | * Transforms {@link NoHandlerFoundException NoHandlerFoundExceptions} into {@link 15 | * HttpStatus#NOT_FOUND not-found} {@link Problem problems}. 16 | * 17 | *

Note: This requires {@link 18 | * DispatcherServlet#setThrowExceptionIfNoHandlerFound(boolean)} being set to true. 19 | * 20 | * @see NoHandlerFoundException 21 | * @see HttpStatus#NOT_FOUND 22 | * @see DispatcherServlet#setThrowExceptionIfNoHandlerFound(boolean) 23 | */ 24 | public interface NoHandlerFoundAdviceTrait extends AdviceTrait { 25 | 26 | @ExceptionHandler 27 | default R handleNoHandlerFound(final NoHandlerFoundException exception, final T request) { 28 | Problem problem = 29 | toProblem( 30 | exception, 31 | ProblemMessageSourceResolver.of( 32 | ProblemConstant.CODE_CODE_PREFIX + GeneralErrorKey.NO_HANDLER_FOUND, 33 | HttpStatus.NOT_FOUND.value()), 34 | ProblemMessageSourceResolver.of( 35 | ProblemConstant.TITLE_CODE_PREFIX + GeneralErrorKey.NO_HANDLER_FOUND, 36 | HttpStatus.NOT_FOUND.getReasonPhrase()), 37 | ProblemMessageSourceResolver.of( 38 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.NO_HANDLER_FOUND, 39 | exception.getMessage())); 40 | return toResponse(exception, request, HttpStatus.NOT_FOUND, problem); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/RoutingAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | public interface RoutingAdviceTraits 4 | extends MissingServletRequestParameterAdviceTrait, 5 | MissingServletRequestPartAdviceTrait, 6 | MissingRequestHeaderAdviceTrait, 7 | NoHandlerFoundAdviceTrait, 8 | ServletRequestBindingAdviceTrait {} 9 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/routing/ServletRequestBindingAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.routing; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.spring.advice.AdviceTrait; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.ServletRequestBindingException; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | /** 11 | * @see ServletRequestBindingException 12 | * @see HttpStatus#BAD_REQUEST 13 | */ 14 | public interface ServletRequestBindingAdviceTrait extends AdviceTrait { 15 | 16 | @ExceptionHandler 17 | default R handleServletRequestBinding( 18 | final ServletRequestBindingException exception, final T request) { 19 | HttpStatus status = HttpStatus.BAD_REQUEST; 20 | Problem problem = toProblem(exception, GeneralErrorKey.INTERNAL_SERVER_ERROR, status); 21 | return toResponse(exception, request, status, problem); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/AccessDeniedAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.security.access.AccessDeniedException; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | 12 | /** 13 | * The request was a valid request, but the server is refusing to respond to it. The user might be 14 | * logged in but does not have the necessary permissions for the resource. 15 | */ 16 | public interface AccessDeniedAdviceTrait extends AdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleAccessDeniedException(final AccessDeniedException exception, final T request) { 20 | Problem problem = 21 | toProblem( 22 | exception, 23 | ProblemMessageSourceResolver.of( 24 | ProblemConstant.CODE_CODE_PREFIX + GeneralErrorKey.SECURITY_ACCESS_DENIED, 25 | HttpStatus.FORBIDDEN.value()), 26 | ProblemMessageSourceResolver.of( 27 | ProblemConstant.TITLE_CODE_PREFIX + GeneralErrorKey.SECURITY_ACCESS_DENIED, 28 | HttpStatus.FORBIDDEN.getReasonPhrase()), 29 | ProblemMessageSourceResolver.of( 30 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.SECURITY_ACCESS_DENIED, 31 | exception.getMessage())); 32 | return toResponse(exception, request, HttpStatus.FORBIDDEN, problem); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/AuthenticationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | 12 | /** 13 | * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed 14 | * or has not yet been provided. 15 | */ 16 | public interface AuthenticationAdviceTrait extends AdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleAuthenticationException( 20 | final AuthenticationException exception, final T request) { 21 | Problem problem = 22 | toProblem( 23 | exception, 24 | ProblemMessageSourceResolver.of( 25 | ProblemConstant.CODE_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 26 | HttpStatus.UNAUTHORIZED.value()), 27 | ProblemMessageSourceResolver.of( 28 | ProblemConstant.TITLE_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 29 | HttpStatus.UNAUTHORIZED.getReasonPhrase()), 30 | ProblemMessageSourceResolver.of( 31 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 32 | exception.getMessage())); 33 | return toResponse(exception, request, HttpStatus.UNAUTHORIZED, problem); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/InsufficientAuthenticationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import com.ksoot.problem.core.GeneralErrorKey; 4 | import com.ksoot.problem.core.Problem; 5 | import com.ksoot.problem.core.ProblemConstant; 6 | import com.ksoot.problem.spring.advice.AdviceTrait; 7 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.security.authentication.InsufficientAuthenticationException; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | 12 | public interface InsufficientAuthenticationAdviceTrait extends AdviceTrait { 13 | 14 | @ExceptionHandler 15 | default R handleInsufficientAuthenticationException( 16 | final InsufficientAuthenticationException exception, final T request) { 17 | Problem problem = 18 | toProblem( 19 | exception, 20 | ProblemMessageSourceResolver.of( 21 | ProblemConstant.CODE_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 22 | HttpStatus.UNAUTHORIZED.value()), 23 | ProblemMessageSourceResolver.of( 24 | ProblemConstant.TITLE_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 25 | HttpStatus.UNAUTHORIZED.getReasonPhrase()), 26 | ProblemMessageSourceResolver.of( 27 | ProblemConstant.DETAIL_CODE_PREFIX + GeneralErrorKey.SECURITY_UNAUTHORIZED, 28 | exception.getMessage())); 29 | return toResponse(exception, request, HttpStatus.UNAUTHORIZED, problem); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/ProblemAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import jakarta.servlet.ServletException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import java.io.IOException; 7 | import org.springframework.security.access.AccessDeniedException; 8 | import org.springframework.security.web.AuthenticationEntryPoint; 9 | import org.springframework.security.web.access.AccessDeniedHandler; 10 | import org.springframework.web.servlet.HandlerExceptionResolver; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 12 | 13 | /** 14 | * A compound {@link AuthenticationEntryPoint} and {@link AccessDeniedHandler} that delegates 15 | * exceptions to Spring WebMVC's {@link HandlerExceptionResolver} as defined in {@link 16 | * WebMvcConfigurationSupport}. 17 | * 18 | *

Compatible with spring-webmvc 4.3.3. 19 | */ 20 | public class ProblemAccessDeniedHandler implements AccessDeniedHandler { 21 | 22 | private final HandlerExceptionResolver resolver; 23 | 24 | public ProblemAccessDeniedHandler(final HandlerExceptionResolver resolver) { 25 | this.resolver = resolver; 26 | } 27 | 28 | @Override 29 | public void handle( 30 | final HttpServletRequest request, 31 | final HttpServletResponse response, 32 | AccessDeniedException exception) 33 | throws IOException, ServletException { 34 | this.resolver.resolveException(request, response, null, exception); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/ProblemAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.springframework.security.core.AuthenticationException; 6 | import org.springframework.security.web.AuthenticationEntryPoint; 7 | import org.springframework.security.web.access.AccessDeniedHandler; 8 | import org.springframework.web.servlet.HandlerExceptionResolver; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 10 | 11 | /** 12 | * A compound {@link AuthenticationEntryPoint} and {@link AccessDeniedHandler} that delegates 13 | * exceptions to Spring WebMVC's {@link HandlerExceptionResolver} as defined in {@link 14 | * WebMvcConfigurationSupport}. 15 | * 16 | *

Compatible with spring-webmvc 4.3.3. 17 | */ 18 | public class ProblemAuthenticationEntryPoint implements AuthenticationEntryPoint { 19 | 20 | private final HandlerExceptionResolver resolver; 21 | 22 | public ProblemAuthenticationEntryPoint(final HandlerExceptionResolver resolver) { 23 | this.resolver = resolver; 24 | } 25 | 26 | @Override 27 | public void commence( 28 | final HttpServletRequest request, 29 | final HttpServletResponse response, 30 | final AuthenticationException exception) { 31 | this.resolver.resolveException(request, response, null, exception); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/ProblemServerAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.ksoot.problem.spring.advice.webflux.SpringWebfluxProblemResponseUtils; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ProblemDetail; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.security.access.AccessDeniedException; 9 | import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; 10 | import org.springframework.web.server.ServerWebExchange; 11 | import reactor.core.publisher.Mono; 12 | 13 | @RequiredArgsConstructor 14 | public class ProblemServerAccessDeniedHandler implements ServerAccessDeniedHandler { 15 | 16 | private final SecurityAdviceTraits>> advice; 17 | 18 | private final ObjectMapper objectMapper; 19 | 20 | @Override 21 | public Mono handle( 22 | final ServerWebExchange exchange, final AccessDeniedException exception) { 23 | return this.advice 24 | .handleAccessDeniedException(exception, exchange) 25 | .flatMap( 26 | entity -> 27 | SpringWebfluxProblemResponseUtils.writeResponse( 28 | entity, exchange, this.objectMapper)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/ProblemServerAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.ksoot.problem.spring.advice.webflux.SpringWebfluxProblemResponseUtils; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ProblemDetail; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.web.server.ServerAuthenticationEntryPoint; 10 | import org.springframework.web.server.ServerWebExchange; 11 | import reactor.core.publisher.Mono; 12 | 13 | @RequiredArgsConstructor 14 | public class ProblemServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { 15 | 16 | private final SecurityAdviceTraits>> advice; 17 | 18 | private final ObjectMapper objectMapper; 19 | 20 | @Override 21 | public Mono commence( 22 | final ServerWebExchange exchange, final AuthenticationException exception) { 23 | return this.advice 24 | .handleAuthenticationException(exception, exchange) 25 | .flatMap( 26 | entity -> 27 | SpringWebfluxProblemResponseUtils.writeResponse( 28 | entity, exchange, this.objectMapper)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/security/SecurityAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.security; 2 | 3 | public interface SecurityAdviceTraits 4 | extends AuthenticationAdviceTrait, 5 | InsufficientAuthenticationAdviceTrait, 6 | AccessDeniedAdviceTrait {} 7 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/BaseBindingResultHandlingAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import com.ksoot.problem.core.ProblemConstant; 4 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.validation.ObjectError; 11 | 12 | public interface BaseBindingResultHandlingAdviceTrait 13 | extends BaseValidationAdviceTrait { 14 | 15 | default List handleBindingResult( 16 | final BindingResult bindingResult, final Throwable exception) { 17 | 18 | final Stream fieldErrors = 19 | bindingResult.getFieldErrors().stream() 20 | .map(fieldError -> handleFieldError(fieldError, exception)); 21 | 22 | final Stream globalErrors = 23 | bindingResult.getGlobalErrors().stream() 24 | .map(objectError -> handleObjectError(objectError, exception)); 25 | 26 | return Stream.concat(fieldErrors, globalErrors).toList(); 27 | } 28 | 29 | default ViolationVM handleFieldError(final FieldError fieldError, final Throwable exception) { 30 | HttpStatus status = defaultConstraintViolationStatus(); 31 | ProblemMessageSourceResolver codeResolver = 32 | ProblemMessageSourceResolver.of( 33 | ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX, fieldError, status.value()); 34 | ProblemMessageSourceResolver messageResolver = 35 | ProblemMessageSourceResolver.of( 36 | ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, fieldError); 37 | return createViolation(codeResolver, messageResolver, fieldError.getField()); 38 | } 39 | 40 | default ViolationVM handleObjectError(final ObjectError objectError, final Throwable exception) { 41 | HttpStatus status = defaultConstraintViolationStatus(); 42 | ProblemMessageSourceResolver codeResolver = 43 | ProblemMessageSourceResolver.of( 44 | ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX, objectError, status.value()); 45 | ProblemMessageSourceResolver messageResolver = 46 | ProblemMessageSourceResolver.of( 47 | ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, objectError); 48 | return createViolation(codeResolver, messageResolver, objectError.getObjectName()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/BaseValidationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | import com.ksoot.problem.spring.config.ProblemBeanRegistry; 5 | import com.ksoot.problem.spring.config.ProblemMessageProvider; 6 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 7 | import org.springframework.http.HttpStatus; 8 | 9 | public interface BaseValidationAdviceTrait extends AdviceTrait { 10 | 11 | default HttpStatus defaultConstraintViolationStatus() { 12 | return HttpStatus.BAD_REQUEST; 13 | } 14 | 15 | default ViolationVM createViolation( 16 | final ProblemMessageSourceResolver codeResolver, 17 | final ProblemMessageSourceResolver messageResolver, 18 | final String propertyPath) { 19 | if (ProblemBeanRegistry.problemProperties().isDebugEnabled()) { 20 | return ViolationVM.of( 21 | ProblemMessageProvider.getMessage(codeResolver), 22 | ProblemMessageProvider.getMessage(messageResolver), 23 | propertyPath, 24 | codeResolver, 25 | messageResolver); 26 | } else { 27 | return ViolationVM.of( 28 | ProblemMessageProvider.getMessage(codeResolver), 29 | ProblemMessageProvider.getMessage(messageResolver), 30 | propertyPath); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/BindAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX; 4 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX; 5 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX; 6 | import static com.ksoot.problem.core.ProblemConstant.VIOLATIONS_KEY; 7 | 8 | import com.ksoot.problem.core.Problem; 9 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 10 | import java.util.LinkedHashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import org.springframework.validation.BindException; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | 16 | /** 17 | * @see BindException 18 | * @see ViolationVM 19 | * @see BaseValidationAdviceTrait#defaultConstraintViolationStatus() 20 | */ 21 | public interface BindAdviceTrait extends BaseBindingResultHandlingAdviceTrait { 22 | 23 | @ExceptionHandler 24 | default R handleBindException(final BindException exception, final T request) { 25 | List violations = handleBindingResult(exception.getBindingResult(), exception); 26 | Map parameters = new LinkedHashMap<>(4); 27 | parameters.put(VIOLATIONS_KEY, violations); 28 | Problem problem = 29 | toProblem( 30 | exception, 31 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_CODE_CODE_PREFIX), 32 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX), 33 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX), 34 | parameters); 35 | return toResponse(exception, request, defaultConstraintViolationStatus(), problem); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/ConstraintViolationAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX; 4 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX; 5 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX; 6 | import static com.ksoot.problem.core.ProblemConstant.VIOLATIONS_KEY; 7 | 8 | import com.ksoot.problem.core.Problem; 9 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 10 | import jakarta.validation.ConstraintViolation; 11 | import jakarta.validation.ConstraintViolationException; 12 | import java.util.LinkedHashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.web.bind.annotation.ExceptionHandler; 17 | 18 | /** 19 | * @see ConstraintViolationException 20 | * @see BaseValidationAdviceTrait#defaultConstraintViolationStatus() 21 | */ 22 | public interface ConstraintViolationAdviceTrait extends BaseValidationAdviceTrait { 23 | 24 | @ExceptionHandler 25 | default R handleConstraintViolationException( 26 | final ConstraintViolationException exception, final T request) { 27 | final List violations = 28 | exception.getConstraintViolations().stream() 29 | .map(violation -> handleConstraintViolation(violation, exception)) 30 | .toList(); 31 | Map parameters = new LinkedHashMap<>(4); 32 | parameters.put(VIOLATIONS_KEY, violations); 33 | Problem problem = 34 | toProblem( 35 | exception, 36 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_CODE_CODE_PREFIX), 37 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX), 38 | ProblemMessageSourceResolver.of( 39 | CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, exception.getMessage()), 40 | parameters); 41 | return toResponse(exception, request, defaultConstraintViolationStatus(), problem); 42 | } 43 | 44 | @SuppressWarnings("rawtypes") 45 | default ViolationVM handleConstraintViolation( 46 | final ConstraintViolation violation, final ConstraintViolationException exception) { 47 | HttpStatus status = defaultConstraintViolationStatus(); 48 | ProblemMessageSourceResolver codeResolver = 49 | ProblemMessageSourceResolver.of( 50 | CONSTRAINT_VIOLATION_CODE_CODE_PREFIX, violation, status.name()); 51 | ProblemMessageSourceResolver messageResolver = 52 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, violation); 53 | return createViolation(codeResolver, messageResolver, violation.getPropertyPath().toString()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/MethodArgumentNotValidAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX; 4 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX; 5 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX; 6 | import static com.ksoot.problem.core.ProblemConstant.VIOLATIONS_KEY; 7 | 8 | import com.ksoot.problem.core.Problem; 9 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 10 | import java.util.LinkedHashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import org.springframework.web.bind.MethodArgumentNotValidException; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | 16 | /** 17 | * @see MethodArgumentNotValidException 18 | * @see BaseValidationAdviceTrait#defaultConstraintViolationStatus() 19 | */ 20 | public interface MethodArgumentNotValidAdviceTrait 21 | extends BaseBindingResultHandlingAdviceTrait { 22 | 23 | @ExceptionHandler 24 | default R handleMethodArgumentNotValid( 25 | final MethodArgumentNotValidException exception, final T request) { 26 | List violations = handleBindingResult(exception.getBindingResult(), exception); 27 | Map parameters = new LinkedHashMap<>(4); 28 | parameters.put(VIOLATIONS_KEY, violations); 29 | Problem problem = 30 | toProblem( 31 | exception, 32 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_CODE_CODE_PREFIX), 33 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX), 34 | ProblemMessageSourceResolver.of( 35 | CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, exception.getMessage()), 36 | parameters); 37 | return toResponse(exception, request, defaultConstraintViolationStatus(), problem); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/MethodArgumentTypeMismatchAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.PROPERTY_PATH_KEY; 4 | 5 | import com.ksoot.problem.core.GeneralErrorKey; 6 | import com.ksoot.problem.core.Problem; 7 | import com.ksoot.problem.core.ProblemConstant; 8 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 14 | 15 | public interface MethodArgumentTypeMismatchAdviceTrait 16 | extends BaseValidationAdviceTrait { 17 | 18 | @ExceptionHandler 19 | default R handleMethodArgumentTypeMismatch( 20 | final MethodArgumentTypeMismatchException exception, final T request) { 21 | 22 | HttpStatus status = defaultConstraintViolationStatus(); 23 | String parameterName = exception.getParameter().getParameterName(); 24 | 25 | String errorKey = 26 | exception.getParameter().getContainingClass().getSimpleName() 27 | + ProblemConstant.DOT 28 | + exception.getParameter().getMethod().getName() 29 | + ProblemConstant.DOT 30 | + parameterName; 31 | 32 | String codeCode = 33 | ProblemConstant.CODE_CODE_PREFIX 34 | + GeneralErrorKey.TYPE_MISMATCH 35 | + ProblemConstant.DOT 36 | + errorKey; 37 | String titleCode = 38 | ProblemConstant.TITLE_CODE_PREFIX 39 | + GeneralErrorKey.TYPE_MISMATCH 40 | + ProblemConstant.DOT 41 | + errorKey; 42 | String detailCode = 43 | ProblemConstant.DETAIL_CODE_PREFIX 44 | + GeneralErrorKey.TYPE_MISMATCH 45 | + ProblemConstant.DOT 46 | + errorKey; 47 | 48 | Map parameters = new LinkedHashMap<>(4); 49 | parameters.put(PROPERTY_PATH_KEY, parameterName); 50 | 51 | Problem problem = 52 | toProblem( 53 | exception, 54 | ProblemMessageSourceResolver.of(codeCode, status.value()), 55 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 56 | ProblemMessageSourceResolver.of( 57 | detailCode, exception.getMostSpecificCause().toString()), 58 | parameters); 59 | 60 | return toResponse(exception, request, status, problem); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/TypeMismatchAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.PROPERTY_PATH_KEY; 4 | 5 | import com.ksoot.problem.core.GeneralErrorKey; 6 | import com.ksoot.problem.core.Problem; 7 | import com.ksoot.problem.core.ProblemConstant; 8 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import org.springframework.beans.TypeMismatchException; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | 15 | /** 16 | * @see TypeMismatchException 17 | * @see HttpStatus#BAD_REQUEST 18 | */ 19 | public interface TypeMismatchAdviceTrait extends BaseValidationAdviceTrait { 20 | 21 | @ExceptionHandler 22 | default R handleTypeMismatch(final TypeMismatchException exception, final T request) { 23 | 24 | HttpStatus status = defaultConstraintViolationStatus(); 25 | String propertyName = exception.getPropertyName(); 26 | 27 | String errorKey = exception.getErrorCode() + ProblemConstant.DOT + propertyName; 28 | 29 | String codeCode = 30 | ProblemConstant.CODE_CODE_PREFIX 31 | + GeneralErrorKey.TYPE_MISMATCH 32 | + ProblemConstant.DOT 33 | + errorKey; 34 | String titleCode = 35 | ProblemConstant.TITLE_CODE_PREFIX 36 | + GeneralErrorKey.TYPE_MISMATCH 37 | + ProblemConstant.DOT 38 | + errorKey; 39 | String detailCode = 40 | ProblemConstant.DETAIL_CODE_PREFIX 41 | + GeneralErrorKey.TYPE_MISMATCH 42 | + ProblemConstant.DOT 43 | + errorKey; 44 | 45 | Map parameters = new LinkedHashMap<>(4); 46 | parameters.put(PROPERTY_PATH_KEY, propertyName); 47 | 48 | Problem problem = 49 | toProblem( 50 | exception, 51 | ProblemMessageSourceResolver.of(codeCode, status.value()), 52 | ProblemMessageSourceResolver.of(titleCode, status.getReasonPhrase()), 53 | ProblemMessageSourceResolver.of( 54 | detailCode, exception.getMostSpecificCause().toString()), 55 | parameters); 56 | 57 | return toResponse(exception, request, status, problem); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/ValidationAdviceTraits.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | /** 4 | * Advice trait to handle any validation exceptions. 5 | * 6 | *

Be careful if you use {@link 7 | * org.springframework.validation.beanvalidation.MethodValidationPostProcessor} in order to validate 8 | * method parameter field directly but {@code violations[].field} value looks like {@code arg0} 9 | * instead of parameter name, you have to configure a {@link 10 | * org.springframework.validation.beanvalidation.LocalValidatorFactoryBean} with your {@link 11 | * org.springframework.validation.beanvalidation.MethodValidationPostProcessor} like following: 12 | * 13 | *


14 |  * {@literal @}Bean
15 |  *  public Validator validator() {
16 |  *      return new LocalValidatorFactoryBean();
17 |  *  }
18 |  *
19 |  * {@literal @}Bean
20 |  *  public MethodValidationPostProcessor methodValidationPostProcessor() {
21 |  *      MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
22 |  *      methodValidationPostProcessor.setValidator(validator());
23 |  *      return methodValidationPostProcessor;
24 |  *  }
25 |  * 
26 | */ 27 | public interface ValidationAdviceTraits 28 | extends ConstraintViolationAdviceTrait, 29 | BindAdviceTrait, 30 | MethodArgumentNotValidAdviceTrait, 31 | MethodArgumentTypeMismatchAdviceTrait, 32 | TypeMismatchAdviceTrait {} 33 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/validation/ViolationVM.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.validation; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CODE_RESOLVER; 4 | import static com.ksoot.problem.core.ProblemConstant.DETAIL_RESOLVER; 5 | 6 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 7 | import com.fasterxml.jackson.annotation.JsonInclude; 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import org.springframework.context.MessageSourceResolvable; 13 | 14 | @Getter 15 | @NoArgsConstructor 16 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 17 | public class ViolationVM { 18 | 19 | private String code; 20 | 21 | private String detail; 22 | 23 | private String propertyPath; 24 | 25 | private Map parameters; 26 | 27 | public static ViolationVM of(final String detail, final String propertyPath) { 28 | ViolationVM violationVM = new ViolationVM(); 29 | violationVM.detail = detail; 30 | violationVM.propertyPath = propertyPath; 31 | return violationVM; 32 | } 33 | 34 | public static ViolationVM of(final String code, final String detail, final String propertyPath) { 35 | ViolationVM violationVM = new ViolationVM(); 36 | violationVM.code = code; 37 | violationVM.detail = detail; 38 | violationVM.propertyPath = propertyPath; 39 | return violationVM; 40 | } 41 | 42 | public static ViolationVM of( 43 | final String code, 44 | final String detail, 45 | final String propertyPath, 46 | MessageSourceResolvable codeResolver, 47 | MessageSourceResolvable messageResolver) { 48 | ViolationVM violationVM = new ViolationVM(); 49 | violationVM.code = code; 50 | violationVM.detail = detail; 51 | violationVM.propertyPath = propertyPath; 52 | 53 | Map parameters = new LinkedHashMap<>(2); 54 | parameters.put(CODE_RESOLVER, codeResolver); 55 | parameters.put(DETAIL_RESOLVER, messageResolver); 56 | violationVM.parameters = parameters; 57 | 58 | return violationVM; 59 | } 60 | 61 | public static ViolationVM of( 62 | final String code, 63 | final String message, 64 | final String propertyPath, 65 | MessageSourceResolvable messageResolver) { 66 | ViolationVM violationVM = new ViolationVM(); 67 | violationVM.code = code; 68 | violationVM.detail = message; 69 | violationVM.propertyPath = propertyPath; 70 | 71 | Map parameters = new LinkedHashMap<>(1); 72 | parameters.put(DETAIL_RESOLVER, messageResolver); 73 | violationVM.parameters = parameters; 74 | 75 | return violationVM; 76 | } 77 | 78 | @JsonAnyGetter 79 | public Map getParameters() { 80 | return this.parameters; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/web/ProblemHandlingWeb.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.web; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | import com.ksoot.problem.spring.advice.application.ApplicationAdviceTraits; 5 | import com.ksoot.problem.spring.advice.general.GeneralAdviceTraits; 6 | import com.ksoot.problem.spring.advice.http.HttpAdviceTraits; 7 | import com.ksoot.problem.spring.advice.io.IOAdviceTraits; 8 | import com.ksoot.problem.spring.advice.network.NetworkAdviceTraits; 9 | import com.ksoot.problem.spring.advice.routing.RoutingAdviceTraits; 10 | import com.ksoot.problem.spring.advice.validation.ValidationAdviceTraits; 11 | import org.springframework.web.context.request.NativeWebRequest; 12 | 13 | /** 14 | * {@link ProblemHandlingWeb} is a composite {@link AdviceTrait} that combines all general built-in 15 | * advice traits into a single interface that makes it easier to use: 16 | * 17 | *

18 |  * {@literal @}ControllerAdvice
19 |  *  public class ExceptionHandling implements ProblemHandlingWeb
20 |  * 
21 | * 22 | * Note: Future versions of this class will be extended with additional traits. 23 | * 24 | * @see GeneralAdviceTraits 25 | * @see HttpAdviceTraits 26 | * @see IOAdviceTraits 27 | * @see NetworkAdviceTraits 28 | * @see RoutingAdviceTraits 29 | * @see ValidationAdviceTraits 30 | * @see ApplicationAdviceTraits 31 | */ 32 | public interface ProblemHandlingWeb 33 | extends GeneralAdviceTraits, 34 | HttpAdviceTraits, 35 | IOAdviceTraits, 36 | // NetworkAdviceTraits, 37 | RoutingAdviceTraits, 38 | ValidationAdviceTraits, 39 | ApplicationAdviceTraits {} 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/webflux/ProblemHandlingWebflux.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.webflux; 2 | 3 | import com.ksoot.problem.spring.advice.AdviceTrait; 4 | import com.ksoot.problem.spring.advice.application.ApplicationAdviceTraits; 5 | import com.ksoot.problem.spring.advice.general.GeneralAdviceTraits; 6 | import com.ksoot.problem.spring.advice.http.HttpAdviceTraits; 7 | import com.ksoot.problem.spring.advice.io.IOAdviceTraits; 8 | import com.ksoot.problem.spring.advice.network.NetworkAdviceTraits; 9 | import com.ksoot.problem.spring.advice.routing.RoutingAdviceTraits; 10 | import com.ksoot.problem.spring.advice.validation.ValidationAdviceTraits; 11 | import org.springframework.web.server.ServerWebExchange; 12 | 13 | /** 14 | * {@link ProblemHandlingWebflux} is a composite {@link AdviceTrait} that combines all built-in 15 | * advice traits into a single interface that makes it easier to use: 16 | * 17 | *

18 |  * {@literal @}ControllerAdvice
19 |  *  public class ExceptionHandling implements ProblemHandlingWebflux
20 |  * 
21 | * 22 | * Note: Future versions of this class will be extended with additional traits. 23 | * 24 | * @see GeneralAdviceTraits 25 | * @see HttpAdviceTraits 26 | * @see IOAdviceTraits 27 | * @see NetworkAdviceTraits 28 | * @see RoutingAdviceTraits 29 | * @see ValidationAdviceTraits 30 | * @see ApplicationAdviceTraits 31 | */ 32 | public interface ProblemHandlingWebflux 33 | extends GeneralAdviceTraits, 34 | HttpAdviceTraits, 35 | IOAdviceTraits, 36 | // NetworkAdviceTraits, 37 | ValidationAdviceTraits, 38 | WebExchangeBindAdviceTrait, 39 | ApplicationAdviceTraits {} 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/webflux/SpringWebfluxProblemResponseUtils.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.webflux; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.core.io.buffer.DataBuffer; 6 | import org.springframework.core.io.buffer.DataBufferUtils; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.http.server.reactive.ServerHttpResponse; 9 | import org.springframework.web.server.ServerWebExchange; 10 | import reactor.core.publisher.Mono; 11 | 12 | public final class SpringWebfluxProblemResponseUtils { 13 | 14 | private SpringWebfluxProblemResponseUtils() { 15 | throw new IllegalStateException("Just a utility class, not supposed to be instantiated"); 16 | } 17 | 18 | public static Mono writeResponse( 19 | final ResponseEntity entity, final ServerWebExchange exchange, final ObjectMapper mapper) { 20 | final ServerHttpResponse response = exchange.getResponse(); 21 | response.setStatusCode(entity.getStatusCode()); 22 | response.getHeaders().addAll(entity.getHeaders()); 23 | try { 24 | final DataBuffer buffer = 25 | response.bufferFactory().wrap(mapper.writeValueAsBytes(entity.getBody())); 26 | return response 27 | .writeWith(Mono.just(buffer)) 28 | .doOnError(error -> DataBufferUtils.release(buffer)); 29 | } catch (final JsonProcessingException ex) { 30 | return Mono.error(ex); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/advice/webflux/WebExchangeBindAdviceTrait.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.advice.webflux; 2 | 3 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_CODE_CODE_PREFIX; 4 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX; 5 | import static com.ksoot.problem.core.ProblemConstant.CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX; 6 | import static com.ksoot.problem.core.ProblemConstant.VIOLATIONS_KEY; 7 | 8 | import com.ksoot.problem.core.Problem; 9 | import com.ksoot.problem.spring.advice.validation.BaseBindingResultHandlingAdviceTrait; 10 | import com.ksoot.problem.spring.advice.validation.ViolationVM; 11 | import com.ksoot.problem.spring.config.ProblemMessageSourceResolver; 12 | import java.util.LinkedHashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import org.springframework.web.bind.annotation.ExceptionHandler; 16 | import org.springframework.web.bind.support.WebExchangeBindException; 17 | 18 | interface WebExchangeBindAdviceTrait extends BaseBindingResultHandlingAdviceTrait { 19 | 20 | @ExceptionHandler 21 | default R handleWebExchangeBindException( 22 | final WebExchangeBindException exception, final T request) { 23 | final List violations = 24 | handleBindingResult(exception.getBindingResult(), exception); 25 | Map parameters = new LinkedHashMap<>(4); 26 | parameters.put(VIOLATIONS_KEY, violations); 27 | Problem problem = 28 | toProblem( 29 | exception, 30 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_CODE_CODE_PREFIX), 31 | ProblemMessageSourceResolver.of(CONSTRAINT_VIOLATION_TITLE_CODE_PREFIX), 32 | ProblemMessageSourceResolver.of( 33 | CONSTRAINT_VIOLATION_DETAIL_CODE_PREFIX, exception.getMessage()), 34 | parameters); 35 | return toResponse(exception, request, defaultConstraintViolationStatus(), problem); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/DaoAdviceEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.context.annotation.Conditional; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.mongodb.MongoDatabaseFactory; 9 | 10 | /** Condition to register advice for DAO exception handling. */ 11 | public class DaoAdviceEnabled extends AnyNestedCondition { 12 | 13 | DaoAdviceEnabled() { 14 | super(ConfigurationPhase.PARSE_CONFIGURATION); 15 | } 16 | 17 | @Conditional(ORMUrlAvailable.class) 18 | @ConditionalOnClass(value = {JpaRepository.class}) 19 | static class ORMAvailable {} 20 | 21 | @ConditionalOnClass(value = {MongoDatabaseFactory.class}) 22 | @ConditionalOnProperty(prefix = "spring.data.mongodb", name = "uri") 23 | static class MongoAvailable {} 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/ORMAdviceEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AllNestedConditions; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | 6 | /** Condition to register advice for ORM exception handling. */ 7 | public class ORMAdviceEnabled extends AllNestedConditions { 8 | 9 | public ORMAdviceEnabled() { 10 | super(ConfigurationPhase.PARSE_CONFIGURATION); 11 | } 12 | 13 | @ConditionalOnProperty( 14 | prefix = "problem", 15 | name = "enabled", 16 | havingValue = "true", 17 | matchIfMissing = true) 18 | static class ProblemEnabled {} 19 | 20 | @ConditionalOnProperty( 21 | prefix = "problem", 22 | name = "dao-advice-enabled", 23 | havingValue = "true", 24 | matchIfMissing = true) 25 | static class ORMEnabledEnabled {} 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/ORMUrlAvailable.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | 6 | /** Condition to register advice for Dao exception handling. */ 7 | public class ORMUrlAvailable extends AnyNestedCondition { 8 | 9 | public ORMUrlAvailable() { 10 | super(ConfigurationPhase.PARSE_CONFIGURATION); 11 | } 12 | 13 | @ConditionalOnProperty(prefix = "spring.datasource", name = "url") 14 | static class SpringDatasourceUrlAvailable {} 15 | 16 | @ConditionalOnProperty(prefix = "spring.r2dbc", name = "url") 17 | static class SpringR2dbcUrlAvailable {} 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/OpenAPIValidationAdviceEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AllNestedConditions; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.context.annotation.Conditional; 6 | 7 | /** Condition to register advice for OpenAPI exception handling. */ 8 | public class OpenAPIValidationAdviceEnabled extends AllNestedConditions { 9 | 10 | public OpenAPIValidationAdviceEnabled() { 11 | super(ConfigurationPhase.PARSE_CONFIGURATION); 12 | } 13 | 14 | @ConditionalOnProperty( 15 | prefix = "problem", 16 | name = "enabled", 17 | havingValue = "true", 18 | matchIfMissing = true) 19 | static class ProblemEnabled {} 20 | 21 | @ConditionalOnProperty(prefix = "problem.open-api", name = "path") 22 | static class OpenAPISpecAvailable {} 23 | 24 | @Conditional(OpenApiConfigsEnabled.class) 25 | static class OpenApiPropertiesEnabled {} 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/OpenApiConfigsEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | 6 | /** Condition to register advice for OpenAPI exception handling. */ 7 | public class OpenApiConfigsEnabled extends AnyNestedCondition { 8 | 9 | public OpenApiConfigsEnabled() { 10 | super(ConfigurationPhase.PARSE_CONFIGURATION); 11 | } 12 | 13 | @ConditionalOnProperty( 14 | prefix = "problem.open-api", 15 | name = "req-validation-enabled", 16 | havingValue = "true") 17 | static class ReqValidationEnabled {} 18 | 19 | @ConditionalOnProperty( 20 | prefix = "problem.open-api", 21 | name = "res-validation-enabled", 22 | havingValue = "true") 23 | static class ResValidationEnabled {} 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/ProblemDaoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import com.ksoot.problem.spring.advice.dao.ConstraintNameResolver; 4 | import com.ksoot.problem.spring.advice.dao.MongoConstraintNameResolver; 5 | import com.ksoot.problem.spring.advice.dao.PostgresConstraintNameResolver; 6 | import com.ksoot.problem.spring.advice.dao.SQLServerConstraintNameResolver; 7 | import com.ksoot.problem.spring.config.ProblemProperties; 8 | import org.springframework.boot.autoconfigure.AutoConfiguration; 9 | import org.springframework.boot.autoconfigure.AutoConfigureOrder; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Conditional; 17 | import org.springframework.core.Ordered; 18 | import org.springframework.core.annotation.Order; 19 | import org.springframework.core.env.Environment; 20 | import org.springframework.data.mongodb.MongoDatabaseFactory; 21 | 22 | @EnableConfigurationProperties(ProblemProperties.class) 23 | @Conditional(value = {DaoAdviceEnabled.class, ORMAdviceEnabled.class}) 24 | @ConditionalOnWebApplication 25 | @AutoConfiguration 26 | @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 27 | @Order(value = Ordered.HIGHEST_PRECEDENCE) 28 | class ProblemDaoConfiguration { 29 | 30 | @ConditionalOnMissingBean(name = "postgresqlConstraintNameResolver") 31 | @Conditional(ORMUrlAvailable.class) 32 | @ConditionalOnProperty(prefix = "spring.jpa", name = "database", havingValue = "POSTGRESQL") 33 | static class PostgresqlConstraintNameResolverConfiguration { 34 | 35 | @Bean 36 | ConstraintNameResolver postgresqlConstraintNameResolver(final Environment env) { 37 | return new PostgresConstraintNameResolver(); 38 | } 39 | } 40 | 41 | @ConditionalOnMissingBean(name = "sqlServerConstraintNameResolver") 42 | @Conditional(ORMUrlAvailable.class) 43 | @ConditionalOnProperty(prefix = "spring.jpa", name = "database", havingValue = "SQL_SERVER") 44 | static class SQLServerConstraintNameResolverConfiguration { 45 | 46 | @Bean 47 | ConstraintNameResolver sqlServerConstraintNameResolver(final Environment env) { 48 | return new SQLServerConstraintNameResolver(); 49 | } 50 | } 51 | 52 | @ConditionalOnClass(value = {MongoDatabaseFactory.class}) 53 | @ConditionalOnProperty(prefix = "spring.data.mongodb", name = "uri") 54 | @ConditionalOnMissingBean(name = "mongoConstraintNameResolver") 55 | static class MongoConstraintNameResolverConfiguration { 56 | 57 | @Bean 58 | ConstraintNameResolver mongoConstraintNameResolver(final Environment env) { 59 | return new MongoConstraintNameResolver(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/ProblemJacksonConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import com.ksoot.problem.jackson.ProblemModule; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.AutoConfigureOrder; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Conditional; 10 | import org.springframework.core.Ordered; 11 | import org.springframework.core.annotation.Order; 12 | 13 | @EnableConfigurationProperties(ProblemProperties.class) 14 | @Conditional(ProblemJacksonEnabled.class) 15 | @AutoConfiguration 16 | @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 17 | @Order(value = Ordered.HIGHEST_PRECEDENCE) 18 | public class ProblemJacksonConfiguration { 19 | 20 | @Bean 21 | ProblemModule problemModule() { 22 | return new ProblemModule(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/ProblemJacksonEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import com.fasterxml.jackson.databind.Module; 4 | import org.springframework.boot.autoconfigure.condition.AllNestedConditions; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | 8 | /** Condition to register Jackson Problem Module. */ 9 | public class ProblemJacksonEnabled extends AllNestedConditions { 10 | 11 | public ProblemJacksonEnabled() { 12 | super(ConfigurationPhase.PARSE_CONFIGURATION); 13 | } 14 | 15 | @ConditionalOnClass(Module.class) 16 | static class JacksonAvailable {} 17 | 18 | @ConditionalOnProperty( 19 | prefix = "problem", 20 | name = "enabled", 21 | havingValue = "true", 22 | matchIfMissing = true) 23 | static class ProblemEnabled {} 24 | 25 | @ConditionalOnProperty( 26 | prefix = "problem", 27 | name = "jackson-module-enabled", 28 | havingValue = "true", 29 | matchIfMissing = true) 30 | static class JacksonModuleEnabled {} 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/SecurityAdviceEnabled.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure; 2 | 3 | import org.springframework.boot.autoconfigure.condition.AllNestedConditions; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | 6 | /** Condition to register advice for Security exception handling. */ 7 | public class SecurityAdviceEnabled extends AllNestedConditions { 8 | 9 | public SecurityAdviceEnabled() { 10 | super(ConfigurationPhase.PARSE_CONFIGURATION); 11 | } 12 | 13 | @ConditionalOnProperty( 14 | prefix = "problem", 15 | name = "enabled", 16 | havingValue = "true", 17 | matchIfMissing = true) 18 | static class ProblemEnabled {} 19 | 20 | @ConditionalOnProperty( 21 | prefix = "problem", 22 | name = "security-advice-enabled", 23 | havingValue = "true", 24 | matchIfMissing = true) 25 | static class SecurityEnabled {} 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/OpenApiValidationExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.atlassian.oai.validator.OpenApiInteractionValidator; 4 | import com.atlassian.oai.validator.springmvc.OpenApiValidationInterceptor; 5 | import com.atlassian.oai.validator.springmvc.ValidationReportHandler; 6 | import com.ksoot.problem.spring.advice.validation.OpenApiValidationAdviceTrait; 7 | import com.ksoot.problem.spring.boot.autoconfigure.OpenAPIValidationAdviceEnabled; 8 | import com.ksoot.problem.spring.config.ProblemConfigException; 9 | import com.ksoot.problem.spring.config.ProblemProperties; 10 | import jakarta.servlet.Filter; 11 | import lombok.RequiredArgsConstructor; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 15 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Conditional; 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.core.Ordered; 20 | import org.springframework.core.annotation.Order; 21 | import org.springframework.http.ProblemDetail; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.bind.annotation.ControllerAdvice; 24 | import org.springframework.web.context.request.NativeWebRequest; 25 | 26 | @Configuration(proxyBeanMethods = false) 27 | @EnableConfigurationProperties(value = {ProblemProperties.class}) 28 | @ConditionalOnClass(ValidationReportHandler.class) 29 | @Conditional(OpenAPIValidationAdviceEnabled.class) 30 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 31 | @Order(Ordered.HIGHEST_PRECEDENCE) 32 | @ControllerAdvice 33 | @RequiredArgsConstructor 34 | public class OpenApiValidationExceptionHandler 35 | implements OpenApiValidationAdviceTrait> { 36 | 37 | private final ProblemProperties problemProperties; 38 | 39 | @Bean 40 | public OpenApiValidationInterceptor validationInterceptor() { 41 | if (StringUtils.isBlank(this.problemProperties.getOpenApi().getPath())) { 42 | throw new ProblemConfigException( 43 | "Invalid OpenAPI Spec file: " + this.problemProperties.getOpenApi().getPath()); 44 | } 45 | OpenApiInteractionValidator validator = 46 | OpenApiInteractionValidator.createFor(this.problemProperties.getOpenApi().getPath()) 47 | .build(); 48 | return new OpenApiValidationInterceptor(validator); 49 | } 50 | 51 | @Bean 52 | public Filter validationFilter() { 53 | return new PathConfigurableOpenApiValidationFilter(this.problemProperties.getOpenApi()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/PathConfigurableOpenApiValidationFilter.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.atlassian.oai.validator.springmvc.OpenApiValidationFilter; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import java.util.List; 7 | import org.apache.commons.collections4.CollectionUtils; 8 | import org.springframework.util.AntPathMatcher; 9 | 10 | public class PathConfigurableOpenApiValidationFilter extends OpenApiValidationFilter { 11 | 12 | private final AntPathMatcher pathMatcher; 13 | 14 | private final String openApiLocation; 15 | 16 | private final List excludePatterns; 17 | 18 | public PathConfigurableOpenApiValidationFilter( 19 | final ProblemProperties.OpenApi openApiProperties) { 20 | super(openApiProperties.isReqValidationEnabled(), openApiProperties.isResValidationEnabled()); 21 | this.openApiLocation = openApiProperties.getPath(); 22 | this.pathMatcher = new AntPathMatcher(); 23 | this.excludePatterns = openApiProperties.getExcludePatterns(); 24 | } 25 | 26 | @Override 27 | protected boolean shouldNotFilter(final HttpServletRequest request) { 28 | String requestPath = request.getRequestURI(); 29 | boolean excludedPath = 30 | CollectionUtils.isNotEmpty(this.excludePatterns) 31 | ? this.excludePatterns.stream() 32 | .anyMatch(pattern -> pathMatcher.match(pattern, requestPath)) 33 | : false; 34 | return this.pathMatcher.match("/**/v3/api-docs", requestPath) 35 | || this.pathMatcher.match("/v3/api-docs", requestPath) 36 | || this.pathMatcher.match("/v3/api-docs/*", requestPath) 37 | || this.pathMatcher.match("/swagger-ui.html", requestPath) 38 | || this.pathMatcher.match("/**/swagger-ui.html", requestPath) 39 | || this.pathMatcher.match("/swagger-ui/*", requestPath) 40 | || this.pathMatcher.match("/**/swagger-ui/*", requestPath) 41 | || this.pathMatcher.match("/**" + this.openApiLocation, requestPath) 42 | || this.pathMatcher.match(this.openApiLocation, requestPath) 43 | || excludedPath; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/ProblemWebAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.ksoot.problem.core.ErrorResponseBuilder; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import org.springframework.boot.autoconfigure.AutoConfigureOrder; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 9 | import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.http.ProblemDetail; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.context.request.NativeWebRequest; 18 | 19 | /** Registers Problem Jackson modules when {@link WebMvcAutoConfiguration} is enabled. */ 20 | @EnableConfigurationProperties(ProblemProperties.class) 21 | @ConditionalOnProperty( 22 | prefix = "problem", 23 | name = "enabled", 24 | havingValue = "true", 25 | matchIfMissing = true) 26 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 27 | @Configuration(proxyBeanMethods = false) 28 | @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 29 | @Order(value = Ordered.HIGHEST_PRECEDENCE) 30 | public class ProblemWebAutoConfiguration { 31 | 32 | @Bean 33 | @ConditionalOnMissingBean(ErrorResponseBuilder.class) 34 | ErrorResponseBuilder> errorResponseBuilder() { 35 | return new SpringWebErrorResponseBuilder(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/SpringWebErrorResponseBuilder.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import static jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION; 4 | import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST; 5 | 6 | import com.ksoot.problem.core.ErrorResponseBuilder; 7 | import com.ksoot.problem.core.MediaTypes; 8 | import com.ksoot.problem.core.Problem; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import java.net.URI; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import lombok.SneakyThrows; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.http.HttpMethod; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.ProblemDetail; 19 | import org.springframework.http.ResponseEntity; 20 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 21 | import org.springframework.web.accept.ContentNegotiationStrategy; 22 | import org.springframework.web.context.request.NativeWebRequest; 23 | 24 | public class SpringWebErrorResponseBuilder 25 | implements ErrorResponseBuilder> { 26 | 27 | @SneakyThrows(HttpMediaTypeNotAcceptableException.class) 28 | public static Optional negotiate(final NativeWebRequest request) { 29 | final ContentNegotiationStrategy negotiator = DEFAULT_CONTENT_NEGOTIATION_STRATEGY; 30 | final List mediaTypes = negotiator.resolveMediaTypes(request); 31 | return ErrorResponseBuilder.getProblemMediaType(mediaTypes); 32 | } 33 | 34 | @Override 35 | public ResponseEntity buildResponse( 36 | final Throwable throwable, 37 | final NativeWebRequest request, 38 | final HttpStatus status, 39 | final HttpHeaders headers, 40 | final Problem problem) { 41 | if (status == HttpStatus.INTERNAL_SERVER_ERROR) { 42 | request.setAttribute(ERROR_EXCEPTION, throwable, SCOPE_REQUEST); 43 | } 44 | 45 | ProblemDetail problemDetail = createProblemDetail(request, status, problem); 46 | Optional> responseEntity = 47 | negotiate(request) 48 | .map( 49 | contentType -> 50 | ResponseEntity.status(status) 51 | .headers(headers) 52 | .contentType(contentType) 53 | .body(problemDetail)); 54 | 55 | if (responseEntity.isPresent()) { 56 | return postProcess(responseEntity.get(), request); 57 | } else { 58 | return fallback(request, status, headers, problem); 59 | } 60 | } 61 | 62 | private ResponseEntity postProcess( 63 | final ResponseEntity errorResponse, final NativeWebRequest request) { 64 | return errorResponse; 65 | } 66 | 67 | private ResponseEntity fallback( 68 | final NativeWebRequest request, 69 | final HttpStatus status, 70 | final HttpHeaders headers, 71 | final Problem problem) { 72 | ProblemDetail problemDetail = createProblemDetail(request, status, problem); 73 | return ResponseEntity.status(status) 74 | .headers(headers) 75 | .contentType(MediaTypes.PROBLEM) 76 | .body(problemDetail); 77 | } 78 | 79 | @Override 80 | public URI requestUri(final NativeWebRequest request) { 81 | return URI.create(request.getNativeRequest(HttpServletRequest.class).getRequestURI()); 82 | } 83 | 84 | @Override 85 | public HttpMethod requestMethod(final NativeWebRequest request) { 86 | return HttpMethod.valueOf(request.getNativeRequest(HttpServletRequest.class).getMethod()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/WebDaoExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.ksoot.problem.spring.advice.dao.AbstractDaoExceptionHandler; 4 | import com.ksoot.problem.spring.advice.dao.ConstraintNameResolver; 5 | import com.ksoot.problem.spring.boot.autoconfigure.DaoAdviceEnabled; 6 | import com.ksoot.problem.spring.boot.autoconfigure.ORMAdviceEnabled; 7 | import com.ksoot.problem.spring.config.ProblemProperties; 8 | import java.util.List; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Conditional; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.core.env.Environment; 16 | import org.springframework.http.ProblemDetail; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.ControllerAdvice; 19 | import org.springframework.web.context.request.NativeWebRequest; 20 | 21 | /** 22 | * @author Rajveer Singh 23 | */ 24 | @Configuration 25 | @EnableConfigurationProperties(ProblemProperties.class) 26 | @Conditional(value = {DaoAdviceEnabled.class, ORMAdviceEnabled.class}) 27 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 28 | @Order(Ordered.HIGHEST_PRECEDENCE) 29 | @ControllerAdvice 30 | public class WebDaoExceptionHandler 31 | extends AbstractDaoExceptionHandler> { 32 | 33 | WebDaoExceptionHandler( 34 | final List constraintNameResolvers, final Environment env) { 35 | super(constraintNameResolvers, env); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/WebExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.ksoot.problem.spring.advice.web.ProblemHandlingWeb; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.ProblemDetail; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.ControllerAdvice; 12 | 13 | @Configuration(proxyBeanMethods = false) 14 | @EnableConfigurationProperties(ProblemProperties.class) 15 | @ConditionalOnProperty( 16 | prefix = "problem", 17 | name = "enabled", 18 | havingValue = "true", 19 | matchIfMissing = true) 20 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 21 | @ControllerAdvice 22 | public class WebExceptionHandler implements ProblemHandlingWeb> {} 23 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/web/WebSecurityExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.web; 2 | 3 | import com.ksoot.problem.spring.advice.security.ProblemAccessDeniedHandler; 4 | import com.ksoot.problem.spring.advice.security.ProblemAuthenticationEntryPoint; 5 | import com.ksoot.problem.spring.advice.security.SecurityAdviceTraits; 6 | import com.ksoot.problem.spring.boot.autoconfigure.SecurityAdviceEnabled; 7 | import com.ksoot.problem.spring.config.ProblemProperties; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 13 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Conditional; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.core.Ordered; 18 | import org.springframework.core.annotation.Order; 19 | import org.springframework.http.ProblemDetail; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; 22 | import org.springframework.security.web.AuthenticationEntryPoint; 23 | import org.springframework.security.web.access.AccessDeniedHandler; 24 | import org.springframework.web.bind.annotation.ControllerAdvice; 25 | import org.springframework.web.context.request.NativeWebRequest; 26 | import org.springframework.web.servlet.HandlerExceptionResolver; 27 | 28 | @Configuration 29 | @EnableConfigurationProperties({ProblemProperties.class}) 30 | @Conditional(SecurityAdviceEnabled.class) 31 | @ConditionalOnClass(value = {WebSecurityConfiguration.class}) 32 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 33 | @Order(Ordered.HIGHEST_PRECEDENCE) 34 | @ControllerAdvice 35 | @RequiredArgsConstructor 36 | public class WebSecurityExceptionHandler 37 | implements SecurityAdviceTraits> { 38 | 39 | @ConditionalOnMissingBean 40 | @Bean 41 | AuthenticationEntryPoint authenticationEntryPoint( 42 | @Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) { 43 | return new ProblemAuthenticationEntryPoint(resolver); 44 | } 45 | 46 | @ConditionalOnMissingBean 47 | @Bean 48 | AccessDeniedHandler accessDeniedHandler( 49 | @Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) { 50 | return new ProblemAccessDeniedHandler(resolver); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/webflux/ProblemWebfluxAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.webflux; 2 | 3 | import com.ksoot.problem.core.ErrorResponseBuilder; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.AutoConfigureOrder; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 10 | import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; 11 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.http.ProblemDetail; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.server.ServerWebExchange; 18 | import reactor.core.publisher.Mono; 19 | 20 | /** Registers Problem Jackson modules when {@link WebMvcAutoConfiguration} is enabled. */ 21 | @EnableConfigurationProperties(ProblemProperties.class) 22 | @ConditionalOnProperty( 23 | prefix = "problem", 24 | name = "enabled", 25 | havingValue = "true", 26 | matchIfMissing = true) 27 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 28 | @AutoConfiguration 29 | @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 30 | @Order(value = Ordered.HIGHEST_PRECEDENCE) 31 | public class ProblemWebfluxAutoConfiguration { 32 | 33 | @Bean 34 | @ConditionalOnMissingBean(ErrorResponseBuilder.class) 35 | ErrorResponseBuilder>> 36 | errorResponseBuilder() { 37 | return new SpringWebfluxErrorResponseBuilder(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/webflux/SpringWebfluxErrorResponseBuilder.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.webflux; 2 | 3 | import static jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION; 4 | 5 | import com.ksoot.problem.core.ErrorResponseBuilder; 6 | import com.ksoot.problem.core.MediaTypes; 7 | import com.ksoot.problem.core.Problem; 8 | import java.net.URI; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import org.springframework.http.HttpHeaders; 12 | import org.springframework.http.HttpMethod; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.http.ProblemDetail; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.reactive.accept.HeaderContentTypeResolver; 18 | import org.springframework.web.server.ServerWebExchange; 19 | import reactor.core.publisher.Mono; 20 | 21 | public class SpringWebfluxErrorResponseBuilder 22 | implements ErrorResponseBuilder>> { 23 | 24 | @Override 25 | public Mono> buildResponse( 26 | final Throwable throwable, 27 | final ServerWebExchange request, 28 | final HttpStatus status, 29 | final HttpHeaders headers, 30 | final Problem problem) { 31 | if (status == HttpStatus.INTERNAL_SERVER_ERROR) { 32 | request.getAttributes().put(ERROR_EXCEPTION, throwable); 33 | } 34 | 35 | ProblemDetail problemDetail = createProblemDetail(request, status, problem); 36 | Optional>> responseEntity = 37 | negotiate(request) 38 | .map( 39 | contentType -> 40 | Mono.just( 41 | ResponseEntity.status(status) 42 | .headers(headers) 43 | .contentType(contentType) 44 | .body(problemDetail))); 45 | 46 | if (responseEntity.isPresent()) { 47 | return postProcess(responseEntity.get(), request); 48 | } else { 49 | return fallback(request, status, headers, problem); 50 | } 51 | } 52 | 53 | Optional negotiate(final ServerWebExchange request) { 54 | final List mediaTypes = new HeaderContentTypeResolver().resolveMediaTypes(request); 55 | return ErrorResponseBuilder.getProblemMediaType(mediaTypes); 56 | } 57 | 58 | private Mono> postProcess( 59 | final Mono> errorResponse, final ServerWebExchange request) { 60 | return errorResponse; 61 | } 62 | 63 | private Mono> fallback( 64 | final ServerWebExchange request, 65 | final HttpStatus status, 66 | final HttpHeaders headers, 67 | final Problem problem) { 68 | ProblemDetail problemDetail = createProblemDetail(request, status, problem); 69 | return Mono.just( 70 | ResponseEntity.status(status) 71 | .headers(headers) 72 | .contentType(MediaTypes.PROBLEM) 73 | .body(problemDetail)); 74 | } 75 | 76 | @Override 77 | public URI requestUri(final ServerWebExchange request) { 78 | return URI.create(request.getRequest().getPath().toString()); 79 | } 80 | 81 | @Override 82 | public HttpMethod requestMethod(final ServerWebExchange request) { 83 | return request.getRequest().getMethod(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/webflux/WebFluxDaoExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.webflux; 2 | 3 | import com.ksoot.problem.spring.advice.dao.AbstractDaoExceptionHandler; 4 | import com.ksoot.problem.spring.advice.dao.ConstraintNameResolver; 5 | import com.ksoot.problem.spring.boot.autoconfigure.DaoAdviceEnabled; 6 | import com.ksoot.problem.spring.config.ProblemProperties; 7 | import java.util.List; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Conditional; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.core.env.Environment; 16 | import org.springframework.http.ProblemDetail; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.ControllerAdvice; 19 | import org.springframework.web.server.ServerWebExchange; 20 | 21 | /** 22 | * @author Rajveer Singh 23 | */ 24 | @Configuration 25 | @EnableConfigurationProperties(ProblemProperties.class) 26 | @ConditionalOnProperty( 27 | prefix = "problem", 28 | name = "enabled", 29 | havingValue = "true", 30 | matchIfMissing = true) 31 | @Conditional(DaoAdviceEnabled.class) 32 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 33 | @Order(Ordered.HIGHEST_PRECEDENCE) 34 | @ControllerAdvice 35 | public class WebFluxDaoExceptionHandler 36 | extends AbstractDaoExceptionHandler> { 37 | 38 | WebFluxDaoExceptionHandler( 39 | final List constraintNameResolvers, final Environment env) { 40 | super(constraintNameResolvers, env); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/webflux/WebFluxExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.webflux; 2 | 3 | import com.ksoot.problem.spring.advice.webflux.ProblemHandlingWebflux; 4 | import com.ksoot.problem.spring.config.ProblemProperties; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.boot.autoconfigure.AutoConfiguration; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.http.ProblemDetail; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.ControllerAdvice; 13 | import reactor.core.publisher.Mono; 14 | 15 | @AutoConfiguration 16 | @EnableConfigurationProperties(ProblemProperties.class) 17 | @ConditionalOnProperty( 18 | prefix = "problem", 19 | name = "enabled", 20 | havingValue = "true", 21 | matchIfMissing = true) 22 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 23 | @ControllerAdvice 24 | @RequiredArgsConstructor 25 | public class WebFluxExceptionHandler 26 | implements ProblemHandlingWebflux>> {} 27 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/boot/autoconfigure/webflux/WebFluxSecurityExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.boot.autoconfigure.webflux; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.ksoot.problem.spring.advice.security.ProblemServerAccessDeniedHandler; 5 | import com.ksoot.problem.spring.advice.security.ProblemServerAuthenticationEntryPoint; 6 | import com.ksoot.problem.spring.advice.security.SecurityAdviceTraits; 7 | import com.ksoot.problem.spring.boot.autoconfigure.SecurityAdviceEnabled; 8 | import com.ksoot.problem.spring.config.ProblemProperties; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.boot.autoconfigure.AutoConfiguration; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Conditional; 17 | import org.springframework.core.Ordered; 18 | import org.springframework.core.annotation.Order; 19 | import org.springframework.http.ProblemDetail; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; 22 | import org.springframework.security.web.server.ServerAuthenticationEntryPoint; 23 | import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; 24 | import org.springframework.web.bind.annotation.ControllerAdvice; 25 | import org.springframework.web.server.ServerWebExchange; 26 | import reactor.core.publisher.Mono; 27 | 28 | @AutoConfiguration 29 | @EnableConfigurationProperties({ProblemProperties.class}) 30 | @Conditional(SecurityAdviceEnabled.class) 31 | @ConditionalOnClass(value = {WebSecurityConfiguration.class}) 32 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 33 | @Order(Ordered.HIGHEST_PRECEDENCE) 34 | @ControllerAdvice 35 | @RequiredArgsConstructor 36 | public class WebFluxSecurityExceptionHandler 37 | implements SecurityAdviceTraits>> { 38 | 39 | @ConditionalOnMissingBean 40 | @Bean 41 | ServerAuthenticationEntryPoint authenticationEntryPoint( 42 | final SecurityAdviceTraits>> advice, 43 | final ObjectMapper objectMapper) { 44 | return new ProblemServerAuthenticationEntryPoint(advice, objectMapper); 45 | } 46 | 47 | @ConditionalOnMissingBean 48 | @Bean 49 | ServerAccessDeniedHandler accessDeniedHandler( 50 | final SecurityAdviceTraits>> advice, 51 | final ObjectMapper objectMapper) { 52 | return new ProblemServerAccessDeniedHandler(advice, objectMapper); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemBeanRegistry.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | import com.ksoot.problem.core.ErrorResponseBuilder; 4 | import org.springframework.beans.BeansException; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.ApplicationContextAware; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | @EnableConfigurationProperties(ProblemProperties.class) 13 | public class ProblemBeanRegistry implements ApplicationContextAware { 14 | 15 | private static ApplicationContext applicationContext; 16 | 17 | public static ApplicationContext getApplicationContext() { 18 | return applicationContext; 19 | } 20 | 21 | @Override 22 | @Autowired 23 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 24 | ProblemBeanRegistry.applicationContext = applicationContext; 25 | } 26 | 27 | public static T getBean(Class requiredType) { 28 | return applicationContext.getBean(requiredType); 29 | } 30 | 31 | public static T getBean(String name, Class requiredType) { 32 | return applicationContext.getBean(name, requiredType); 33 | } 34 | 35 | public static ProblemProperties problemProperties() { 36 | return applicationContext.getBean(ProblemProperties.class); 37 | } 38 | 39 | @SuppressWarnings("unchecked") 40 | public static ErrorResponseBuilder errorResponseBuilder() { 41 | return applicationContext.getBean(ErrorResponseBuilder.class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemConfigException.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | /** A configuration related runtime exception. */ 4 | public class ProblemConfigException extends RuntimeException { 5 | /** The serial version ID. */ 6 | private static final long serialVersionUID = -7838702245512140996L; 7 | 8 | /** Constructs a new {@code ConfigurationRuntimeException} without specified detail message. */ 9 | public ProblemConfigException() { 10 | super(); 11 | } 12 | 13 | /** 14 | * Constructs a new {@code ConfigurationRuntimeException} with specified detail message. 15 | * 16 | * @param message the error message 17 | */ 18 | public ProblemConfigException(final String message) { 19 | super(message); 20 | } 21 | 22 | /** 23 | * Constructs a new {@code ConfigurationRuntimeException} with specified detail message using 24 | * {@link String#format(String, Object...)}. 25 | * 26 | * @param message the error message 27 | * @param args arguments to the error message 28 | * @see String#format(String, Object...) 29 | */ 30 | public ProblemConfigException(final String message, final Object... args) { 31 | super(String.format(message, args)); 32 | } 33 | 34 | /** 35 | * Constructs a new {@code ConfigurationRuntimeException} with specified nested {@code Throwable}. 36 | * 37 | * @param cause the exception or error that caused this exception to be thrown 38 | */ 39 | public ProblemConfigException(final Throwable cause) { 40 | super(cause); 41 | } 42 | 43 | /** 44 | * Constructs a new {@code ConfigurationRuntimeException} with specified detail message and nested 45 | * {@code Throwable}. 46 | * 47 | * @param message the error message 48 | * @param cause the exception or error that caused this exception to be thrown 49 | */ 50 | public ProblemConfigException(final String message, final Throwable cause) { 51 | super(message, cause); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemMessageProvider.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | import org.springframework.context.MessageSource; 4 | import org.springframework.context.MessageSourceResolvable; 5 | import org.springframework.context.i18n.LocaleContextHolder; 6 | 7 | /** 8 | * @author Rajveer Singh 9 | */ 10 | public class ProblemMessageProvider { 11 | 12 | private static MessageSource messageSource; 13 | 14 | public ProblemMessageProvider(final MessageSource messageSource) { 15 | ProblemMessageProvider.messageSource = messageSource; 16 | } 17 | 18 | public static String getMessage(final String messageCode, final String defaultMessage) { 19 | return messageSource.getMessage( 20 | messageCode, null, defaultMessage, LocaleContextHolder.getLocale()); 21 | } 22 | 23 | public static String getMessage( 24 | final String messageCode, final String defaultMessage, final Object... params) { 25 | return messageSource.getMessage( 26 | messageCode, params, defaultMessage, LocaleContextHolder.getLocale()); 27 | } 28 | 29 | public static String getMessage(final MessageSourceResolvable resolvable) { 30 | return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemMessageProviderConfig.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 4 | import org.springframework.context.MessageSource; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConditionalOnMissingBean(value = ProblemMessageProvider.class) 10 | public class ProblemMessageProviderConfig { 11 | 12 | @Bean 13 | ProblemMessageProvider problemMessageProvider(final MessageSource messageSource) { 14 | return new ProblemMessageProvider(messageSource); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemMessageSourceResolver.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | import com.ksoot.problem.core.ProblemConstant; 4 | import jakarta.validation.ConstraintViolation; 5 | import java.io.Serializable; 6 | import java.util.Arrays; 7 | import org.springframework.context.MessageSourceResolvable; 8 | import org.springframework.lang.Nullable; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.validation.ObjectError; 11 | 12 | /** 13 | * @author Rajveer Singh 14 | */ 15 | @SuppressWarnings("serial") 16 | public class ProblemMessageSourceResolver implements MessageSourceResolvable, Serializable { 17 | 18 | private final String[] codes; 19 | 20 | @Nullable private String defaultMessage; 21 | 22 | @Nullable private Object[] arguments; 23 | 24 | private ProblemMessageSourceResolver( 25 | final String[] codes, final String defaultMessage, Object[] arguments) { 26 | this.codes = codes; 27 | this.defaultMessage = defaultMessage; 28 | this.arguments = arguments; 29 | } 30 | 31 | public static ProblemMessageSourceResolver of(final String code) { 32 | return new ProblemMessageSourceResolver(new String[] {code}, null, null); 33 | } 34 | 35 | public static ProblemMessageSourceResolver of(final String[] codes) { 36 | return new ProblemMessageSourceResolver(codes, null, null); 37 | } 38 | 39 | public static ProblemMessageSourceResolver of(final String code, final Object[] arguments) { 40 | return new ProblemMessageSourceResolver(new String[] {code}, null, arguments); 41 | } 42 | 43 | public static ProblemMessageSourceResolver of(final String[] codes, final Object[] arguments) { 44 | return new ProblemMessageSourceResolver(codes, null, arguments); 45 | } 46 | 47 | public static ProblemMessageSourceResolver of(final String[] codes, final String defaultMessage) { 48 | return new ProblemMessageSourceResolver(codes, defaultMessage, null); 49 | } 50 | 51 | public static ProblemMessageSourceResolver of(final String[] codes, final int status) { 52 | return new ProblemMessageSourceResolver(codes, String.valueOf(status), null); 53 | } 54 | 55 | public static ProblemMessageSourceResolver of(final String code, final String defaultMessage) { 56 | return new ProblemMessageSourceResolver(new String[] {code}, defaultMessage, null); 57 | } 58 | 59 | public static ProblemMessageSourceResolver of(final String code, final int statusCode) { 60 | return new ProblemMessageSourceResolver(new String[] {code}, String.valueOf(statusCode), null); 61 | } 62 | 63 | public static ProblemMessageSourceResolver of( 64 | final String[] codes, final String defaultMessage, final Object[] arguments) { 65 | return new ProblemMessageSourceResolver(codes, defaultMessage, arguments); 66 | } 67 | 68 | public static ProblemMessageSourceResolver of( 69 | final String code, final String defaultMessage, final Object[] arguments) { 70 | return new ProblemMessageSourceResolver(new String[] {code}, defaultMessage, arguments); 71 | } 72 | 73 | public static ProblemMessageSourceResolver of( 74 | final String prefix, final ObjectError objectError, final String defaultMessage) { 75 | return of( 76 | Arrays.stream(objectError.getCodes()) 77 | .map(code -> prefix + ProblemConstant.DOT + code) 78 | .toArray(String[]::new), 79 | defaultMessage); 80 | } 81 | 82 | public static ProblemMessageSourceResolver of( 83 | final String prefix, final ObjectError objectError, final int status) { 84 | return of( 85 | Arrays.stream(objectError.getCodes()) 86 | .map(code -> prefix + ProblemConstant.DOT + code) 87 | .toArray(String[]::new), 88 | "" + status); 89 | } 90 | 91 | public static ProblemMessageSourceResolver of( 92 | final String prefix, final ObjectError objectError) { 93 | return new ProblemMessageSourceResolver( 94 | Arrays.stream(objectError.getCodes()) 95 | .map(code -> prefix + ProblemConstant.DOT + code) 96 | .toArray(String[]::new), 97 | objectError.getDefaultMessage(), 98 | null); 99 | } 100 | 101 | public static ProblemMessageSourceResolver of(final String prefix, final FieldError fieldError) { 102 | return new ProblemMessageSourceResolver( 103 | Arrays.stream(fieldError.getCodes()) 104 | .map(code -> prefix + ProblemConstant.DOT + code) 105 | .toArray(String[]::new), 106 | fieldError.getDefaultMessage(), 107 | null); 108 | } 109 | 110 | public static ProblemMessageSourceResolver of( 111 | final String prefix, final FieldError fieldError, final String defaultMessage) { 112 | return of( 113 | Arrays.stream(fieldError.getCodes()) 114 | .map(code -> prefix + ProblemConstant.DOT + code) 115 | .toArray(String[]::new), 116 | defaultMessage); 117 | } 118 | 119 | public static ProblemMessageSourceResolver of( 120 | final String prefix, final FieldError fieldError, final int status) { 121 | return of( 122 | Arrays.stream(fieldError.getCodes()) 123 | .map(code -> prefix + ProblemConstant.DOT + code) 124 | .toArray(String[]::new), 125 | "" + status); 126 | } 127 | 128 | @SuppressWarnings("rawtypes") 129 | public static ProblemMessageSourceResolver of( 130 | final String prefix, final ConstraintViolation violation) { 131 | return of( 132 | prefix + ProblemConstant.DOT + violation.getPropertyPath().toString(), 133 | violation.getMessage()); 134 | } 135 | 136 | @SuppressWarnings("rawtypes") 137 | public static ProblemMessageSourceResolver of( 138 | final String prefix, final ConstraintViolation violation, final String defaultMessage) { 139 | return of( 140 | prefix + ProblemConstant.DOT + violation.getPropertyPath().toString(), defaultMessage); 141 | } 142 | 143 | @SuppressWarnings("rawtypes") 144 | public static ProblemMessageSourceResolver of( 145 | final String prefix, final ConstraintViolation violation, final int status) { 146 | return of(prefix + ProblemConstant.DOT + violation.getPropertyPath().toString(), "" + status); 147 | } 148 | 149 | @Override 150 | public String[] getCodes() { 151 | return this.codes; 152 | } 153 | 154 | @Override 155 | public String getDefaultMessage() { 156 | return this.defaultMessage; 157 | } 158 | 159 | @Override 160 | public Object[] getArguments() { 161 | return this.arguments; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/ksoot/problem/spring/config/ProblemProperties.java: -------------------------------------------------------------------------------- 1 | package com.ksoot.problem.spring.config; 2 | 3 | import jakarta.validation.Valid; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | import org.springframework.boot.context.properties.ConfigurationProperties; 11 | 12 | /** 13 | * @author Rajveer Singh 14 | */ 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | @ToString 19 | @ConfigurationProperties(prefix = "problem") 20 | public class ProblemProperties { 21 | 22 | /** Default: true, Whether or not to enable Problem handling. */ 23 | private boolean enabled = true; 24 | 25 | /** Default: http://localhost:8080/problems/help.html, Help page base url. */ 26 | private String typeUrl = "http://localhost:8080/problems/help.html"; 27 | 28 | /** 29 | * Default: false, Whether or not to include debug-info such as message codes etc. in error 30 | * response messages. 31 | */ 32 | private boolean debugEnabled = false; 33 | 34 | /** Default: false, Whether or not to include stacktrace in error response messages. */ 35 | private boolean stacktraceEnabled = false; 36 | 37 | /** Default: false, Whether or not to include exception cause in error response messages. */ 38 | private boolean causeChainsEnabled = false; 39 | 40 | /** Default: true, Whether or not to Enable Jackson Problem module. */ 41 | private boolean jacksonModuleEnabled = true; 42 | 43 | /** Default: true, Whether or not to Enable DAO exception handling advices. */ 44 | private boolean daoAdviceEnabled = true; 45 | 46 | /** Default: true, Whether or not to Enable Security exception handling advices. */ 47 | private boolean securityAdviceEnabled = true; 48 | 49 | private OpenApi openApi = new OpenApi(); 50 | 51 | @Getter 52 | @Setter 53 | @NoArgsConstructor 54 | @ToString 55 | @Valid 56 | public static class OpenApi { 57 | 58 | /** Default: /oas/api.json, Path of API Specification json file. */ 59 | private String path = "/oas/api.json"; 60 | 61 | /** 62 | * Default: None. List of path patterns in ant-pattern format to exclude from OpenAPI 63 | * Specification validation. 64 | */ 65 | private List excludePatterns = new ArrayList<>(); 66 | 67 | /** 68 | * Default: true, Whether or not to enable Open API request validation.
While enabling make 69 | * sure Problem is also enabled. 70 | */ 71 | private boolean reqValidationEnabled = false; 72 | 73 | /** 74 | * Default: false, Whether or not to enable Open API response validation.
While enabling 75 | * make sure Problem is also enabled. 76 | */ 77 | private boolean resValidationEnabled = false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "problem.enabled", 5 | "type": "java.lang.Boolean", 6 | "defaultValue": "true", 7 | "description": "Default: true, Whether or not to enable Problem handling." 8 | }, 9 | { 10 | "name": "problem.type-url", 11 | "type": "java.lang.String", 12 | "defaultValue": "http://localhost:8080/problems/help.html", 13 | "description": "Default: http://localhost:8080/problems/help.html, Help page base url." 14 | }, 15 | { 16 | "name": "problem.debug-enabled", 17 | "type": "java.lang.Boolean", 18 | "defaultValue": "false", 19 | "description": "Default: false, Whether or not to include debug-info such as message codes etc. in error response messages." 20 | }, 21 | { 22 | "name": "problem.stacktrace-enabled", 23 | "type": "java.lang.Boolean", 24 | "defaultValue": "false", 25 | "description": "Default: false, Whether or not to include stacktrace in error response messages." 26 | }, 27 | { 28 | "name": "problem.cause-chains-enabled", 29 | "type": "java.lang.Boolean", 30 | "defaultValue": "false", 31 | "description": "Default: false, Whether or not to include exception cause in error response messages." 32 | }, 33 | { 34 | "name": "problem.jackson-module-enabled", 35 | "type": "java.lang.Boolean", 36 | "defaultValue": "true", 37 | "description": "Default: true, Whether or not to Enable Jackson Problem module." 38 | }, 39 | { 40 | "name": "problem.dao-advice-enabled", 41 | "type": "java.lang.Boolean", 42 | "defaultValue": "true", 43 | "description": "Default: true, Whether or not to Enable DAO exception handling advices." 44 | }, 45 | { 46 | "name": "problem.security-advice-enabled", 47 | "type": "java.lang.Boolean", 48 | "defaultValue": "true", 49 | "description": "Default: true, Whether or not to Enable Security exception handling advices." 50 | }, 51 | { 52 | "name": "problem.open-api.path", 53 | "type": "java.lang.String", 54 | "defaultValue": "/oas/api.json", 55 | "description": "Default: /oas/api.json, Path of API Specification json file." 56 | }, 57 | { 58 | "name": "problem.open-api.exclude-patterns", 59 | "type": "java.lang.String", 60 | "description": "Default: None. List of path patterns in ant-pattern format to exclude from OpenAPI Specification validation." 61 | }, 62 | { 63 | "name": "problem.open-api.req-validation-enabled", 64 | "type": "java.lang.Boolean", 65 | "defaultValue": "true", 66 | "description": "Default: true, Whether or not to enable Open API request validation.
While enabling make sure Problem is also enabled." 67 | }, 68 | { 69 | "name": "problem.open-api.res-validation-enabled", 70 | "type": "java.lang.Boolean", 71 | "defaultValue": "false", 72 | "description": "Default: false, Whether or not to enable Open API response validation.
While enabling make sure Problem is also enabled." 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.ksoot.problem.spring.config.ProblemMessageProviderConfig 2 | com.ksoot.problem.spring.config.ProblemBeanRegistry 3 | com.ksoot.problem.spring.boot.autoconfigure.ProblemJacksonConfiguration 4 | com.ksoot.problem.spring.boot.autoconfigure.ProblemDaoConfiguration 5 | com.ksoot.problem.spring.boot.autoconfigure.web.ProblemWebAutoConfiguration 6 | com.ksoot.problem.spring.boot.autoconfigure.web.WebExceptionHandler 7 | com.ksoot.problem.spring.boot.autoconfigure.web.WebSecurityExceptionHandler 8 | com.ksoot.problem.spring.boot.autoconfigure.web.WebDaoExceptionHandler 9 | com.ksoot.problem.spring.boot.autoconfigure.web.OpenApiValidationExceptionHandler 10 | com.ksoot.problem.spring.boot.autoconfigure.webflux.ProblemWebfluxAutoConfiguration 11 | com.ksoot.problem.spring.boot.autoconfigure.webflux.WebFluxExceptionHandler 12 | com.ksoot.problem.spring.boot.autoconfigure.webflux.WebFluxSecurityExceptionHandler 13 | com.ksoot.problem.spring.boot.autoconfigure.webflux.WebFluxDaoExceptionHandler 14 | -------------------------------------------------------------------------------- /src/main/resources/i18n/problems.properties: -------------------------------------------------------------------------------- 1 | ###################### Common Problem messages ###################### 2 | 3 | message.internal.server.error=Something has gone wrong, please try again 4 | 5 | code.not.found=404 6 | title.not.found=Not Found 7 | detail.not.found=Requested resource not found 8 | 9 | code.constraint.violation=constraint-violations 10 | title.constraint.violation=Bad Request 11 | detail.constraint.violation=Constraint violations has happened, please correct the request and try again 12 | 13 | 14 | status.java.net.SocketTimeoutException=503 15 | detail.org.springframework.dao.ConcurrencyFailureException=Conflicted with another concurrent update, please retry 16 | 17 | detail.security.unauthorized=Either Authorization header bearer token is missing or invalid 18 | detail.security.access.denied=Insufficient permissions to access the requested resource 19 | 20 | detail.java.lang.UnsupportedOperationException=The requested operation is not supported yet 21 | 22 | detail.org.springframework.web.multipart.MaxUploadSizeExceededException=Upload file size exceeded the maximum allowed limit: {0} 23 | -------------------------------------------------------------------------------- /tooling/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------