Spring Framework ile Bean Validation Tarihsel @Past/@Present Gibi Kontrolleri Yönetme

Spring Framework ile Bean Validation Tarihsel @Past/@Present Gibi Kontrolleri Yönetme

Yazılımlarda özellikle test senaryolarını işletirken bazen tarih ve saati değiştirmeye ihtiyaç duyarız.

Örneğin 3 ay sonra zamanı gelecek olan bir taksit ödemesi için yapılacak “bekleyen ödemeleriniz” gibi bir dashboard widget’inin o tarih geldiğinde doğru hesaplamalar yaparak sonucu doğru bir şekilde gösterebildiğini test etmek isteyebiliriz.

Ya da ileri tarihli işlem yapılması istenmeyen bir yerde ileri tarihli veri girişi yapılarak sistemin o tarihte nasıl sonuçlar üreteceğini kontrol etmemiz gerekebilir.

Özellikle kullanıcı kabul testleri için bunu gösterebiliyor olmak gerekebiliyor.

Restful api’larımızda request ile gelen veriler üzerinde temel kontrollerimizi Bean Validation ile yaparız. Herhangi bir tarih alanının ileriye dönük olmaması için @PastOrPresent ya da @Past annotation’larını kullanabiliriz. Yukarıda bahesttiğim gibi bir senaryoda ileriye dönük tarih girmeye çalıştığınızda bean validation kurallarından dolayı validator bize girilen tarihin ileriye dönük olamayacağına dair bir hata mesajı verir.

Bu yazıda tam da bu noktada sistem tarihinin ileriki bir zamandaymış gibi veya girilen tarihteymiş gibi davranmasını nasıl sağlayabileceğimizi anlatmaya çalışacağım.

Bunu yapabilmek için Spring üzerindeki iki özelliği kullandım; javax.validation.Configuration.clockProvider ve @RefreshScope

javax.validation.Validator implementasyonunun ClockProvider nesnesini özelleştirmek için aşağıdaki gibi bir konfigürasyon yapmak gerekmektedir. ClockProvider validation yapılırken tarihsel kontrollerde referans alınacak java.time.Clock nesnesini sağlar. Spring boot default olarak hibernate-validator kullanır ve ClockProvider olarak hibernate-validator’un sağladığı DefaultClockProvider nesnesi uygulamada kullanılır.

@RefreshScope spring-cloud-context tarafından sağlanır ve dağıtık uygulamalarda konfigürasyon değişikliklerinin runtime’da uygulanabilmesini sağlar. RefreshScope olarak belirlenen bean’ler RefreshEvent tipinde bir ApplicationEvent yayınlandığında yeniden initialize edilerek yeni konfigürasyonu ile hizmet vermeye başlar.

@EnableWebMvc
@Configuration
public class FrozenClockConfig implements WebMvcConfigurer {

    @RefreshScope
    @Bean
    @Override
    public org.springframework.validation.Validator getValidator() {
        return new LocalValidatorFactoryBean() {
            @Override
            protected void postProcessConfiguration(javax.validation.Configuration<?> configuration) {
                configuration.clockProvider(FrozenClock.getClockProvider());
            }
        };
    }
}

Uygulama içerisindeki zaman bilgisini yönetmek için aşağıdaki gibi bir class oluşturdum. FrozenClock uygulama tarihinin son durumunu ve buna bağlı yeni bir clock provider nesnesi döndürmeyi sağlar.

public final class FrozenClock {

    private static Date systemDate = new Date();
    private static boolean frozen = false;

    private FrozenClock() {
    }

    public static void freeze(final Date newDate) {
        systemDate = newDate;
        frozen = true;
    }

    public static void unfreeze() {
        frozen = false;
    }

    public static ClockProvider getClockProvider() {
        return () -> {
            if (frozen) {
                return Clock.fixed(Instant.ofEpochSecond(systemDate.getTime()), Clock.systemDefaultZone().getZone());
            } else {
                return Clock.systemDefaultZone();
            }
        };
    }
}

Artık sistem tarihini dışarıdan yönetebileceğim bir API’a ihtiyacım var. Bunu aşağıdaki restful controller ile yapıyorum.

@RestController
@RequestMapping("/clock")
@RequiredArgsConstructor
public class ClockManagementController {

    private final ApplicationEventPublisher eventPublisher;

    @PutMapping(path = "/freeze")
    public ResponseEntity freeze(@RequestBody @Valid FrozenClockInfo frozenClockInfo) {
        FrozenClock.freeze(frozenClockInfo.getNewDate());
        refreshSystemClock();
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @PutMapping(path = "/unfreeze")
    public ResponseEntity unfreeze() {
        FrozenClock.unfreeze();
        refreshSystemClock();

        return new ResponseEntity<>(HttpStatus.OK);
    }

    private void refreshSystemClock() {
        eventPublisher.publishEvent(new RefreshEvent(this, "RefreshEvent", "Refresh synchronized clock  related beans"));
    }
}

Bu controller içerisinde freeze ve unfreeze adında iki metod var. Freeze metodu ile sistem tarihini dışarıdan göndereceğim bir tarih bilgisi ile değiştirip sabitleyebiliyorum. Görüldüğü gibi freeze ve unfreeze metodları her çağırıldığında refreshSystemClock metodu ile ApplicationEventPublisher aracılığı ile uygulama içerisinde bir RefreshEvent tetikleniyor. Spring tarfından bu event RefreshScope olarak işaretlenmiş bean’lerin yeniden initialize edilmelerini sağlar.

Freeze

curl -X PUT "http://localhost:6001/clock/freeze" -H  "accept: */*" -H  "Content-Type: application/json" -d "{  \"newDate\": \"2020-12-31T18:10:29.574Z\"}"

Unfreeze metodu ile sistem tarihini default değerlerine döndürebilirim.

curl -X PUT "http://localhost:6001/clock/unfreeze" -H  "accept: */*"

Birde DemoController adında tarih kontrollerine sahip bir API’ım var.

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    @PostMapping
    public @ResponseBody
    ResponseEntity create(@Valid @RequestBody DemoDTO demoDTO) {
        log.debug("demoDTO = {}", demoDTO);
        return new ResponseEntity<>(HttpStatus.CREATED);
    }
}

create metodu DemoDTO adında bir nesne kabul etmekte.

@Getter
@Setter
@ToString
public class DemoDTO {
    @NotEmpty
    private String id;

    @PastOrPresent
    @NotNull
    private Date operationDate;
}

Gördüğünüz gibi operationDate alanı @PastOrPresent kontrolüne sahip. Bu sayede ileri tarihte bir değer gönderemiyoruz. Ve ben ileri tarihli bir işlem gerçekleştirerek sistemi test etmek istiyorum. Fakat bu şartlarda kontrollerden geçemiyorum. Atık yukarıdaki düzenlemeler ile birlikte bu problemi çözebilirim.

Sistem Tarihi : 2020–10–31 bu tarihe göre ilk request denememi yapıyorum.

Bu requeste yanıt olarak aşağıdaki gibi HTTP-400-Bad Request hatası alıyorum ve hatanın sebebi ise @PastOrPresent kontrolü.

Artık sistem tarihini ileriye alıyorum.

Bundan sonra tekrar işlem yapacağım requesti tekrarlıyorum.

Ve sonuç başarılı HTTP-201. Testimi yaptıktan sonra sistem tarihini tekrar default değerlerine alabilirim.

Bu yazı için oluşturduğum örnek uygulamaya aşağıdaki adresten erişebilirsiniz.

https://github.com/dilaverdemirel/frozen-clock-demo

Dilaver DEMİREL

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir