Input:
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import tech.inceptive.ai4czc.entsoedataretrieval.csv.CSVGenerator;
import tech.inceptive.ai4czc.entsoedataretrieval.csv.transformers.CSVTransformer;
import tech.inceptive.ai4czc.entsoedataretrieval.csv.transformers.GLDocumentCSVTransformer;
import tech.inceptive.ai4czc.entsoedataretrieval.csv.transformers.PublicationDocCSVTransformer;
import tech.inceptive.ai4czc.entsoedataretrieval.csv.transformers.UnavailabilityDocCSVTransformer;
import tech.inceptive.ai4czc.entsoedataretrieval.exceptions.DataRetrievalError;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.ENTSOEDataFetcher;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.inputs.Area;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.inputs.ColumnType;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.xjc.GLMarketDocument;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.xjc.PublicationMarketDocument;
import tech.inceptive.ai4czc.entsoedataretrieval.fetcher.xjc.UnavailabilityMarketDocument;
import tech.inceptive.ai4czc.entsoedataretrieval.tools.DateTimeDivider;

Context:
src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/ENTSOEHistoricalDataRetrieval.java
/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 */
package tech.inceptive.ai4czc.entsoedataretrieval;

/**
 * The class to fetch an dataset from entsoe.
 *
 * @author andres
 */
public class ENTSOEHistoricalDataRetrieval {

    private final Set<ColumnType> columnType;
    private ENTSOEDataFetcher fetcher; // not final for testing purpose
    private CSVGenerator csvGen; // not final for testing purpose
    private final String csvEscapeChar;
    private final String csvSeparator;
    // TODO : make the time granularity configurable?

    public ENTSOEHistoricalDataRetrieval(String authToken,
            Set<ColumnType> columnType, String csvEscapeChar, String csvSeparator, boolean useRequestCache) {
        this.columnType = columnType;
        this.fetcher = new ENTSOEDataFetcher(authToken, useRequestCache);
        this.csvEscapeChar = csvEscapeChar;
        this.csvSeparator = csvSeparator;
        this.csvGen = new CSVGenerator(csvSeparator, csvEscapeChar);
    }

    /**
     * Retrieves to a temporal file the dataset, on CSV format.
     *
     * @param startDate
     * @param endDate
     * @return
     */
    public File fetchDataset(LocalDateTime startDate, LocalDateTime endDate, Duration timeStep, Set<Area> targetAreas,
            Area mainFlowsArea, Duration maxTimeDuration) {
        List<CSVTransformer> transformers = new ArrayList<>(columnType.size());
        List<LocalDateTime> splits;
        if (Duration.ofDays(365).minus(maxTimeDuration).isNegative()) {
            throw new DataRetrievalError("Maximal duration can not be bigger than 365 days");
        }
        if (columnType.contains(ColumnType.ACTUAL_LOAD)) {
            // split request depending on stard and end date
            splits = DateTimeDivider.splitInMilestones(startDate, endDate, maxTimeDuration,
                    Duration.of(60, ChronoUnit.MINUTES));
            for (int i = 0; i < splits.size() - 1; i++) {
                for (Area targetArea : targetAreas) {
                    Optional<GLMarketDocument> opt = fetcher.fetchActualLoad(targetArea, splits.get(i), splits.get(i + 1));
                    if (opt.isPresent()) {
                        transformers.add(new GLDocumentCSVTransformer(csvSeparator, csvEscapeChar, opt.get()));
                    }
                }
            }
        }
        if (columnType.contains(ColumnType.DAY_AHEAD_LOAD)) {
            splits = DateTimeDivider.splitInMilestones(startDate, endDate, maxTimeDuration,
                    Duration.of(24, ChronoUnit.HOURS));
            for (int i = 0; i < splits.size() - 1; i++) {
                for (Area targetArea : targetAreas) {
                    Optional<GLMarketDocument> opt = fetcher.fetchDayAheadLoadForecast(targetArea, splits.get(i), splits.get(i + 1));
                    if (opt.isPresent()) {
                        transformers.add(new GLDocumentCSVTransformer(csvSeparator, csvEscapeChar, opt.get()));
                    }
                }
            }
        }
        if (columnType.contains(ColumnType.WEAK_AHEAD_LOAD)) {
            splits = DateTimeDivider.splitInMilestones(startDate, endDate, maxTimeDuration,
                    Duration.of(7, ChronoUnit.DAYS));
            for (int i = 0; i < splits.size() - 1; i++) {
                for (Area targetArea : targetAreas) {
                    Optional<GLMarketDocument> opt = fetcher.fetchWeekAheadLoadForecast(targetArea, splits.get(i), splits.get(i + 1));
                    if (opt.isPresent()) {
                        transformers.add(new GLDocumentCSVTransformer(csvSeparator, csvEscapeChar, opt.get()));
                    }
                }
            }
        }
        if (columnType.contains(ColumnType.AGGREGATED_GENERATION_TYPE)) {
            splits = DateTimeDivider.splitInMilestones(startDate, endDate, maxTimeDuration,
                    Duration.of(60, ChronoUnit.MINUTES));
            for (int i = 0; i < splits.size() - 1; i++) {
                for (Area targetArea : targetAreas) {
                    Optional<GLMarketDocument> opt = fetcher.fetchAggregatedGenerationType(targetArea, splits.get(i), splits.get(i + 1));
                    if (opt.isPresent()) {

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/fetcher/xjc/GLMarketDocument.java
@XmlRootElement(name = "GL_MarketDocument", namespace = "urn:iec62325.351:tc57wg16:451-6:generationloaddocument:3:0")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "GL_MarketDocument", propOrder = {
    "mrid",
    "revisionNumber",
    "type",
    "processProcessType",
    "senderMarketParticipantMRID",
    "senderMarketParticipantMarketRoleType",
    "receiverMarketParticipantMRID",
    "receiverMarketParticipantMarketRoleType",
    "createdDateTime",
    "timePeriodTimeInterval",
    "timeSeries"
})
public class GLMarketDocument {

    @XmlElement(name = "mRID", required = true)
    protected String mrid;
    @XmlElement(required = true)
    protected String revisionNumber;
    @XmlElement(required = true)
    protected String type;
    @XmlElement(name = "process.processType", required = true)
    protected String processProcessType;
    @XmlElement(name = "sender_MarketParticipant.mRID", required = true)
    protected PartyIDString senderMarketParticipantMRID;
    @XmlElement(name = "sender_MarketParticipant.marketRole.type", required = true)
    protected String senderMarketParticipantMarketRoleType;
    @XmlElement(name = "receiver_MarketParticipant.mRID", required = true)
    protected PartyIDString receiverMarketParticipantMRID;
    @XmlElement(name = "receiver_MarketParticipant.marketRole.type", required = true)
    protected String receiverMarketParticipantMarketRoleType;
    @XmlElement(required = true)
    @XmlSchemaType(name = "dateTime")
    protected XMLGregorianCalendar createdDateTime;
    @XmlElement(name = "time_Period.timeInterval", required = true)
    protected ESMPDateTimeInterval timePeriodTimeInterval;
    @XmlElement(name = "TimeSeries", required = true)
    protected List<TimeSeries> timeSeries;

    /**
     * Gets the value of the mrid property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getMRID() {
        return mrid;
    }

    /**
     * Sets the value of the mrid property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setMRID(String value) {
        this.mrid = value;
    }

    /**
     * Gets the value of the revisionNumber property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getRevisionNumber() {
        return revisionNumber;
    }

    /**
     * Sets the value of the revisionNumber property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setRevisionNumber(String value) {
        this.revisionNumber = value;
    }

    /**
     * Gets the value of the type property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getType() {
        return type;
    }

    /**
     * Sets the value of the type property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setType(String value) {
        this.type = value;
    }

    /**
     * Gets the value of the processProcessType property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getProcessProcessType() {
        return processProcessType;
    }

    /**
     * Sets the value of the processProcessType property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setProcessProcessType(String value) {
        this.processProcessType = value;
    }

    /**
     * Gets the value of the senderMarketParticipantMRID property.
     *
     * @return
     *     possible object is
     *     {@link PartyIDString }
     *
     */
    public PartyIDString getSenderMarketParticipantMRID() {
        return senderMarketParticipantMRID;
    }

    /**
     * Sets the value of the senderMarketParticipantMRID property.
     *
     * @param value
     *     allowed object is
     *     {@link PartyIDString }
     *
     */
    public void setSenderMarketParticipantMRID(PartyIDString value) {
        this.senderMarketParticipantMRID = value;
    }

    /**
     * Gets the value of the senderMarketParticipantMarketRoleType property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getSenderMarketParticipantMarketRoleType() {
        return senderMarketParticipantMarketRoleType;
    }

    /**
     * Sets the value of the senderMarketParticipantMarketRoleType property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setSenderMarketParticipantMarketRoleType(String value) {
        this.senderMarketParticipantMarketRoleType = value;
    }

    /**
     * Gets the value of the receiverMarketParticipantMRID property.
     *
     * @return
     *     possible object is
     *     {@link PartyIDString }
     *
     */
    public PartyIDString getReceiverMarketParticipantMRID() {
        return receiverMarketParticipantMRID;
    }

    /**
     * Sets the value of the receiverMarketParticipantMRID property.
     *
     * @param value
     *     allowed object is
     *     {@link PartyIDString }
     *
     */
    public void setReceiverMarketParticipantMRID(PartyIDString value) {
        this.receiverMarketParticipantMRID = value;
    }

    /**
     * Gets the value of the receiverMarketParticipantMarketRoleType property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getReceiverMarketParticipantMarketRoleType() {
        return receiverMarketParticipantMarketRoleType;
    }

    /**
     * Sets the value of the receiverMarketParticipantMarketRoleType property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setReceiverMarketParticipantMarketRoleType(String value) {
        this.receiverMarketParticipantMarketRoleType = value;
    }

    /**
     * Gets the value of the createdDateTime property.
     *
     * @return
     *     possible object is
     *     {@link XMLGregorianCalendar }
     *
     */
    public XMLGregorianCalendar getCreatedDateTime() {
        return createdDateTime;
    }

    /**
     * Sets the value of the createdDateTime property.
     *
     * @param value
     *     allowed object is
     *     {@link XMLGregorianCalendar }
     *
     */
    public void setCreatedDateTime(XMLGregorianCalendar value) {
        this.createdDateTime = value;
    }

    /**
     * Gets the value of the timePeriodTimeInterval property.
     *
     * @return
     *     possible object is
     *     {@link ESMPDateTimeInterval }
     *
     */
    public ESMPDateTimeInterval getTimePeriodTimeInterval() {
        return timePeriodTimeInterval;
    }

    /**
     * Sets the value of the timePeriodTimeInterval property.
     *
     * @param value
     *     allowed object is
     *     {@link ESMPDateTimeInterval }
     *
     */
    public void setTimePeriodTimeInterval(ESMPDateTimeInterval value) {
        this.timePeriodTimeInterval = value;
    }

    /**
     * Gets the value of the timeSeries property.
     *
     *

     * This accessor method returns a reference to the live list,
     * not a snapshot. Therefore any modification you make to the
     * returned list will be present inside the JAXB object.
     * This is why there is not a set method for the timeSeries property.
     *
     *

     * For example, to add a new item, do as follows:
     *

     *    getTimeSeries().add(newItem);
     *

     *
     *
     *

     * Objects of the following type(s) are allowed in the list
     * {@link TimeSeries }
     *
     *
     */
    public List<TimeSeries> getTimeSeries() {
        if (timeSeries == null) {
            timeSeries = new ArrayList<TimeSeries>();
        }
        return this.timeSeries;
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/fetcher/inputs/ColumnType.java
public enum ColumnType {
    ACTUAL_LOAD("6.1.A"),
    DAY_AHEAD_LOAD("6.1.B"),
    WEAK_AHEAD_LOAD("6.1.C"),
    TRANSMISSION_OUTAGE("10.1.A&B"),
    WEEK_FORECAST_CAPACITY("11.1.A.C"),
    PHYSICAL_FLOW("12.1.G"),
    GENERATION_OUTAGE("15.1.A&B"),
    AGGREGATED_GENERATION_TYPE("16.1.B&C");

    private static final Logger LOGGER = LogManager.getLogger(ColumnType.class);

    private static final Map<String, ColumnType> ARTICLE_TO_COLUMN_TYPE;

    static {
        ARTICLE_TO_COLUMN_TYPE = new HashMap<>();
        for(ColumnType curCol : ColumnType.values()) {
            ARTICLE_TO_COLUMN_TYPE.put(curCol.getRelevantArticle(), curCol);
        }
    }

    private final String relevantArticle;

    private ColumnType(String relevantArticle) {
        this.relevantArticle = relevantArticle;
    }

    public String getRelevantArticle() {
        return relevantArticle;
    }

    public static ColumnType ColumnTypeFromArticle(String article) {
        if (article == null) {
            throw new IllegalArgumentException("Article cannot be null");
        }
        ColumnType colType = ARTICLE_TO_COLUMN_TYPE.get(article);
        if (colType == null) {
            throw new IllegalArgumentException("Can not find the columnType for input article " + article);
        }
        return colType;
    }
}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/tools/DateTimeDivider.java
public class DateTimeDivider {

    private static final Logger LOGGER = LogManager.getLogger(DateTimeDivider.class);

    private DateTimeDivider() {
        // API class
    }

    public static List<LocalDateTime> splitInMilestones(LocalDateTime startDate, LocalDateTime endDate, Duration maxDur,
            Duration minDur) {
        if(maxDur.minus(minDur).isNegative()) {
            throw new DataRetrievalRuntimeException("Max duration should be bigger than min duration ");
        }
        LocalDateTime nextStep = startDate.plus(maxDur);
        List<LocalDateTime> res = new ArrayList<>();
        res.add(startDate);
        while(nextStep.isBefore(endDate)) {
            res.add(nextStep);
            nextStep= nextStep.plus(maxDur);
        }
        LocalDateTime last = nextStep.minus(maxDur);
        if(Duration.between(last, endDate).isZero()) {
            return res;
        }
        if(Duration.between(nextStep, endDate).isZero()) {
            res.add(nextStep);
            return res;
        }
        if(maxDur.minus(minDur).isZero()) {
            throw new DataRetrievalRuntimeException("Max duration should be bigger than min duration ");
        }
        if(Duration.between(last, endDate).minus(minDur).isNegative()) {
            res.remove(res.size()-1);
            res.add(endDate.minus(minDur));
        }
        res.add(endDate);
        return res;
    }
}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/fetcher/ENTSOEDataFetcher.java
public class ENTSOEDataFetcher {

    private static final Logger LOGGER = LogManager.getLogger(ENTSOEDataFetcher.class);

    public static String BASE_URL = "https://web-api.tp.entsoe.eu/api";

    private String authToken;
    private HttpBridge bridge;
    private DisponibilityChecker checker;

    public ENTSOEDataFetcher(String authToken, boolean useRequestCache) {
        this.authToken = authToken;
        this.bridge = new HttpBridge(useRequestCache); // TODO : fix server URL
        this.checker = new DisponibilityChecker();
    }

    /**
     * One year limitation applies. Fetchs actual load (documentType : A65, ProcessType : A16)
     *
     * @return
     */
    public Optional<GLMarketDocument> fetchActualLoad(Area outBiddingZoneDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("6.1.A", periodStart, outBiddingZoneDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toMinutes() < 60) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 60 minutes but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.SYSTEM_TOTAL_LOAD.getId());
        params.put(Params.PROCESS_TYPE.getValue(), ProcessType.REALISED.getId());
        params.put(Params.OUT_BIDDINDZONE_DOMAIN.getValue(), outBiddingZoneDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        String msg = "Unable to correctly fetch actual load at " + outBiddingZoneDomain.getPrettyName() + " from "
                + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, msg, GLMarketDocument.class);
    }

    public Optional<GLMarketDocument> fetchDayAheadLoadForecast(Area outBiddingZoneDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("6.1.B", periodStart, outBiddingZoneDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toHours() < 24) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 24 hours but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.SYSTEM_TOTAL_LOAD.getId());
        params.put(Params.PROCESS_TYPE.getValue(), ProcessType.DAY_AHEAD.getId());
        params.put(Params.OUT_BIDDINDZONE_DOMAIN.getValue(), outBiddingZoneDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        GLMarketDocument doc = null;
        String msg = "Unable to correctly fetch day ahead load at " + outBiddingZoneDomain.getPrettyName() + " from "
                + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, msg, GLMarketDocument.class);
    }

    public Optional<GLMarketDocument> fetchWeekAheadLoadForecast(Area outBiddingZoneDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("6.1.C", periodStart, outBiddingZoneDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toDays() < 7) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 7 days but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.SYSTEM_TOTAL_LOAD.getId());
        params.put(Params.PROCESS_TYPE.getValue(), ProcessType.WEEK_AHEAD.getId());
        params.put(Params.OUT_BIDDINDZONE_DOMAIN.getValue(), outBiddingZoneDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));

        String msg = "Unable to correctly fetch week ahead load at " + outBiddingZoneDomain.getPrettyName() + " from "
                + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, msg, GLMarketDocument.class);
    }

    public Optional<GLMarketDocument> fetchAggregatedGenerationType(Area inDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("16.1.B&C", periodStart, inDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toDays() < 7) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 7 days but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.ACTUAL_GENERATION_PER_TYPE.getId());
        params.put(Params.PROCESS_TYPE.getValue(), ProcessType.REALISED.getId());
        params.put(Params.IN_DOMAIN.getValue(), inDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));

        String msg = "Unable to correctly fetch aggregated generation per type on " + inDomain.getPrettyName() + " from "
                + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, msg, GLMarketDocument.class);
    }

    public Optional<PublicationMarketDocument> fetchPhysicalFlows(Area inDomain, Area outDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart1 = checker.checkAvailability("12.1.G", periodStart, inDomain);
        LocalDateTime dataStart2 = checker.checkAvailability("12.1.G", periodStart, outDomain);
        LocalDateTime dataStart;
        if (dataStart1.equals(dataStart2)) {
            dataStart = dataStart1;
        } else if (dataStart1.isAfter(dataStart2)) {
            dataStart = dataStart1;
        } else {
            dataStart = dataStart2;
        }
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toHours() < 24) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 24 hours but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.AGGREGATED_ENERGY_DATA_REPORT.getId());
        params.put(Params.IN_DOMAIN.getValue(), inDomain.getId());
        params.put(Params.OUT_DOMAIN.getValue(), outDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        String erroMsg = "Unable to correctly retrieve physical flows from " + outDomain.getPrettyName() + " to "
                + inDomain.getPrettyName() + " from " + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, erroMsg, PublicationMarketDocument.class);
    }

    /**
     * Fetchs the transmission grid outages.
     *
     * @param inDomain
     * @param outDomain
     * @param periodStart
     * @param periodEnd
     * @return One document per outage (can be a lot)
     */
    public List<UnavailabilityMarketDocument> fetchTransmissionGridOutages(Area inDomain, Area outDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("10.1.A&B", periodStart, inDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return new ArrayList<>();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return new ArrayList<>();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.TRANSMISSION_UNAVAILABILITY.getId());
        params.put(Params.IN_DOMAIN.getValue(), inDomain.getId());
        params.put(Params.OUT_DOMAIN.getValue(), outDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        try {
            return bridge.doZipGetOperation(params, BASE_URL, UnavailabilityMarketDocument.class);
        } catch (DataRetrievalRuntimeException ex) {
            LOGGER.catching(ex);
            LOGGER.warn("Unable to correctly retrieve outages between " + inDomain.getPrettyName() + " and "
                    + outDomain.getPrettyName() + " for time between " + usedStart.format(DateTimeFormatter.ISO_DATE_TIME)
                    + " and " + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME));
            return new ArrayList<>();
        }
    }

    public List<UnavailabilityMarketDocument> fetchGenerationUnitOutages(Area biddingZoneDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime dataStart = checker.checkAvailability("15.1.A&B", periodStart, biddingZoneDomain);
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dataStart)) {
            if (dataStart.isAfter(periodEnd)) {
                return new ArrayList<>();
            }
            usedStart = dataStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return new ArrayList<>();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.GENERATION_UNAVAILABILITY.getId());
        params.put(Params.BIDDING_ZONE_DOMAIN.getValue(), biddingZoneDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        try {
            return bridge.doZipGetOperation(params, BASE_URL, UnavailabilityMarketDocument.class);
        } catch (DataRetrievalRuntimeException ex) {
            LOGGER.catching(ex);
            LOGGER.warn("Unable to correctly retrieve generation outages at " + biddingZoneDomain.getPrettyName()
                    + " for time between " + usedStart.format(DateTimeFormatter.ISO_DATE_TIME)
                    + " and " + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME));
            return new ArrayList<>();
        }
    }

    public Optional<PublicationMarketDocument> fetchWeekAheadCapacityForecast(Area inDomain, Area outDomain, LocalDateTime periodStart,
            LocalDateTime periodEnd) {
        LocalDateTime date1 = checker.checkAvailability(ColumnType.WEEK_FORECAST_CAPACITY.getRelevantArticle(),
                periodStart, outDomain);
        LocalDateTime date2 = checker.checkAvailability(ColumnType.WEEK_FORECAST_CAPACITY.getRelevantArticle(),
                periodStart, inDomain);
        LocalDateTime dateStart = (date1.isAfter(date2)) ? date1 : date2;
        LocalDateTime usedStart = periodStart;
        if (!periodStart.equals(dateStart)) {
            if (dateStart.isAfter(periodEnd)) {
                return Optional.empty();
            }
            usedStart = dateStart;
        }
        Duration gap = Duration.between(usedStart, periodEnd);
        if (gap.toDays() > 365) {
            LOGGER.warn("Error, uncorrect duration. Expected request of less than 365 days, but was " + gap.toDays() + " days");
            return Optional.empty();
        }
        if (gap.toDays() < 7) {
            LOGGER.warn("Error, uncorrect duration. Expected at least 24 hours but it was " + gap.toMinutes() + " minutes");
            return Optional.empty();
        }
        Map<String, String> params = new HashMap<>();
        params.put(Params.SECURITY_TOKEN.getValue(), authToken);
        params.put(Params.DOCUMENT_TYPE.getValue(), DocumentType.ESTIMATED_NET_TRANSFER_CAPACITY.getId());
        params.put(Params.CONTRACT_MARKET_AGREEMENT_TYPE.getValue(), ContractMarketAgreement.WEEKLY.getCode());
        params.put(Params.IN_DOMAIN.getValue(), inDomain.getId());
        params.put(Params.OUT_DOMAIN.getValue(), outDomain.getId());
        params.put(Params.PERIOD_START.getValue(), usedStart.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        params.put(Params.PERIOD_END.getValue(), periodEnd.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")));
        String erroMsg = "Unable to correctly week ahead transfert capacity forecast from " + outDomain.getPrettyName() + " to "
                + inDomain.getPrettyName() + " from " + usedStart.format(DateTimeFormatter.ISO_DATE_TIME) + " to "
                + periodEnd.format(DateTimeFormatter.ISO_DATE_TIME);
        return performGetOperation(params, erroMsg, PublicationMarketDocument.class);
    }

    public String getAuthToken() {
        return authToken;
    }

    public void setAuthToken(String authToken) {
        this.authToken = authToken;
    }

    private <T> Optional<T> performGetOperation(Map<String, String> params, String errorMsg, Class<T> reflectedClass) throws DataBindingException {
        T doc = null;
        try {
            doc = bridge.doGetOperation(params, BASE_URL, reflectedClass);
        } catch (DataRetrievalRuntimeException ex) {
            LOGGER.catching(ex);
            LOGGER.warn(errorMsg);
            return Optional.empty();
        }
        return Optional.ofNullable(doc);
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/csv/transformers/CSVTransformer.java
public abstract class CSVTransformer {

    private static final Logger LOGGER = LogManager.getLogger(CSVTransformer.class);

    /**
     * A time serie with a selected point. Selected period : the period index selected on ts Selected point : the
     * selected index on ts.getPeriod.getPoint
     */
    public record MarkedTS(TimeSeries ts, int selectedPeriod, int selectedPoint) {

        /**
         * The value of the selected point of this time serie
         *
         * @return
         */
        public int getSelectedPointValue() {
            return ts.getPeriod().get(selectedPeriod).getPoint().get(selectedPoint).getQuantity().intValue();
        }
    }
    ;

    private final String csvSeparator;
    private final String csvEscapeChar;
    private List<ColumnDefinition> columnDefinition = null;

    public CSVTransformer(String csvSeparator, String csvEscapeChar) {
        this.csvSeparator = csvSeparator;
        this.csvEscapeChar = csvEscapeChar;
    }

    public List<ColumnDefinition> getColumnDefinition() {
        if (columnDefinition == null) {
            columnDefinition = computeColumnDef();
        }
        return columnDefinition;
    }

    /**
     * Writes the next line of the given time stamp.Starts with the separator.
     *
     * @param os
     * @return true if the entry was writed, false otherwise
     */
    public abstract boolean writeNextEntry(LocalDateTime timeStamp, BufferedWriter os);

    protected abstract List<ColumnDefinition> computeColumnDef();

    /**
     *
     * @param startingPoint
     * @param timeStamp
     * @return
     */
    protected static List<GLDocumentCSVTransformer.MarkedTS> getPeriodForTimeStamp(List<TimeSeries> timeSeries,
            LocalDateTime timeStamp) {
        // search for the correct ts
        // compute the corresponding positions
        // we assume that is there is many point of interest, they will be in different ts
        List<Integer> tsPositions = computeTSPosition(timeSeries,timeStamp);
        if (tsPositions.isEmpty()) {
            return new ArrayList<>();
        }
        // start of the time series
        List<GLDocumentCSVTransformer.MarkedTS> res = new ArrayList<>();
        for (int tsPosition : tsPositions) {
            for (int periodPosition = 0; periodPosition < timeSeries.size(); periodPosition++) {
                LocalDateTime startDT = timeSeries.get(tsPosition).getPeriod().get(0).getTimeInterval().getLDTStart();
                int resolutionInt = getResolutionInMinutes(timeSeries.get(tsPosition).getPeriod().get(0).getResolution());
                long gapMinutes = ChronoUnit.MINUTES.between(startDT, timeStamp);
                long selectPosition = (gapMinutes / resolutionInt);
                if (selectPosition > Integer.MAX_VALUE) {
                    throw new DataRetrievalError("The position to select is to big. Please use smaller queries");
                }
                List<Point> points = timeSeries.get(tsPosition).getPeriod().get(0).getPoint();
                if (selectPosition >= points.size()) {
                    // we handle the exceptional case of some week ahead time series that have a lenght of 1 week and one hour and are in this
                    // exceptional case. We handle it
                    if (selectPosition == points.size()
                            && ("A60".equals(timeSeries.get(tsPosition).getBusinessType()) || "A61".equals(timeSeries.get(tsPosition).getBusinessType()))) {
                        res.add(new GLDocumentCSVTransformer.MarkedTS(timeSeries.get(tsPosition), periodPosition, (int) selectPosition - 1));
                        break;
                    }
                    String msg = "Error computing the selected position. Position computed : " + selectPosition
                            + " in TS of size " + points.size() + " .Requested time stamp is : "
                            + timeStamp.format(DateTimeFormatter.ISO_DATE_TIME) + " on document starting in "
                            + timeSeries.get(tsPosition).getPeriod().get(0).getTimeInterval().getLDTStart().format(DateTimeFormatter.ISO_DATE_TIME)
                            + " and ending at " + timeSeries.get(tsPosition).getPeriod().get(0).getTimeInterval().getLDTEnd().format(DateTimeFormatter.ISO_DATE_TIME);
                    LOGGER.warn(msg);
                    LOGGER.warn("Gap in minutes : {}, resolution in minutes : {}, ratio in double {}", gapMinutes, resolutionInt, gapMinutes / (double) resolutionInt);
                    // invalid time series, do nothing
                } else {
                    res.add(new GLDocumentCSVTransformer.MarkedTS(timeSeries.get(tsPosition), periodPosition, (int) selectPosition));
                    // supose there is only one pertienent period
                    break;
                }
            }
        }
        return res;
    }

    private static int getResolutionInMinutes(Duration duration) {
        if (duration.isSet(DatatypeConstants.MINUTES)) {
            return duration.getMinutes();
        } else if (duration.isSet(DatatypeConstants.DAYS)) {
            return duration.getDays() * 24 * 60;
        } else {
            throw new UnsupportedOperationException("Non suported duration type");
        }
    }

    private static List<Integer> computeTSPosition(List<TimeSeries> timeSeries, LocalDateTime timeStamp) {
        List<Integer> res = new ArrayList<>();
        for (int curPos = 0; curPos < timeSeries.size(); curPos++) {
            // TODO : handle periods
            LocalDateTime curStart = timeSeries.get(curPos).getPeriod().get(0).getTimeInterval().getLDTStart();
            LocalDateTime curEnd = timeSeries.get(curPos).getPeriod().get(0).getTimeInterval().getLDTEnd();
            if ((timeStamp.isAfter(curStart) || timeStamp.isEqual(curStart)) && timeStamp.isBefore(curEnd)) {
                res.add(curPos);
            }
        }
        return res;
    }

    public String getCsvSeparator() {
        return csvSeparator;
    }

    public String getCsvEscapeChar() {
        return csvEscapeChar;
    }

    /**
     * Generates missing values for each element of column definition.
     * @return The missing values for this
     */
    public List<String> getMissingValues() {
        return IntStream.range(0, getColumnDefinition().size()).
                mapToObj(i -> "").
                toList();
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/fetcher/xjc/PublicationMarketDocument.java
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Publication_MarketDocument", propOrder = {
    "mrid",
    "revisionNumber",
    "type",
    "senderMarketParticipantMRID",
    "senderMarketParticipantMarketRoleType",
    "receiverMarketParticipantMRID",
    "receiverMarketParticipantMarketRoleType",
    "createdDateTime",
    "periodTimeInterval",
    "domainMRID",
    "timeSeries"
})
@XmlRootElement(name = "Publication_MarketDocument")
public class PublicationMarketDocument {

    @XmlElement(name = "mRID", required = true)
    protected String mrid;
    @XmlElement(required = true)
    protected String revisionNumber;
    @XmlElement(required = true)
    protected String type;
    @XmlElement(name = "sender_MarketParticipant.mRID", required = true)
    protected PartyIDString senderMarketParticipantMRID;
    @XmlElement(name = "sender_MarketParticipant.marketRole.type", required = true)
    protected String senderMarketParticipantMarketRoleType;
    @XmlElement(name = "receiver_MarketParticipant.mRID")
    protected PartyIDString receiverMarketParticipantMRID;
    @XmlElement(name = "receiver_MarketParticipant.marketRole.type")
    protected String receiverMarketParticipantMarketRoleType;
    @XmlElement(required = true)
    @XmlSchemaType(name = "dateTime")
    protected XMLGregorianCalendar createdDateTime;
    @XmlElement(name = "period.timeInterval", required = true)
    protected ESMPDateTimeInterval periodTimeInterval;
    @XmlElement(name = "domain.mRID")
    protected AreaIDString domainMRID;
    @XmlElement(name = "TimeSeries", required = true)
    protected List<TimeSeries> timeSeries;

    /**
     * Gets the value of the mrid property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getMRID() {
        return mrid;
    }

    /**
     * Sets the value of the mrid property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setMRID(String value) {
        this.mrid = value;
    }

    /**
     * Gets the value of the revisionNumber property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getRevisionNumber() {
        return revisionNumber;
    }

    /**
     * Sets the value of the revisionNumber property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setRevisionNumber(String value) {
        this.revisionNumber = value;
    }

    /**
     * Gets the value of the type property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getType() {
        return type;
    }

    /**
     * Sets the value of the type property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setType(String value) {
        this.type = value;
    }

    /**
     * Gets the value of the senderMarketParticipantMRID property.
     *
     * @return
     *     possible object is
     *     {@link PartyIDString }
     *
     */
    public PartyIDString getSenderMarketParticipantMRID() {
        return senderMarketParticipantMRID;
    }

    /**
     * Sets the value of the senderMarketParticipantMRID property.
     *
     * @param value
     *     allowed object is
     *     {@link PartyIDString }
     *
     */
    public void setSenderMarketParticipantMRID(PartyIDString value) {
        this.senderMarketParticipantMRID = value;
    }

    /**
     * Gets the value of the senderMarketParticipantMarketRoleType property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getSenderMarketParticipantMarketRoleType() {
        return senderMarketParticipantMarketRoleType;
    }

    /**
     * Sets the value of the senderMarketParticipantMarketRoleType property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setSenderMarketParticipantMarketRoleType(String value) {
        this.senderMarketParticipantMarketRoleType = value;
    }

    /**
     * Gets the value of the receiverMarketParticipantMRID property.
     *
     * @return
     *     possible object is
     *     {@link PartyIDString }
     *
     */
    public PartyIDString getReceiverMarketParticipantMRID() {
        return receiverMarketParticipantMRID;
    }

    /**
     * Sets the value of the receiverMarketParticipantMRID property.
     *
     * @param value
     *     allowed object is
     *     {@link PartyIDString }
     *
     */
    public void setReceiverMarketParticipantMRID(PartyIDString value) {
        this.receiverMarketParticipantMRID = value;
    }

    /**
     * Gets the value of the receiverMarketParticipantMarketRoleType property.
     *
     * @return
     *     possible object is
     *     {@link String }
     *
     */
    public String getReceiverMarketParticipantMarketRoleType() {
        return receiverMarketParticipantMarketRoleType;
    }

    /**
     * Sets the value of the receiverMarketParticipantMarketRoleType property.
     *
     * @param value
     *     allowed object is
     *     {@link String }
     *
     */
    public void setReceiverMarketParticipantMarketRoleType(String value) {
        this.receiverMarketParticipantMarketRoleType = value;
    }

    /**
     * Gets the value of the createdDateTime property.
     *
     * @return
     *     possible object is
     *     {@link XMLGregorianCalendar }
     *
     */
    public XMLGregorianCalendar getCreatedDateTime() {
        return createdDateTime;
    }

    /**
     * Sets the value of the createdDateTime property.
     *
     * @param value
     *     allowed object is
     *     {@link XMLGregorianCalendar }
     *
     */
    public void setCreatedDateTime(XMLGregorianCalendar value) {
        this.createdDateTime = value;
    }

    /**
     * Gets the value of the periodTimeInterval property.
     *
     * @return
     *     possible object is
     *     {@link ESMPDateTimeInterval }
     *
     */
    public ESMPDateTimeInterval getPeriodTimeInterval() {
        return periodTimeInterval;
    }

    /**
     * Sets the value of the periodTimeInterval property.
     *
     * @param value
     *     allowed object is
     *     {@link ESMPDateTimeInterval }
     *
     */
    public void setPeriodTimeInterval(ESMPDateTimeInterval value) {
        this.periodTimeInterval = value;
    }

    /**
     * Gets the value of the domainMRID property.
     *
     * @return
     *     possible object is
     *     {@link AreaIDString }
     *
     */
    public AreaIDString getDomainMRID() {
        return domainMRID;
    }

    /**
     * Sets the value of the domainMRID property.
     *
     * @param value
     *     allowed object is
     *     {@link AreaIDString }
     *
     */
    public void setDomainMRID(AreaIDString value) {
        this.domainMRID = value;
    }

    /**
     * Gets the value of the timeSeries property.
     *
     *

     * This accessor method returns a reference to the live list,
     * not a snapshot. Therefore any modification you make to the
     * returned list will be present inside the JAXB object.
     * This is why there is not a set method for the timeSeries property.
     *
     *

     * For example, to add a new item, do as follows:
     *

     *    getTimeSeries().add(newItem);
     *

     *
     *
     *

     * Objects of the following type(s) are allowed in the list
     * {@link TimeSeries }
     *
     *
     */
    public List<TimeSeries> getTimeSeries() {
        if (timeSeries == null) {
            timeSeries = new ArrayList<TimeSeries>();
        }
        return this.timeSeries;
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/csv/transformers/UnavailabilityDocCSVTransformer.java
public class UnavailabilityDocCSVTransformer extends CSVTransformer {

    private static final Logger LOGGER = LogManager.getLogger(UnavailabilityDocCSVTransformer.class);

    private static record ReadedInfo(boolean isInTS, String nominalPower) {

    }
    ;

    static final String TRANSMISSION_UNAVAILABILITY_BASE_NAME = "outage_grid_";
    static final String UNAVAILABILITY_SCHEDULED_BN = "scheduled_outage_";
    static final String GENERATION_UNAVAILABILITY_BASE_NAME = "outage_generation_";
    static final String NOMINAL_OUTAGE_POWER = "outage_gen_nominal_";

    private final UnavailabilityMarketDocument doc;
    private final DocumentType docType;

    public UnavailabilityDocCSVTransformer(UnavailabilityMarketDocument doc, String csvSeparator, String csvEscapeChar) {
        super(csvSeparator, csvEscapeChar);
        this.doc = doc;
        docType = DocumentType.fromId(doc.getType());
    }

    @Override
    public boolean writeNextEntry(LocalDateTime timeStamp, BufferedWriter os) {
        ReadedInfo readedInfo = checkIsInOutage(timeStamp);
        if (readedInfo.isInTS) {
            boolean isScheduled = checkIsScheduled();
            String content;
            if (isScheduled) {
                content = getCsvSeparator() + "1" + getCsvSeparator() + "1";
            } else {
                content = getCsvSeparator() + "1" + getCsvSeparator() + "0";
            }
            switch (docType) {
                case GENERATION_UNAVAILABILITY:
                    String gen = readedInfo.nominalPower;
                    if (gen == null) {
                        throw new DataRetrievalRuntimeException("Generation unavailability without a nominal power");
                    }
                    content += getCsvSeparator() + gen;
                    break;
                case TRANSMISSION_UNAVAILABILITY:
                    // nothing to do
                    break;
                default:
                    throw new UnsupportedOperationException("Not suported outage column name for type " + docType.getId()
                            + " ; " + docType.getDescription());
            }
            try {
                os.write(content);
            } catch (IOException ex) {
                throw new DataRetrievalRuntimeException("Error writing timeStamp " + timeStamp.format(DateTimeFormatter.ISO_DATE), ex);
            }
            return true;
        } else {
            return false;
        }
    }

    private ReadedInfo checkIsInOutage(LocalDateTime timeStamp) {
        for (TimeSeries curTs : doc.getTimeSeries()) {
            LocalDateTime startDT = LocalDateTime.of(curTs.getStartDateAndOrTimeDate().getYear(),
                    curTs.getStartDateAndOrTimeDate().getMonth(), curTs.getStartDateAndOrTimeDate().getDay(),
                    curTs.getStartDateAndOrTimeTime().getHour(), curTs.getStartDateAndOrTimeTime().getMinute());
            LocalDateTime endDT = LocalDateTime.of(curTs.getEndDateAndOrTimeDate().getYear(),
                    curTs.getEndDateAndOrTimeDate().getMonth(), curTs.getEndDateAndOrTimeDate().getDay(),
                    curTs.getEndDateAndOrTimeTime().getHour(), curTs.getEndDateAndOrTimeTime().getMinute());
            if ((startDT.isBefore(timeStamp) && endDT.isAfter(timeStamp)) || startDT.equals(timeStamp)) {
                String nominalPower = null;
                if (curTs.getProductionRegisteredResourcePSRTypePowerSystemResourcesNominalP() != null) {
                    nominalPower = Float.toString(curTs.getProductionRegisteredResourcePSRTypePowerSystemResourcesNominalP().getValue());
                }
                return new ReadedInfo(true, nominalPower);
            }
        }
        return new ReadedInfo(false, "");
    }

    private boolean checkIsScheduled() {
        List<Reason> reasonList = doc.getReason();
        if (reasonList.size() != 1) {
            throw new UnsupportedOperationException("Expected one reason but there are " + reasonList.size() + " reasons");
        }
        Reason reason = reasonList.get(0);
        switch (reason.getCode()) {
            case "B19":
                return true;
            case "B20": // shutdown
            case "B18": // failure on generation unit
            case "A95": // complentary info?
                return false;
            default:
                printCurDoc();
                throw new UnsupportedOperationException("Unknow reason code " + reason.getCode());

        }
    }

    private void printCurDoc() {
        try {
            // just print th pb doucment
            JAXBContext jaxbContext = JAXBContext.newInstance(UnavailabilityMarketDocument.class);
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.marshal(doc, System.out);
        } catch (JAXBException ex) {
            LOGGER.catching(ex);
        }
    }

    @Override
    protected List<ColumnDefinition> computeColumnDef() {
        DocumentType docType = DocumentType.fromId(doc.getType());
        switch (docType) {
            case TRANSMISSION_UNAVAILABILITY:
                return doc.getTimeSeries().stream().
                        flatMap(ts -> {
                            if (ts.getAssetRegisteredResource().isEmpty()) {
                                AssetRegisteredResource res = new AssetRegisteredResource();
                                Area inDomain = Area.fromID(ts.getInDomainMRID().getValue());
                                Area outDomain = Area.fromID(ts.getOutDomainMRID().getValue());
                                ResourceIDString mrdid = new ResourceIDString();
                                mrdid.setValue("Unknown_asset_" + outDomain.getOptionCLIID() + "_" + inDomain.getOptionCLIID());
                                res.setMRID(mrdid);
                                return Stream.of(res);
                            }
                            return ts.getAssetRegisteredResource().stream();
                        }).
                        map(AssetRegisteredResource::getMRID).
                        map(ResourceIDString::getValue).
                        map(s -> Arrays.asList(new ColumnDefinition(TRANSMISSION_UNAVAILABILITY_BASE_NAME + s),
                        new ColumnDefinition(UNAVAILABILITY_SCHEDULED_BN + s))).
                        flatMap(List::stream).
                        toList();
            case GENERATION_UNAVAILABILITY:
                Area area = Area.fromID(doc.getTimeSeries().get(0).getBiddingZoneDomainMRID().getValue());
                return doc.getTimeSeries().stream().
                        map(ts -> ts.getProductionRegisteredResourceMRID()).
                        map(ResourceIDString::getValue).
                        map(s -> Arrays.asList(new ColumnDefinition(GENERATION_UNAVAILABILITY_BASE_NAME + s),
                        new ColumnDefinition(UNAVAILABILITY_SCHEDULED_BN + s),
                        new ColumnDefinition(area.getOptionCLIID() + "_" + NOMINAL_OUTAGE_POWER + s))).
                        flatMap(List::stream).
                        toList();
            default:
                throw new UnsupportedOperationException("Not suported outage column name for type " + docType.getId()
                        + " ; " + docType.getDescription());
        }

    }

    @Override
    public List<String> getMissingValues() {
        return IntStream.range(0, getColumnDefinition().size()).
                mapToObj(i -> "0").
                toList();
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/fetcher/inputs/Area.java
public enum Area {
    MONTENEGRO("10YCS-CG-TSO---S", "BZN|ME, CTA|ME, Montenegro (ME), MBA|ME, SCA|ME, LFA|ME",
            "Montenegro", "ME"),
    ITALY_CENTRE_SOUTH("10Y1001A1001A71M", "BZN|IT-Centre-South, SCA|IT-Centre-South, MBA|IT-Z-Centre-South",
            "Italy Centre South", "IT-CS"),
    BOSNIA("10YBA-JPCC-----D", "LFA|BA, BZN|BA, CTA|BA, Bosnia and Herz. (BA), SCA|BA, MBA|BA",
            "Bosnia", "BA"),
    SERBIA("10YCS-SERBIATSOV", "LFA|RS, SCA|RS, MBA|RS, Serbia (RS), CTA|RS, BZN|RS",
            "Serbia", "RS"),
    KOSOVO("10Y1001C--00100H", "BZN|XK, CTA|XK, Kosovo (XK), MBA|XK, LFB|XK, LFA|XK",
            "Kosovo", "XK"),
    ALBANIA("10YAL-KESH-----5", "LFB|AL, LFA|AL, BZN|AL, CTA|AL, Albania (AL), SCA|AL, MBA|AL",
            "Albania", "AL");

    private static final Logger LOGGER = LogManager.getLogger(Area.class);

    private static final Map<String, Area> CLIID_TO_AREA = new HashMap<>();
    private static final Map<String, Area> ID_TO_AREA = new HashMap<>();

    static {
        for (Area area : Area.values()) {
            CLIID_TO_AREA.put(area.getOptionCLIID(), area);
            ID_TO_AREA.put(area.getId(), area);
        }
    }

    private final String id;
    private final String longDescription;
    private final String prettyName;
    private final String optionCLIID;

    private Area(String id, String longDescription, String prettyName, String optionCLIID) {
        this.id = id;
        this.longDescription = longDescription;
        this.prettyName = prettyName;
        this.optionCLIID = optionCLIID;
    }

    /**
     *
     * @return
     */
    public String getId() {
        return id;
    }

    /**
     * The long description as presented on ENTSOE platform
     *
     * @return
     */
    public String getLongDescription() {
        return longDescription;
    }

    /**
     * Pretty name of the selected area
     *
     * @return
     */
    public String getPrettyName() {
        return prettyName;
    }

    /**
     * The ID on the CLI interface
     *
     * @return
     */
    public String getOptionCLIID() {
        return optionCLIID;
    }

    public static Area fromCLIID(String cliId) {
        if (cliId == null) {
            throw new IllegalArgumentException("Id cannot be null");
        }

        Area area = CLIID_TO_AREA.get(cliId);
        if (area == null) {
            throw new IllegalArgumentException("No enum instance found with id: " + cliId);
        }
        return area;
    }

    public static Area fromID(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Id cannot be null");
        }
        Area area = ID_TO_AREA.get(id);
        if (area == null) {
            throw new IllegalArgumentException("Non enum instance found with id:" + id);
        }
        return area;
    }

}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/csv/CSVGenerator.java
public class CSVGenerator {

    private static final Logger LOGGER = LogManager.getLogger(CSVGenerator.class);

    private static record ColumnBlock(List<ColumnDefinition> colDefs, List<String> missingValues){

        public ColumnBlock(List<ColumnDefinition> colDefs, List<String> missingValues) {
            if(colDefs.size() != missingValues.size()) {
                throw new DataRetrievalError("Malformed column signature. Column definition size of " + colDefs.size() +
                        " while " + missingValues.size());
            }
            this.colDefs = colDefs;
            this.missingValues = missingValues;
        }

    }

    private final String csvSepator;
    private final String csvEscapeChar;

    public CSVGenerator(String csvSepator, String csvEscapeChar) {
        this.csvSepator = csvSepator;
        this.csvEscapeChar = csvEscapeChar;
    }

    public void writeCSVFile(List<CSVTransformer> transformers, File targetFile, LocalDateTime startDate,
            LocalDateTime endDate, Duration timeStep) {

        List<ColumnBlock> colDefs = transformers.stream().
                map(t -> new ColumnBlock(t.getColumnDefinition(), t.getMissingValues())).
                distinct().// it can be many documents representing the same thing but at different
                toList();
        // column definitions as provided by transformers.getColumnDefinition
        // In the values, transformers are grouped by same column. In fact, they represent transformers of the same column, but at different time stamps
        Map<ColumnBlock, List<CSVTransformer>> transformersIndex = buildTransformersIndex(transformers);
        FileWriter fw = null;
        BufferedWriter bw = null;
        try {
            fw = new FileWriter(targetFile);
            bw = new BufferedWriter(fw);
            //write header
            writeHeader(colDefs, bw);
            LocalDateTime curDT = startDate;
            int idCount = 0;
            while (curDT.isBefore(endDate)) {
                bw.newLine();
                // id
                bw.write(String.valueOf(idCount));
                bw.write(csvSepator);
                // Time stamp
                bw.write(curDT.format(DateTimeFormatter.ISO_DATE_TIME));
                bw.flush();
                for (ColumnBlock colBlock : colDefs) {
                    boolean writedColumn = false;
                    if(transformersIndex.containsKey(colBlock)) {
                        List<CSVTransformer> associatedTransformer = transformersIndex.get(colBlock);
                        for(CSVTransformer curTransformer : associatedTransformer) {
                            boolean writed = curTransformer.writeNextEntry(curDT, bw);
                            if(writed) {
                                // when one transformer
                                writedColumn = true;
                                break;
                            }
                        }
                        if(!writedColumn) {
                            // None of the transformers could write for the given entry, filling it
                            for(int i=0; i<colBlock.colDefs.size(); i++) {
                                bw.write(csvSepator + colBlock.missingValues.get(i));
                            }
                        }
                    } else {
                        throw new DataRetrievalError("Uncoherent columns to transformers index. This should not happen");
                    }
                }
                bw.flush();
                idCount++;
                curDT = curDT.plus(timeStep);
            }
        } catch (IOException ex) {
            throw new DataRetrievalError(ex);
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException ex) {
                LOGGER.catching(ex);
            }
        }
    }

    private void writeHeader(List<ColumnBlock> colDefs, BufferedWriter bof) throws IOException {
        bof.write("id,time_stamp");
        colDefs.stream().
                map(ColumnBlock::colDefs).
                flatMap(List::stream).
                forEach((Consumer<ColumnDefinition>) c -> {
            try {
                bof.write(csvSepator);
                String colName = c.colName();
                if (colName.contains(this.csvEscapeChar)) {
                    throw new UnsupportedOperationException("Non suported, escapechar on column name " + colName);
                }
                if (colName.contains(this.csvSepator)) {
                    colName = csvEscapeChar + colName + csvEscapeChar;
                }
                bof.write(colName);
            } catch (IOException ex) {
                throw new DataRetrievalError(ex);
            }
        });
        bof.flush();
    }

    private Map<ColumnBlock, List<CSVTransformer>> buildTransformersIndex(List<CSVTransformer> transformers) {
        Map<ColumnBlock, List<CSVTransformer>> res = new HashMap<>();
        for(CSVTransformer curTransformer : transformers) {
            List<ColumnDefinition> colDefs = curTransformer.getColumnDefinition();
            List<String> missingValues = curTransformer.getMissingValues();
            ColumnBlock colBlock = new ColumnBlock(colDefs, missingValues);
            res.computeIfAbsent(colBlock, e -> new ArrayList<>());
            res.get(colBlock).add(curTransformer);
        }
        return res;
    }
}

src/main/java/tech/inceptive/ai4czc/entsoedataretrieval/csv/transformers/GLDocumentCSVTransformer.java
public class GLDocumentCSVTransformer extends CSVTransformer {

    private static final Logger LOGGER = LogManager.getLogger(GLDocumentCSVTransformer.class);

    private final GLMarketDocument doc;

    public GLDocumentCSVTransformer(String csvSeparator, String csvEscapeChar, GLMarketDocument doc) {
        super(csvSeparator, csvEscapeChar);
        this.doc = doc;
    }

    @Override
    public boolean writeNextEntry(LocalDateTime timeStamp, BufferedWriter os) {
        // start inclusive, end exclusive
        if (doc.getTimePeriodTimeInterval().getLDTStart().isEqual(timeStamp)
                || (doc.getTimePeriodTimeInterval().getLDTStart().isBefore(timeStamp)
                && doc.getTimePeriodTimeInterval().getLDTEnd().isAfter(timeStamp))) {
            // we asume that the whole information was requested for a single continuos interval
            List<MarkedTS> timeSeries = getPeriodForTimeStamp(doc.getTimeSeries(),timeStamp);
            if (timeSeries.isEmpty()) {
                // probably a missing value, do not write nothing by convention
                return false;
            }
            DocumentType docType = DocumentType.fromId(doc.getType());
            ProcessType processType = ProcessType.fromId(doc.getProcessProcessType());
            String content;
            switch (docType) {
                case SYSTEM_TOTAL_LOAD:
                    switch (processType) {
                        case REALISED:
                        case DAY_AHEAD:
                            if (timeSeries.size() != 1) {
                                throw new DataRetrievalError("Expected one point at time stamp "
                                        + timeStamp.format(DateTimeFormatter.ISO_DATE_TIME) + " but got " + timeSeries.size());
//                                int[] vals = pt.stream().mapToInt(cp -> cp.getQuantity().intValue()).toArray();
                                // TODO : end the case where we check if values are the same or different
//                                LOGGER.warn("Warning! Expected one valid point for processType {} but got {}. "
//                                        + "Values are different so stoping", processType.getDescription(), pt.size());
                            }
                            content = this.getCsvSeparator() + Integer.toString(timeSeries.get(0).getSelectedPointValue());// only one point expected
                            break;
                        case WEEK_AHEAD:
                            if (timeSeries.size() != 2) {
                                throw new DataRetrievalError("Expected two point at time stamp "
                                        + timeStamp.format(DateTimeFormatter.ISO_DATE_TIME) + " but got " + timeSeries.size());
                            }
                            int val1 = timeSeries.get(0).getSelectedPointValue();
                            int val2 = timeSeries.get(1).getSelectedPointValue();
                            content = getCsvSeparator() + Integer.toString(Math.min(val1, val2)) + getCsvSeparator() + Integer.toString(Math.max(val1, val2));
                            break;
                        default:
                            throw new UnsupportedOperationException("Non suported processType " + processType.name());
                    }
                    break;
                case ACTUAL_GENERATION_PER_TYPE:
                    Area curArea = Area.fromID(doc.getTimeSeries().get(0).getInBiddingZoneDomainMRID().getValue());
                    if (timeSeries.size() > getColumnDefinition().size()) {
                        throw new DataRetrievalError("Expected at maximun " + getColumnDefinition().size() + " elements for "
                                + " but got " + timeSeries.size() + " for " + curArea.getPrettyName() + " are on generation per type");
                    }
                    // index the found time series
                    Map<PsrType, String> indexedTs = new HashMap<>();
                    timeSeries.stream().forEach(ts -> indexedTs.put(PsrType.fromID(ts.ts().getMktPSRType().getPsrType()),
                            Integer.toString(ts.getSelectedPointValue())));
                    List<PsrType> prodTypes = PsrTypesByArea.INSTANCE.getProductionTypesForArea(curArea);
                    StringBuilder sb = new StringBuilder();
                    for (PsrType curProdType : prodTypes) {
                        sb.append(getCsvSeparator());
                        sb.append(indexedTs.getOrDefault(curProdType, ""));
                    }
                    content = sb.toString();
                    break;
                default:
                    throw new UnsupportedOperationException("Non suported document type " + docType.name());
            }
            try {
                os.write(content);
                return true;
            } catch (IOException ex) {
                throw new DataRetrievalRuntimeException("Error writing timeStamp " + timeStamp.format(DateTimeFormatter.ISO_DATE), ex);
            }
        }
        // nothing to write, out of bounds
        return false;
    }

    @Override
    protected List<ColumnDefinition> computeColumnDef() {
        List<ColumnDefinition> res = new ArrayList<>();
        DocumentType docType = DocumentType.fromId(doc.getType());
        ProcessType processType = ProcessType.fromId(doc.getProcessProcessType());
        Area curArea;
        switch (docType) {
            case SYSTEM_TOTAL_LOAD:
                curArea = Area.fromID(doc.getTimeSeries().get(0).getOutBiddingZoneDomainMRID().getValue());
                switch (processType) {
                    // TODO : add the geographic zone to the col name
                    case REALISED:
                        res.add(new ColumnDefinition("load_realised_actual_" + curArea.getOptionCLIID()));
                        break;
                    case DAY_AHEAD:
                        res.add(new ColumnDefinition("load_day_ahead_forecast_" + curArea.getOptionCLIID()));
                        break;
                    case WEEK_AHEAD:
                        res.add(new ColumnDefinition("load_week_ahead_forecast_min_" + curArea.getOptionCLIID()));
                        res.add(new ColumnDefinition("load_week_ahead_forecast_max_" + curArea.getOptionCLIID()));
                        break;
                    default:
                        throw new UnsupportedOperationException("Non suported processType " + processType.name());
                }
                break;
            case ACTUAL_GENERATION_PER_TYPE:
                curArea = Area.fromID(doc.getTimeSeries().get(0).getInBiddingZoneDomainMRID().getValue());
                // list all the generation mediums available
                // Two options :
                // 1 column per document, so we need to keep a track of the productions types per area
                // ALL columns in document, but there is a problem if a doc has not all the production types of a country...
                // ALL columns in document, keep track of existing columns so we can handle when there are missing values
                //
                //The last seems better
                String baseName = "_generation_";
                List<PsrType> prodTypes = PsrTypesByArea.INSTANCE.getProductionTypesForArea(curArea);
                for (PsrType curType : prodTypes) {
                    res.add(new ColumnDefinition(curArea.getOptionCLIID() + baseName + curType.getCsvName()));
                }
                break;
            default:
                throw new UnsupportedOperationException("Non suported docType " + docType.name());
        }
        return Collections.unmodifiableList(res);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 59 * hash + Objects.hashCode(this.doc);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final GLDocumentCSVTransformer other = (GLDocumentCSVTransformer) obj;
        return Objects.equals(this.doc, other.doc);
    }

}

Answer: transformers.add(new GLDocumentCSVTransformer(csvSeparator, csvEscapeChar, opt.get()));

<think>
The method fetchDataset builds a list of CSVTransformer objects for each fetched document. In the earlier if blocks (ACTUAL_LOAD, DAY_AHEAD_LOAD, WEAK_AHEAD_LOAD) the code adds a new GLDocumentCSVTransformer to the transformers list when an Optional document is present. The AGGREGATED_GENERATION_TYPE block follows the same pattern, so the next line after the provided snippet should add the transformer for the fetched aggregated generation type document.
</think>
transformers.add(new GLDocumentCSVTransformer(csvSeparator, csvEscapeChar, opt.get()));