GlobalExceptionHandler.java

package io.mersel.dss.signer.api;

import io.mersel.dss.signer.api.exceptions.CertificateValidationException;
import io.mersel.dss.signer.api.exceptions.SignatureException;
import io.mersel.dss.signer.api.exceptions.TimestampException;
import io.mersel.dss.signer.api.models.ErrorModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

/**
 * Uygulama için global exception handler.
 * Tutarlı hata yanıtları ve loglama sağlar.
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * İmza ile ilgili exception'ları yönetir.
     */
    @ExceptionHandler(SignatureException.class)
    public ResponseEntity<ErrorModel> handleSignatureException(SignatureException ex) {
        LOGGER.error("İmza işlemi başarısız: {} - {}", ex.getErrorCode(), ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorModel(ex.getErrorCode(), ex.getMessage()));
    }

    /**
     * Sertifika doğrulama exception'larını yönetir.
     */
    @ExceptionHandler(CertificateValidationException.class)
    public ResponseEntity<ErrorModel> handleCertificateValidationException(
            CertificateValidationException ex) {
        LOGGER.error("Sertifika doğrulama başarısız: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorModel(ex.getErrorCode(), ex.getMessage()));
    }

    /**
     * Keystore exception'larını yönetir.
     */
    @ExceptionHandler(io.mersel.dss.signer.api.exceptions.KeyStoreException.class)
    public ResponseEntity<ErrorModel> handleKeyStoreException(
            io.mersel.dss.signer.api.exceptions.KeyStoreException ex) {
        LOGGER.error("KeyStore işlemi başarısız: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorModel(ex.getErrorCode(), ex.getMessage()));
    }

    /**
     * Zaman damgası exception'larını yönetir.
     */
    @ExceptionHandler(TimestampException.class)
    public ResponseEntity<ErrorModel> handleTimestampException(TimestampException ex) {
        LOGGER.error("Zaman damgası işlemi başarısız: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(new ErrorModel(ex.getErrorCode(), ex.getMessage()));
    }

    /**
     * Dosya yükleme boyut aşımı exception'larını yönetir.
     */
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<ErrorModel> handleMaxUploadSizeExceeded(
            MaxUploadSizeExceededException ex) {
        LOGGER.warn("Dosya yükleme boyutu aşıldı: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
            .body(new ErrorModel("FILE_TOO_LARGE", 
                "Yüklenen dosya maksimum izin verilen boyutu aşıyor"));
    }

    /**
     * Yanlış {@code Content-Type} ile gelen istekler için 415 mapping.
     *
     * <p>Sign endpoint'leri {@code consumes=multipart/form-data} sözleşmesi
     * ile çalışır; client {@code application/json}, {@code text/plain} ya da
     * benzeri media type ile POST attığında Spring
     * {@link HttpMediaTypeNotSupportedException} fırlatır. Bu exception
     * generic {@code Exception} handler'a düşmemeli — operasyonel UX için
     * 415 UNSUPPORTED_MEDIA_TYPE + açık bir {@code WRONG_CONTENT_TYPE}
     * error kodu daha doğru ki client doğru header'ı ayarlasın.</p>
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ResponseEntity<ErrorModel> handleHttpMediaTypeNotSupported(
            HttpMediaTypeNotSupportedException ex) {
        LOGGER.warn("Desteklenmeyen content-type: {}", ex.getContentType());
        return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
            .body(new ErrorModel("WRONG_CONTENT_TYPE",
                "İstek 'multipart/form-data' Content-Type'ı ile gönderilmeli"));
    }

    /**
     * Java güvenlik exception'larını yönetir.
     */
    @ExceptionHandler({
        KeyStoreException.class,
        CertificateException.class,
        NoSuchAlgorithmException.class,
        UnrecoverableKeyException.class
    })
    public ResponseEntity<ErrorModel> handleSecurityException(Exception ex) {
        LOGGER.error("Güvenlik işlemi başarısız: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorModel("SECURITY_ERROR", 
                "Bir güvenlik hatası oluştu: " + ex.getMessage()));
    }

    /**
     * Diğer tüm beklenmeyen exception'ları yönetir.
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorModel> handleGenericException(Exception ex) {
        LOGGER.error("Beklenmeyen hata oluştu", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorModel("INTERNAL_ERROR", 
                "Beklenmeyen bir hata oluştu. Sorun devam ederse lütfen destek ile iletişime geçin."));
    }
}