FbConstraintImporter.java

/*
 * Copyright (c) 2022, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.powsybl.openrao.data.crac.io.fbconstraint;

import com.google.auto.service.AutoService;
import com.powsybl.iidm.network.Network;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.data.crac.api.CracCreationContext;
import com.powsybl.openrao.data.crac.api.io.Importer;
import com.powsybl.openrao.data.crac.api.parameters.CracCreationParameters;
import com.powsybl.openrao.data.crac.io.fbconstraint.xsd.FlowBasedConstraintDocument;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Unmarshaller;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.*;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;

import static java.lang.Integer.parseInt;

/**
 * @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
 * @author Baptiste Seguinot{@literal <baptiste.seguinot at rte-france.com>}
 */
@AutoService(Importer.class)
public class FbConstraintImporter implements Importer {

    private static final Logger LOGGER = LoggerFactory.getLogger(FbConstraintImporter.class);
    private static final String XML_EXTENSION = "xml";
    private static final String XML_SCHEMA_VERSION = "flowbasedconstraintdocument-";
    private static final String FLOWBASED_CONSTRAINT_V11_SCHEMA_FILE = "/xsd/validation/flowbasedconstraintdocument-11.xsd";
    private static final String FLOWBASED_CONSTRAINT_V18_SCHEMA_FILE = "/xsd/validation/flowbasedconstraintdocument-18.xsd";
    private static final String FLOWBASED_CONSTRAINT_V23_SCHEMA_FILE = "/xsd/flowbasedconstraintdocument-23.xsd";
    private static final String ETSO_CODE_LIST_SCHEMA_FILE = "/xsd/etso-code-lists.xsd";
    private static final String ETSO_CORE_CMPTS_SCHEMA_FILE = "/xsd/etso-core-cmpts.xsd";

    @Override
    public String getFormat() {
        return "FlowBasedConstraintDocument";
    }

    private FlowBasedConstraintDocument importNativeCrac(InputStream inputStream) {
        try {
            byte[] bytes = getBytesFromInputStream(inputStream);
            JAXBContext jaxbContext = JAXBContext.newInstance(FlowBasedConstraintDocument.class);
            Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
            return (FlowBasedConstraintDocument) jaxbUnmarshaller.unmarshal(new ByteArrayInputStream(bytes));
        } catch (JAXBException | IOException e) {
            throw new OpenRaoException(e);
        }
    }

    @Override
    public boolean exists(String filename, InputStream inputStream) {
        if (!FilenameUtils.getExtension(filename).equals(XML_EXTENSION)) {
            return false;
        }
        try {
            byte[] bytes = getBytesFromInputStream(inputStream);
            int flowBasedDocumentVersion = flowBasedDocumentVersion(new ByteArrayInputStream(bytes));
            String schemaFile = schemaVersion(flowBasedDocumentVersion);

            if (schemaFile != null) {
                Source xmlFile = new StreamSource(new ByteArrayInputStream(bytes));
                // The following line triggers sonar issue java:S2755 which prevents us from accessing XSD schema files
                SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); //NOSONAR

                Schema schema = schemaFactory.newSchema(new Source[]{
                    new StreamSource(Objects.requireNonNull(FbConstraintImporter.class.getResource(schemaFile)).toExternalForm()),
                    new StreamSource(Objects.requireNonNull(FbConstraintImporter.class.getResource(ETSO_CORE_CMPTS_SCHEMA_FILE)).toExternalForm()),
                    new StreamSource(Objects.requireNonNull(FbConstraintImporter.class.getResource(ETSO_CODE_LIST_SCHEMA_FILE)).toExternalForm())
                });

                Validator validator = schema.newValidator();
                validator.validate(xmlFile);
                LOGGER.info("FlowBased Constraint Document format is valid");
                return true;
            } else {
                LOGGER.debug("The schema can't be validated because no xsd file is available. Validity check is skipped.");
                return false;
            }
        } catch (MalformedURLException e) {
            throw new OpenRaoException("URL error");
        } catch (SAXException e) {
            LOGGER.debug("FlowBased Constraint Document format is NOT valid. Reason: {}", e.getMessage());
            return false;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public CracCreationContext importData(InputStream inputStream, CracCreationParameters cracCreationParameters, Network network) {
        return new FbConstraintCracCreator().createCrac(importNativeCrac(inputStream), network, cracCreationParameters);
    }

    private int flowBasedDocumentVersion(InputStream inputStream) {
        int schemaVersion = Integer.MIN_VALUE;

        try (InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             BufferedReader br = new BufferedReader(isr)) {
            Optional<String> xsdLine = br.lines().filter(e -> e.contains(XML_SCHEMA_VERSION)).findFirst();
            if (xsdLine.isPresent()) {
                String[] versionNumber = xsdLine.get().split(XML_SCHEMA_VERSION);
                schemaVersion = parseInt(versionNumber[1].substring(0, 2));
            }
        } catch (IOException e) {
            LOGGER.debug("The schema can't be validated because the xml header is not one of a flow-based constraint document.");
        }
        return schemaVersion;
    }

    private String schemaVersion(int flowBasedDocumentVersion) {
        if (flowBasedDocumentVersion >= 23) {
            return FLOWBASED_CONSTRAINT_V23_SCHEMA_FILE;
        } else if (flowBasedDocumentVersion >= 17) {
            return FLOWBASED_CONSTRAINT_V18_SCHEMA_FILE;
        } else if (flowBasedDocumentVersion == 11) {
            return FLOWBASED_CONSTRAINT_V11_SCHEMA_FILE;
        } else {
            LOGGER.debug("Flow-based constraint document with version {} are not handled by the FbConstraintImporter", flowBasedDocumentVersion);
            return null;
        }
    }

    private static byte[] getBytesFromInputStream(InputStream inputStream) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        org.apache.commons.io.IOUtils.copy(inputStream, baos);
        return baos.toByteArray();
    }
}