XAdESSigningTimeZoneHolder.java

// @formatter:off

package eu.europa.esig.dss.xades.signature;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;

// ########################OVERRIDE_DSS#########################
// #####  DİKKAT: OVERRIDE DEĞİLDİR!                        ####
// #####  XAdES <SigningTime> elementinin hangi timezone    ####
// #####  ile yazılacağını taşıyan statik tutucu sınıf.     ####
// #####  DSS upstream her zaman UTC ('Z') yazar; bu sınıf  ####
// #####  TÜBİTAK MA3 çıktısı ile uyumlu '+03:00' default'u ####
// #####  sağlar. Operatör env üzerinden değiştirebilir.    ####
// #############################################################

/**
 * XAdES {@code <SigningTime>} elemanının XML serileştirmesinde kullanılacak
 * zaman dilimini tutan singleton.
 *
 * <p>Default değer {@code +03:00} (Türkiye saati). Bu seçim, TÜBİTAK MA3
 * kütüphanesinin ürettiği XAdES çıktılarıyla birebir uyumlu olmasını sağlar
 * (bkz. issue #7). Default'u değiştirmek için {@code XADES_SIGNING_TIME_ZONE}
 * ortam değişkeni kullanılır — örn. UTC için {@code Z}, sabit ofset için
 * {@code +03:00} veya named zone için {@code Europe/Istanbul}.</p>
 *
 * <p>DSS {@link XAdESSignatureBuilder#incorporateSigningTime()} override'ı
 * çalışma anında bu sınıftan {@link #formatSigningTime(Date)} okuyarak
 * çıktıyı oluşturur. Bu, builder'ın Spring container'a bağlı olmamasını ve
 * DSS upstream paket yapısının (eu.europa.esig.dss.xades.signature) korunmasını
 * sağlar. Spring tarafı uygulamanın açılışında bir kez {@link #setZone(ZoneId)}
 * çağırarak konfigürasyonu yansıtır.</p>
 *
 * <p>Thread-safety: tek bir {@code volatile} alan üzerinden okuma/yazma yapılır.
 * Çağrı sıklığı düşük (her imza başına bir defa) olduğundan kilit gerekmez.</p>
 */
public final class XAdESSigningTimeZoneHolder {

    /**
     * Default değer: {@code +03:00}. TÜBİTAK uyumluluğu için (issue #7).
     * Operatör isterse Spring config üzerinden değiştirir.
     */
    public static final ZoneId DEFAULT_ZONE = ZoneOffset.of("+03:00");

    /**
     * XML çıktı formatı. {@code XXX} ofseti {@code +03:00} olarak, UTC'yi ise
     * {@code Z} olarak basar — her ikisi de {@code xsd:dateTime} grameri
     * altında geçerlidir.
     */
    private static final DateTimeFormatter FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX");

    private static volatile ZoneId zone = DEFAULT_ZONE;

    private XAdESSigningTimeZoneHolder() {
        // utility class
    }

    /**
     * Aktif imza zaman dilimini döner. Default {@link #DEFAULT_ZONE}.
     */
    public static ZoneId getZone() {
        return zone;
    }

    /**
     * İmza zaman dilimini günceller. Genellikle uygulama açılışında bir kez
     * {@code XADES_SIGNING_TIME_ZONE} ortam değişkeninden çağrılır.
     *
     * @param newZone yeni zaman dilimi; null verilirse {@link #DEFAULT_ZONE}'a döner
     */
    public static void setZone(ZoneId newZone) {
        zone = (newZone != null) ? newZone : DEFAULT_ZONE;
    }

    /**
     * String değerden zaman dilimini parse eder. Desteklenen formatlar:
     * <ul>
     *   <li>{@code +03:00}, {@code -05:30} — sabit ofset</li>
     *   <li>{@code Z}, {@code UTC}, {@code GMT} — UTC</li>
     *   <li>{@code Europe/Istanbul} — IANA zone (DST destekli)</li>
     * </ul>
     *
     * <p>{@code null}/boş input default'a düşer. Geçersiz string için
     * {@link DateTimeException} fırlatılır; bu hata, fail-fast davranışını
     * tercih eden Spring bootstrap akışında bilinçli olarak yakalanmaz.</p>
     *
     * @param raw kullanıcı/env tarafından verilen zaman dilimi metni
     * @return parse edilmiş {@link ZoneId}
     */
    public static ZoneId parseZone(String raw) {
        if (raw == null) {
            return DEFAULT_ZONE;
        }
        String trimmed = raw.trim();
        if (trimmed.isEmpty()) {
            return DEFAULT_ZONE;
        }
        return ZoneId.of(trimmed);
    }

    /**
     * Verilen tarihi aktif zaman dilimi ile XML {@code dateTime} formatına
     * dönüştürür. Örnek çıktılar:
     * <ul>
     *   <li>{@code +03:00} zone: {@code 2025-11-19T14:12:39+03:00}</li>
     *   <li>UTC zone: {@code 2025-11-19T11:12:39Z}</li>
     * </ul>
     *
     * @param signingDate imza anı; null değilse {@link Date#toInstant()} kullanılır
     * @return XAdES {@code <SigningTime>} text node'una konulacak metin
     */
    public static String formatSigningTime(Date signingDate) {
        Objects.requireNonNull(signingDate, "signingDate null olamaz");
        Instant instant = signingDate.toInstant();
        ZonedDateTime zdt = instant.atZone(zone);
        return FORMATTER.format(zdt);
    }
}