RestApiExceptionMapperBootProperties.java

/*
 * Copyright 2020-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.exception.spring.boot.autoconfigure;

import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.bremersee.exception.RestApiExceptionMapperProperties;
import org.bremersee.exception.RestApiExceptionMapperProperties.ExceptionMapping;
import org.bremersee.exception.RestApiExceptionMapperProperties.ExceptionMappingConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpStatus;

/**
 * Configuration properties for the rest api exception handler or resolver.
 *
 * @author Christian Bremer
 */
@ConditionalOnClass(RestApiExceptionMapperProperties.class)
@ConfigurationProperties(prefix = "bremersee.exception-mapping")
@Getter
@Setter
@ToString
@EqualsAndHashCode
public class RestApiExceptionMapperBootProperties {

  /**
   * The request paths the handler is responsible for. Default is empty.
   */
  private List<String> apiPaths = new ArrayList<>();

  /**
   * The default values of a rest api exception that will be set, if a value is not detected. The
   * default values are:
   * <table style="border: 1px solid">
   * <thead>
   * <tr>
   * <th style="border: 1px solid">attribute</th>
   * <th style="border: 1px solid">value</th>
   * </tr>
   * </thead>
   * <tbody>
   * <tr>
   * <td style="border: 1px solid">status</td>
   * <td style="border: 1px solid">500</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">message</td>
   * <td style="border: 1px solid">Internal Server Error</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">code</td>
   * <td style="border: 1px solid">UNSPECIFIED</td>
   * </tr>
   * </tbody>
   * </table>
   */
  private ExceptionMappingImpl defaultExceptionMapping;

  /**
   * Values ​​of errors whose values ​​can not be determined automatically. The name of the
   * exception can be a package, too (e. g. org.bremersee.foobar.*).
   *
   * <p>Examples application.yml:
   * <pre>
   * bremersee:
   *   exception-mapping:
   *     exception-mappings:
   *     - exception-class-name: org.springframework.security.access.AccessDeniedException
   *       status: 403
   *       message: Forbidden
   *       code: XYZ:0815
   *     - exception-class-name: javax.persistence.EntityNotFoundException
   *       status: 404
   *       message: Not Found
   *       code: GEN:404
   * </pre>
   */
  private List<ExceptionMappingImpl> exceptionMappings = new ArrayList<>();

  /**
   * The default configuration of the exception mapping.
   *
   * <table style="border: 1px solid">
   * <thead>
   * <tr>
   * <th style="border: 1px solid">attribute</th>
   * <th style="border: 1px solid">value</th>
   * </tr>
   * </thead>
   * <tbody>
   * <tr>
   * <td style="border: 1px solid">includeException</td>
   * <td style="border: 1px solid">true</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">includeApplicationName</td>
   * <td style="border: 1px solid">true</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">includePath</td>
   * <td style="border: 1px solid">true</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">includeHandler</td>
   * <td style="border: 1px solid">false</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">includeStackTrace</td>
   * <td style="border: 1px solid">false</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">includeCause</td>
   * <td style="border: 1px solid">true</td>
   * </tr>
   * <tr>
   * <td style="border: 1px solid">evaluateAnnotationFirst</td>
   * <td style="border: 1px solid">false</td>
   * </tr>
   * </tbody>
   * </table>
   *
   * <p>If two values of a key are available (e. g. per annotation (see {@link
   * org.springframework.web.bind.annotation.ResponseStatus}) and member attribute) it is possible
   * with {@code evaluateAnnotationFirst} to specify which one should be used.
   */
  private ExceptionMappingConfigImpl defaultExceptionMappingConfig;

  /**
   * Specifies mapping configuration per exception class.  The name of the exception can be a
   * package, too (e. g. org.bremersee.foobar.*).
   *
   * <p>Examples application.yml:
   * <pre>
   * bremersee:
   *   exception-mapping:
   *     exception-mapping-configs:
   *     - exception-class-name: org.springframework.security.access.AccessDeniedException
   *       include-exception-class-name: false
   *       include-handler: true
   *       include-cause: true
   *     - exception-class-name: org.springframework.*
   *       include-exception-class-name: true
   *       include-handler: true
   *       include-cause: false
   * </pre>
   */
  private List<ExceptionMappingConfigImpl> exceptionMappingConfigs = new ArrayList<>();

  /**
   * Instantiates rest api exception mapper properties.
   */
  public RestApiExceptionMapperBootProperties() {

    defaultExceptionMapping = new ExceptionMappingImpl();
    defaultExceptionMapping.setExceptionClassName("*");
    defaultExceptionMapping.setMessage(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
    defaultExceptionMapping.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());

    defaultExceptionMappingConfig = new ExceptionMappingConfigImpl();
    defaultExceptionMappingConfig.setExceptionClassName("*");

    exceptionMappings.add(new ExceptionMappingImpl(
        IllegalArgumentException.class.getName(),
        HttpStatus.BAD_REQUEST,
        null));

    exceptionMappings.add(new ExceptionMappingImpl(
        "org.springframework.security.access.AccessDeniedException",
        HttpStatus.FORBIDDEN,
        null));

    exceptionMappings.add(new ExceptionMappingImpl(
        "javax.persistence.EntityNotFoundException",
        HttpStatus.NOT_FOUND,
        null));
  }

  /**
   * To rest api exception mapper properties rest api exception mapper properties.
   *
   * @return the rest api exception mapper properties
   */
  public RestApiExceptionMapperProperties toRestApiExceptionMapperProperties() {
    return RestApiExceptionMapperProperties.builder()
        .defaultExceptionMapping(getDefaultExceptionMapping())
        .exceptionMappings(getExceptionMappings())
        .defaultExceptionMappingConfig(getDefaultExceptionMappingConfig())
        .exceptionMappingConfigs(getExceptionMappingConfigs())
        .build();
  }

  /**
   * The exception mapping.
   */
  @Setter
  @ToString
  @EqualsAndHashCode
  @NoArgsConstructor
  @AllArgsConstructor
  public static class ExceptionMappingImpl implements ExceptionMapping {

    /**
     * Instantiates a new exception mapping.
     *
     * @param exceptionClassName the exception class name
     * @param httpStatus the http status
     * @param code the code
     */
    public ExceptionMappingImpl(String exceptionClassName, HttpStatus httpStatus, String code) {
      this.exceptionClassName = exceptionClassName;
      if (httpStatus != null) {
        this.status = httpStatus.value();
        this.message = httpStatus.getReasonPhrase();
      }
      this.code = code;
    }

    /**
     * The exception class name.
     */
    @Getter
    private String exceptionClassName;

    private int status;

    /**
     * The exception message.
     */
    @Getter
    private String message;

    /**
     * The exception code.
     */
    @Getter
    private String code;

    /**
     * Gets status.
     *
     * @return the status
     */
    public int getStatus() {
      if (HttpStatus.resolve(status) == null) {
        return HttpStatus.INTERNAL_SERVER_ERROR.value();
      }
      return status;
    }

  }

  /**
   * The exception mapping config.
   */
  @Data
  @NoArgsConstructor
  @AllArgsConstructor
  public static class ExceptionMappingConfigImpl implements ExceptionMappingConfig {

    private String exceptionClassName;

    private Boolean includeMessage = true;

    private Boolean includeException = true;

    private Boolean includeApplicationName = true;

    private Boolean includePath = true;

    private Boolean includeHandler = false;

    private Boolean includeStackTrace = false;

    private Boolean includeCause = false;

    private Boolean evaluateAnnotationFirst = false;

  }

}