SweCneExporter.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.raoresult.io.cne.swe;

import com.google.auto.service.AutoService;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider;
import com.powsybl.openrao.data.crac.api.CracCreationContext;
import com.powsybl.openrao.data.crac.io.cim.craccreator.CimCracCreationContext;
import com.powsybl.openrao.data.raoresult.api.RaoResult;
import com.powsybl.openrao.data.raoresult.api.io.Exporter;
import com.powsybl.openrao.data.raoresult.io.cne.swe.xsd.CriticalNetworkElementMarketDocument;
import com.powsybl.openrao.data.crac.api.Crac;
import org.apache.commons.lang3.NotImplementedException;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import javax.xml.namespace.QName;
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.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import static com.powsybl.openrao.data.raoresult.io.cne.commons.CneConstants.*;
import static com.powsybl.openrao.data.raoresult.io.cne.swe.SweCneUtil.SWE_CNE_EXPORT_PROPERTIES_PREFIX;

/**
 * SWE-CNE Rao Result exporter in XML format.
 * <p/>
 * Optional properties:
 * <ul>
 *     <li>
 *         <i>relative-positive-margins</i>: boolean (default is "false").
 *     </li>
 *     <li>
 *         <i>with-loop-flows</i>: boolean (default is "false").
 *     </li>
 *     <li>
 *         <i>mnec-acceptable-margin-diminution</i>: double (default is "0").
 *     </li>
 * </ul>
 *
 * Required properties:
 * <ul>
 *     <li>
 *         <i>document-id</i>: string
 *     </li>
 *     <li>
 *         <i>revision-number</i>: integer
 *     </li>
 *     <li>
 *         <i>domain-id</i>: string
 *     </li>
 *     <li>
 *         <i>process-type</i>: string
 *     </li>
 *     <li>
 *         <i>sender-id</i>: string
 *     </li>
 *     <li>
 *         <i>sender-role</i>: string
 *     </li>
 *     <li>
 *         <i>receiver-id</i>: string
 *     </li>
 *     <li>
 *         <i>receiver-role</i>: string
 *     </li>
 *     <li>
 *         <i>time-interval</i>: string
 *     </li>
 * </ul>
 * @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
@AutoService(Exporter.class)
public class SweCneExporter implements Exporter {
    @Override
    public String getFormat() {
        return "SWE-CNE";
    }

    @Override
    public Set<String> getRequiredProperties() {
        return CNE_REQUIRED_PROPERTIES.stream().map(propertyName -> SWE_CNE_EXPORT_PROPERTIES_PREFIX + propertyName).collect(Collectors.toSet());
    }

    @Override
    public Class<? extends CracCreationContext> getCracCreationContextClass() {
        return CimCracCreationContext.class;
    }

    @Override
    public void exportData(RaoResult raoResult, CracCreationContext cracCreationContext, Properties properties, OutputStream outputStream) {
        validateDataToExport(cracCreationContext, properties);
        SweCne cne = new SweCne((CimCracCreationContext) cracCreationContext, raoResult, properties);
        cne.generate();
        CriticalNetworkElementMarketDocument marketDocument = cne.getMarketDocument();
        StringWriter stringWriter = new StringWriter();

        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(CriticalNetworkElementMarketDocument.class);
            Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

            // format the XML output
            jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            jaxbMarshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, CNE_XSD_2_3);

            QName qName = new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, CNE_TAG);
            JAXBElement<CriticalNetworkElementMarketDocument> root = new JAXBElement<>(qName, CriticalNetworkElementMarketDocument.class, marketDocument);

            jaxbMarshaller.marshal(root, stringWriter);

            String result = stringWriter.toString().replace("xsi:" + CNE_TAG, CNE_TAG);

            if (!validateCNESchema(result)) {
                OpenRaoLoggerProvider.TECHNICAL_LOGS.warn("CNE output doesn't fit the xsd.");
            }

            outputStream.write(result.getBytes());

        } catch (JAXBException | IOException e) {
            throw new OpenRaoException("Could not write SWE CNE file.");
        }
    }

    @Override
    public void exportData(RaoResult raoResult, Crac crac, Properties properties, OutputStream outputStream) {
        throw new NotImplementedException("CracCreationContext is required for CNE export.");
    }

    private static String getSchemaFile(String schemaName) {
        return Objects.requireNonNull(SweCneExporter.class.getResource("/xsd/" + schemaName)).toExternalForm();
    }

    public static boolean validateCNESchema(String xmlContent) {

        try {
            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");

            Source[] source = {new StreamSource(getSchemaFile(CNE_XSD_2_3)),
                               new StreamSource(getSchemaFile(CODELISTS_XSD)),
                               new StreamSource(getSchemaFile(LOCALTYPES_XSD))};
            Schema schema = factory.newSchema(source);
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");

            Validator validator = schema.newValidator();
            validator.validate(new StreamSource(new StringReader(xmlContent)));
        } catch (IOException | SAXException e) {
            OpenRaoLoggerProvider.TECHNICAL_LOGS.warn("Exception: {}", e.getMessage());
            return false;
        }
        return true;
    }
}