ExportXmlCompare.java
/**
* Copyright (c) 2020, 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/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.cgmes.conversion.test.export;
import com.powsybl.cgmes.model.CgmesNames;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.builder.Input;
import org.xmlunit.diff.*;
import org.xmlunit.util.IsNullPredicate;
import org.xmlunit.util.Linqy;
import org.xmlunit.util.Nodes;
import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.powsybl.cgmes.model.CgmesNamespace.RDF_NAMESPACE;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
*/
final class ExportXmlCompare {
private ExportXmlCompare() {
}
static boolean compareNetworks(Path expected, Path actual) {
try (InputStream expectedIs = Files.newInputStream(expected);
InputStream actualIs = Files.newInputStream(actual)) {
return compareNetworks(expectedIs, actualIs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static boolean compareNetworks(Path expected, Path actual, DifferenceEvaluator knownDiffs) {
try (InputStream expectedIs = Files.newInputStream(expected);
InputStream actualIs = Files.newInputStream(actual)) {
return compareNetworks(expectedIs, actualIs, knownDiffs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static boolean compareEQNetworks(Path expected, Path actual, DifferenceEvaluator knownDiffs) {
try (InputStream expectedIs = Files.newInputStream(expected);
InputStream actualIs = Files.newInputStream(actual)) {
return compareEQNetworks(expectedIs, actualIs, knownDiffs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static boolean compareNetworks(InputStream expected, InputStream actual) {
return compareNetworks(expected, actual, DifferenceEvaluators.chain(
DifferenceEvaluators.Default,
ExportXmlCompare::numericDifferenceEvaluator));
}
static boolean compareNetworks(InputStream expected, InputStream actual, DifferenceEvaluator knownDiffs) {
Source control = Input.fromStream(expected).build();
Source test = Input.fromStream(actual).build();
Diff diff = DiffBuilder
.compare(control)
.withTest(test)
.ignoreWhitespace()
.ignoreComments()
.withAttributeFilter(ExportXmlCompare::isConsideredForNetwork)
.withDifferenceEvaluator(knownDiffs)
.withComparisonListeners(ExportXmlCompare::debugComparison)
.build();
assertFalse(diff.hasDifferences());
return !diff.hasDifferences();
}
static boolean compareEQNetworks(InputStream expected, InputStream actual, DifferenceEvaluator knownDiffs) {
Source control = Input.fromStream(expected).build();
Source test = Input.fromStream(actual).build();
Diff diff = DiffBuilder
.compare(control)
.withTest(test)
.ignoreWhitespace()
.ignoreComments()
.withAttributeFilter(ExportXmlCompare::isConsideredForEQNetwork)
.withNodeFilter(ExportXmlCompare::isConsideredEQNode)
.withDifferenceEvaluator(knownDiffs)
.withComparisonListeners(ExportXmlCompare::debugComparison)
.build();
assertFalse(diff.hasDifferences());
return !diff.hasDifferences();
}
static boolean isConsideredForNetwork(Attr attr) {
if (attr.getLocalName().equals("forecastDistance")) {
return false;
}
if (attr.getLocalName().equals("value")) {
Element e = attr.getOwnerElement();
return !e.getLocalName().equals("property")
|| !"CGMES.TP_ID".equals(e.getAttribute("name")) && !"CGMES.SSH_ID".equals(e.getAttribute("name"));
}
return true;
}
private static boolean isConsideredForEQNetwork(Attr attr) {
String elementName = attr.getOwnerElement().getLocalName();
boolean ignored = false;
if (elementName != null) {
if (elementName.equals("danglingLine")) {
ignored = true;
} else if (elementName.startsWith("network")) {
ignored = attr.getLocalName().equals("id") || attr.getLocalName().equals("forecastDistance") || attr.getLocalName().equals("caseDate") || attr.getLocalName().equals("sourceFormat");
} else if (elementName.startsWith("voltageLevel")) {
ignored = attr.getLocalName().equals("topologyKind") || attr.getLocalName().equals("lowVoltageLimit") || attr.getLocalName().equals("highVoltageLimit");
} else if (elementName.startsWith("hvdcLine")) {
ignored = attr.getLocalName().equals("converterStation1") || attr.getLocalName().equals("converterStation2") || attr.getLocalName().equals("convertersMode");
} else if (elementName.contains("TapChanger")) {
ignored = attr.getLocalName().equals("regulating") || attr.getLocalName().equals("regulationMode") || attr.getLocalName().equals("regulationValue")
|| attr.getLocalName().equals("targetV") || attr.getLocalName().equals("targetDeadband");
} else if (elementName.startsWith("generator")) {
// ratedS is optional for generators,
// but we always export it to keep up with the quality of datasets rules.
// So we do not enforce this attribute to be equal in the original and exported network
ignored = attr.getLocalName().equals("ratedS");
} else {
ignored = attr.getLocalName().contains("node") || attr.getLocalName().contains("bus") || attr.getLocalName().contains("Bus");
}
}
return !ignored;
}
// Present in small grid HVDC
private static final Set<String> SMALLGRID_SUBSTATIONS = Stream.of(
"68-116_SUBSTATION",
"71-73_SUBSTATION",
"12-117_SUBSTATION")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> SMALLGRID_VOLTAGELEVELS = Stream.of(
"68-116_VL",
"71-73_VL",
"12-117_VL")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> SMALLGRID_LINES = Stream.of(
"68-116_DL",
"71-73_DL",
"12-117_DL")
.collect(Collectors.toCollection(HashSet::new));
// Present in mini grid
private static final Set<String> MINIGRID_SUBSTATIONS = Stream.of(
"XQ2-N5_SUBSTATION",
"XQ1-N1_SUBSTATION")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> MINIGRID_VOLTAGELEVELS = Stream.of(
"XQ2-N5_VL",
"XQ1-N1_VL")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> MINIGRID_LINES = Stream.of(
"XQ2-N5_DL",
"XQ1-N1_DL")
.collect(Collectors.toCollection(HashSet::new));
// Present in micro grid
private static final Set<String> MICROGRID_SUBSTATIONS = Stream.of(
"BE-Line_1_SUBSTATION",
"BE-Line_3_SUBSTATION",
"BE-Line_4_SUBSTATION",
"BE-Line_5_SUBSTATION",
"BE-Line_7_SUBSTATION")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> MICROGRID_VOLTAGELEVELS = Stream.of(
"BE-Line_1_VL",
"BE-Line_3_VL",
"BE-Line_4_VL",
"BE-Line_5_VL",
"BE-Line_7_VL")
.collect(Collectors.toCollection(HashSet::new));
private static final Set<String> MICROGRID_LINES = Stream.of(
"BE-Line_1_DL",
"BE-Line_3_DL",
"BE-Line_4_DL",
"BE-Line_5_DL",
"BE-Line_7_DL")
.collect(Collectors.toCollection(HashSet::new));
private static boolean isConsideredEQNode(Node n) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
String name = n.getLocalName();
return !name.startsWith("danglingLine") && !name.contains("BreakerTopology") && !name.startsWith("internalConnection")
&& !name.startsWith("property") && !name.startsWith("regulatingTerminal") && !name.startsWith("terminalRef")
&& !isDanglingLineConversion(n) && !isNodeBreakerProperty(n);
}
return false;
}
private static boolean isNodeBreakerProperty(Node n) {
if (n.getAttributes().getNamedItem("name") != null && n.getAttributes().getNamedItem("name").getTextContent() != null) {
return n.getAttributes().getNamedItem("name").getTextContent().equals("CGMESModelDetail");
}
return false;
}
private static boolean isDanglingLineConversion(Node n) {
String name = n.getLocalName();
return name.startsWith("substation") && SMALLGRID_SUBSTATIONS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("voltageLevel") && SMALLGRID_VOLTAGELEVELS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("line") && SMALLGRID_LINES.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("substation") && MINIGRID_SUBSTATIONS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("voltageLevel") && MINIGRID_VOLTAGELEVELS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("line") && MINIGRID_LINES.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("substation") && MICROGRID_SUBSTATIONS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("voltageLevel") && MICROGRID_VOLTAGELEVELS.contains(n.getAttributes().getNamedItem("name").getTextContent())
|| name.startsWith("line") && MICROGRID_LINES.contains(n.getAttributes().getNamedItem("name").getTextContent());
}
static DiffBuilder diffSSH(InputStream expected, InputStream actual, DifferenceEvaluator de) {
// Original CGMES PhaseTapChangerLinear, PhaseTapChangerSymmetrical and PhaseTapChangerAsymmetrical
// have been exported as PhaseTapChangerTabular.
// We just check that the objects being compared keep a PhaseTapChanger class, not exactly the same one.
DifferenceEvaluator de1 = DifferenceEvaluators.chain(de, (comparison, result) -> {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.ELEMENT_TAG_NAME) {
if (comparison.getControlDetails().getTarget().getLocalName().startsWith("PhaseTapChanger") &&
comparison.getTestDetails().getTarget().getLocalName().startsWith("PhaseTapChanger")) {
return ComparisonResult.EQUAL;
}
}
return result;
});
return selectingEquivalentSshObjects(ignoringNonPersistentSshIds(withSelectedSshNodes(diff(expected, actual, de1))));
}
static boolean compareSSH(InputStream expected, InputStream actual, DifferenceEvaluator knownDiffs) {
return onlyNodeListSequenceDiffs(compare(diffSSH(expected, actual, knownDiffs).checkForIdentical()));
}
static ComparisonResult ignoringCgmesMetadataModels(Comparison comparison, ComparisonResult result) {
// FIXME(Luma) Refactoring in progress. cgmes metadata models should be serialized in the same way, maybe we could check something else
if (result == ComparisonResult.DIFFERENT) {
String xpath = comparison.getControlDetails().getXPath();
if (xpath == null) {
xpath = comparison.getControlDetails().getParentXPath();
}
if (xpath != null && xpath.contains("cgmesMetadataModels")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ensuringIncreasedModelVersion(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Node control = comparison.getControlDetails().getTarget();
if (comparison.getType() == ComparisonType.TEXT_VALUE && control.getParentNode().getLocalName().equals("Model.version")) {
Node test = comparison.getTestDetails().getTarget();
int vcontrol = Integer.parseInt(control.getTextContent());
int vtest = Integer.parseInt(test.getTextContent());
if (vtest == vcontrol + 1) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult sameScenarioTime(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
Node control = comparison.getControlDetails().getTarget();
Node test = comparison.getTestDetails().getTarget();
if (test != null && control != null && control.getParentNode().getLocalName().equals("Model.scenarioTime")) {
String scontrol = control.getTextContent();
String stest = test.getTextContent();
ZonedDateTime dcontrol = ZonedDateTime.parse(scontrol, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").withZone(ZoneOffset.UTC));
ZonedDateTime dtest = ZonedDateTime.parse(stest);
if (dcontrol.equals(dtest)) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringCreatedTime(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
Node control = comparison.getControlDetails().getTarget();
Node test = comparison.getTestDetails().getTarget();
if (test != null && control != null && control.getParentNode().getLocalName().equals("Model.created")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringSynchronousMachinesSVCsWithTargetDeadband(Comparison comparison, ComparisonResult result) {
// In micro grid there are two regulating controls for synchronous machines
// that have a target deadband of 0.5
// PowSyBl does not allow deadband for generator regulation
// we export deadband 0
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
Node control = comparison.getControlDetails().getTarget();
// XPtah is RDF/RegulatingControl/RegulatingControl.targetDeadband/text()
// check that parent of current text node is a regulating control target deadband,
// then check grand parent is one of the known diff identifiers
if (control.getParentNode() != null
&& control.getParentNode().getNodeType() == Node.ELEMENT_NODE
&& control.getParentNode().getLocalName().equals("RegulatingControl.targetDeadband")) {
Node rccontrol = control.getParentNode().getParentNode();
String about = rccontrol.getAttributes().getNamedItemNS(RDF_NAMESPACE, "about").getTextContent();
if (about.equals("#_84bf5be8-eb59-4555-b131-fce4d2d7775d")
|| about.equals("#_6ba406ce-78cf-4485-9b01-a34e584f1a8d")
|| about.equals("#_caf65447-3cfb-48d7-aaaa-cd9af3d34261")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringHvdcLinePmax(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Node control = comparison.getControlDetails().getTarget();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
if (control != null && control.getLocalName().equals("maxP")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringNonEQ(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Node control = comparison.getControlDetails().getTarget();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
if (control != null && control.getLocalName().equals("geographicalTags")) {
return ComparisonResult.EQUAL;
} else if (control != null && control.getLocalName().equals("targetQ")) {
return ComparisonResult.EQUAL;
} else if (comparison.getControlDetails().getXPath().contains("temporaryLimit")) {
// If the control node is a temporary limit, the order depends on the name attribute,
// this attribute is generated as a unique identifier in the EQ export to avoid duplicates in CGMES
return ComparisonResult.EQUAL;
}
} else if (comparison.getType() == ComparisonType.CHILD_NODELIST_LENGTH) {
if (control.getLocalName().equals("network") || control.getLocalName().equals("shunt")
|| control.getLocalName().equals("generator")) {
return ComparisonResult.EQUAL;
}
} else if (comparison.getType() == ComparisonType.ELEMENT_TAG_NAME) {
if (control.getLocalName().equals("temporaryLimits")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
return result;
}
static ComparisonResult ignoringOperationalLimitsGroupId(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Node control = comparison.getControlDetails().getTarget();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
if (comparison.getControlDetails().getXPath().contains("operationalLimitsGroup") && control != null
&& control.getLocalName().equals("id")) {
return ComparisonResult.EQUAL;
} else if (control != null && control.getLocalName().contains("selectedOperationalLimitsGroupId")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringJunctionOrBusbarTerminals(Comparison comparison, ComparisonResult result) {
// If control node is a terminal of a junction, ignore the difference
// Means that we also have to ignore length of children of RDF element
if (result == ComparisonResult.DIFFERENT) {
if (comparison.getType() == ComparisonType.CHILD_NODELIST_LENGTH
&& comparison.getControlDetails().getTarget().getLocalName().equals("RDF")) {
return ComparisonResult.EQUAL;
} else if (isJunctionOrBusbarTerminal(comparison.getControlDetails().getTarget())) {
return ComparisonResult.EQUAL;
}
}
return result;
}
// Present in small grid
private static final Set<String> JUNCTIONS_TERMINALS = Stream.of(
"#_65a95678-1819-43cd-94d2-a03756822725",
"#_5c96df9d-39fa-46fe-a424-7b3e50184f79",
"#_9c6a1e49-17b2-46fc-8e32-c95dec33eba8")
.collect(Collectors.toCollection(HashSet::new));
// Present in micro grid
private static final Set<String> BUSBAR_TERMINALS = Stream.of(
"#_3c6d83a3-b5f9-41a2-a3d9-cf15d903ed0a",
"#_ad794c0e-b9ec-420b-ada1-97680e3dde05",
"#_a1b46f53-86f1-497e-bf57-c3b6268bcd6c",
"#_65b8c937-9b25-4b9e-addf-602dbc1337f9",
"#_62fc0a4e-00aa-4bf7-b1a0-3a5b2c0b5492",
"#_800ada75-8c8c-4568-aec5-20f799e45f3c",
"#_fa9e0f4d-8a2f-45e1-9e36-3611600d1c94",
"#_302fe23a-f64d-41bd-8a81-78130433916d",
"#_8f1c492f-a7cc-4160-9a14-54f1743e4850")
.collect(Collectors.toCollection(HashSet::new));
private static boolean isJunctionOrBusbarTerminal(Node n) {
if (n != null && n.getNodeType() == Node.ELEMENT_NODE && n.getLocalName().equals(CgmesNames.TERMINAL)) {
String about = n.getAttributes().getNamedItemNS(RDF_NAMESPACE, "about").getTextContent();
return JUNCTIONS_TERMINALS.contains(about) || BUSBAR_TERMINALS.contains(about);
}
return false;
}
private static boolean onlyNodeListSequenceDiffs(Diff diff) {
for (Difference d : diff.getDifferences()) {
if (ComparisonType.CHILD_NODELIST_SEQUENCE != d.getComparison().getType()) {
return false;
}
if (ComparisonResult.SIMILAR != d.getResult()) {
return false;
}
}
return true;
}
static ComparisonResult ignoringFullModelAbout(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
Comparison.Detail control = comparison.getControlDetails();
if (control.getXPath().contains("FullModel")
&& control.getTarget().getLocalName().equals("about")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringFullModelDependentOn(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("FullModel") && cxpath.contains("Model.DependentOn")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringFullModelModelingAuthoritySet(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("FullModel") && cxpath.contains("Model.modelingAuthoritySet")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringSubstationNumAttributes(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.ELEMENT_NUM_ATTRIBUTES) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("network") && cxpath.contains("substation")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringSubstationLookup(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.ATTR_NAME_LOOKUP) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("network") && cxpath.contains("substation")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringGeneratorAttributes(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.ATTR_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("generator")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringLoadChildNodeListLength(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_NODELIST_LENGTH) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("load")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringRdfChildNodeListLength(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_NODELIST_LENGTH) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("RDF")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringChildLookupNull(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_LOOKUP) {
if (comparison.getControlDetails().getXPath() == null) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringTextValueShuntCompensatorControlEnabled(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("ShuntCompensator") && cxpath.contains("controlEnabled")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringRdfChildLookupTerminal(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_LOOKUP) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("RDF") && cxpath.contains("Terminal")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringRdfChildLookupEquivalentInjection(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_LOOKUP) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("RDF") && cxpath.contains(CgmesNames.EQUIVALENT_INJECTION)) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringStaticVarCompensatorControlEnabled(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("StaticVarCompensator") && cxpath.contains(".controlEnabled")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringStaticVarCompensatorQ(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("StaticVarCompensator.q")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringRegulatingControl(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("RDF") && cxpath.contains("RegulatingControl")) {
return ComparisonResult.EQUAL;
}
} else if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.CHILD_LOOKUP) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("RDF") && cxpath.contains("RegulatingControl")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringTextValueTapChangerControlEnabled(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("TapChanger") && cxpath.contains("controlEnabled")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringConformLoad(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.ELEMENT_TAG_NAME) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("ConformLoad")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringTextValueEquivalentInjection(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT && comparison.getType() == ComparisonType.TEXT_VALUE) {
String cxpath = comparison.getControlDetails().getXPath();
if (cxpath != null && cxpath.contains("EquivalentInjection")) {
return ComparisonResult.EQUAL;
}
}
return result;
}
static ComparisonResult ignoringOperationalLimitIds(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Comparison.Detail control = comparison.getControlDetails();
String cxpath = control.getXPath();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
String cname = control.getTarget().getLocalName();
if (cxpath.contains("CurrentLimit") && (cname.equals("ID") || cname.equals("resource"))) {
return ComparisonResult.EQUAL;
} else if (cxpath.contains("OperationalLimit") && cname.equals("ID")) {
return ComparisonResult.EQUAL;
}
} else if (comparison.getType() == ComparisonType.TEXT_VALUE) {
if (cxpath.contains("CurrentLimit") || cxpath.contains("OperationalLimit")) {
if (control.getTarget().getParentNode().getLocalName().endsWith(".mRID")) {
return ComparisonResult.EQUAL;
}
}
}
}
return result;
}
static ComparisonResult ignoringLoadAreaIds(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Comparison.Detail control = comparison.getControlDetails();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
if (control.getXPath().contains("SubLoadArea")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
} else if (control.getXPath().contains("LoadArea")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
} else if (control.getXPath().contains("SubLoadArea.LoadArea")
&& control.getTarget().getLocalName().equals("resource")) {
return ComparisonResult.EQUAL;
}
} else if (comparison.getType() == ComparisonType.TEXT_VALUE) {
if (control.getXPath().contains("SubLoadArea") || control.getXPath().contains("LoadArea")) {
if (control.getTarget().getParentNode().getLocalName().endsWith(".mRID")) {
return ComparisonResult.EQUAL;
}
}
}
}
return result;
}
static ComparisonResult ignoringEnergyAreaIdOfControlArea(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
Comparison.Detail control = comparison.getControlDetails();
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
if (control.getXPath().contains("ControlArea.EnergyArea")
&& control.getTarget().getLocalName().equals("resource")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult ignoringSVIds(Comparison comparison, ComparisonResult result) {
if (result == ComparisonResult.DIFFERENT) {
if (comparison.getType() == ComparisonType.ATTR_VALUE) {
Comparison.Detail control = comparison.getControlDetails();
if (control.getXPath().contains("SvVoltage")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
} else if (control.getXPath().contains("SvPowerFlow")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
} else if (control.getXPath().contains("SvShuntCompensatorSections")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
} else if (control.getXPath().contains("SvStatus")
&& control.getTarget().getLocalName().equals("ID")) {
return ComparisonResult.EQUAL;
}
}
}
return result;
}
static ComparisonResult numericDifferenceEvaluator(Comparison comparison, ComparisonResult result) {
// If both control and test nodes are text that can be converted to a number
// check that they represent the same number
if (result == ComparisonResult.DIFFERENT
&& (comparison.getType() == ComparisonType.TEXT_VALUE || comparison.getType() == ComparisonType.ATTR_VALUE)) {
// If different result for control and test values, check node
// (parent for elements, target for attributes)
Node n = comparison.getControlDetails().getTarget();
if (n.getNodeType() == Node.TEXT_NODE) {
n = n.getParentNode();
}
if (isTextContentNumeric(n)) {
try {
double control = Double.parseDouble(comparison.getControlDetails().getTarget().getTextContent());
try {
double test = Double.parseDouble(comparison.getTestDetails().getTarget().getTextContent());
if (Math.abs(control - test) < toleranceForNumericContent(n)) {
return ComparisonResult.EQUAL;
}
} catch (NumberFormatException x) {
// not numeric, keep previous comparison result
}
} catch (NumberFormatException x) {
// not numeric, keep previous comparison result
}
}
}
return result;
}
private static boolean isTextContentNumeric(Node n) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
String name = n.getLocalName();
return name.endsWith(".p") || name.endsWith(".q")
|| name.endsWith(".v") || name.endsWith(".angle")
|| name.endsWith(".sections")
|| name.endsWith("TapChanger.step")
|| name.endsWith(".regulationTarget") || name.endsWith(".targetValue") || name.endsWith(".targetDeadband")
|| name.endsWith("pTolerance")
|| name.endsWith(".normalPF");
} else if (n.getNodeType() == Node.ATTRIBUTE_NODE) {
// DanglingLine p, q, p0, q0 attributes in IIDM Network
// TapChanger r, x, g, b, rho, alpha attributes in IIDM Network
// Line b1, b2, g1, g2 attributes in IIDM Network
// Shunt bPerSection attribute in IIDM Network
String name = n.getLocalName();
return name.equals("p") || name.equals("q") || name.equals("p0") || name.equals("q0")
|| name.equals("r") || name.equals("x") || name.equals("g") || name.equals("b") || name.equals("rho") || name.equals("alpha")
|| name.equals("b1") || name.equals("b2") || name.equals("g1") || name.equals("g2")
|| name.equals("bPerSection") || name.equals("activePowerSetpoint") || name.equals("maxP")
|| isAttrValueOfNumericProperty((Attr) n);
}
return false;
}
private static boolean isAttrValueOfNumericProperty(Attr attr) {
// Check if we are inside a property element and if the name of property is voltage or angle
if (attr.getLocalName().equals("value")) {
Node p = attr.getOwnerElement();
if (p.getNodeType() == Node.ELEMENT_NODE && p.getLocalName().equals("property")) {
Node npname = p.getAttributes().getNamedItem("name");
if (npname != null) {
String pname = npname.getTextContent();
return pname.equals("v") || pname.equals("angle");
}
}
}
return false;
}
private static double toleranceForNumericContent(Node n) {
if (n.getLocalName().endsWith(".p") || n.getLocalName().endsWith(".q")) {
return 1e-5;
} else if (n.getLocalName().equals("p") || n.getLocalName().equals("q")) {
return 1e-1;
} else if (n.getLocalName().equals("p0") || n.getLocalName().equals("q0")) {
return 1e-5;
} else if (n.getLocalName().equals("r") || n.getLocalName().equals("x")
|| n.getLocalName().equals("b") || n.getLocalName().equals("g")
|| n.getLocalName().equals("b1") || n.getLocalName().equals("b2")
|| n.getLocalName().equals("g1") || n.getLocalName().equals("g2")
|| n.getLocalName().equals("alpha") || n.getLocalName().equals("rho")
|| n.getLocalName().equals("bPerSection") || n.getLocalName().equals("activePowerSetpoint")
|| n.getLocalName().equals("maxP")) {
return 1e-5;
}
return 1e-10;
}
private static DiffBuilder diff(InputStream expected, InputStream actual, DifferenceEvaluator user) {
Source control = Input.fromStream(expected).build();
Source test = Input.fromStream(actual).build();
return DiffBuilder.compare(control).withTest(test)
.ignoreWhitespace()
.ignoreComments()
.withDifferenceEvaluator(
DifferenceEvaluators.chain(DifferenceEvaluators.Default, ExportXmlCompare::numericDifferenceEvaluator, user))
.withComparisonListeners(ExportXmlCompare::debugComparison);
}
static void debugComparison(Comparison comparison, ComparisonResult comparisonResult) {
if (comparisonResult.equals(ComparisonResult.DIFFERENT)) {
LOG.error("comparison {}", comparison.getType());
LOG.error(" control {}", comparison.getControlDetails().getXPath());
debugNode(comparison.getControlDetails().getTarget());
LOG.error(" test {}", comparison.getTestDetails().getXPath());
debugNode(comparison.getTestDetails().getTarget());
LOG.error(" result {}", comparisonResult);
}
}
private static void debugNode(Node n) {
if (n != null) {
debugAttributes(n, " ");
if (n.getNodeType() == Node.TEXT_NODE) {
LOG.error(" {}", n.getTextContent());
}
int maxNodes = 5;
for (int k = 0; k < maxNodes && k < n.getChildNodes().getLength(); k++) {
Node n1 = n.getChildNodes().item(k);
if (n1.getLocalName() != null) {
LOG.error(" {} {}", n1.getLocalName(), n1.getTextContent());
} else {
LOG.error(" {}", n1.getTextContent());
}
debugAttributes(n1, " ");
}
if (n.getChildNodes().getLength() > maxNodes) {
LOG.error(" ...");
}
}
}
private static void debugAttributes(Node n, String indent) {
debugAttribute(n, RDF_NAMESPACE, "resource", indent);
debugAttribute(n, RDF_NAMESPACE, "about", indent);
}
private static void debugAttribute(Node n, String namespace, String localName, String indent) {
if (n.getAttributes() != null) {
Node a = n.getAttributes().getNamedItemNS(namespace, localName);
if (a != null) {
LOG.error("{}{} = {}", indent, localName, a.getTextContent());
}
}
}
private static DiffBuilder withSelectedSshNodes(DiffBuilder diffBuilder) {
return diffBuilder.withNodeFilter(n -> n.getNodeType() == Node.TEXT_NODE || isConsideredSshNode(n));
}
private static boolean isConsideredSshNode(Node n) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
String name = n.getLocalName();
// Optional attributes with default values
if (name.equals("EquivalentInjection.regulationStatus") && n.getTextContent().equals("false")) {
return false;
} else if (name.equals("EquivalentInjection.regulationTarget") && Double.parseDouble(n.getTextContent()) == 0) {
return false;
}
return name.equals("RDF")
|| name.startsWith("EnergyConsumer") || name.startsWith("ConformLoad") || name.startsWith("NonConformLoad")
|| name.startsWith("Terminal")
|| name.startsWith("EquivalentInjection")
|| name.contains("TapChanger")
|| name.contains("ShuntCompensator")
|| name.startsWith("SynchronousMachine")
|| name.startsWith("RotatingMachine")
|| name.startsWith("RegulatingCondEq")
|| name.startsWith("RegulatingControl")
|| name.startsWith("StaticVarCompensator")
// Previous condition includes this one
// but we state explicitly that we consider tap changer control objects
|| name.startsWith("TapChangerControl")
// TODO include control areas
|| name.contains("GeneratingUnit")
|| isConsideredModelElementName(name);
}
return false;
}
private static boolean isConsideredModelElementName(String name) {
return name.equals("FullModel")
|| name.startsWith("Model.")
&& !(name.equals("Model.created")
|| name.equals("Model.Supersedes")
|| name.equals("Model.createdBy"));
}
private static DiffBuilder ignoringNonPersistentSshIds(DiffBuilder diffBuilder) {
return diffBuilder.withAttributeFilter(attr -> {
String elementName = attr.getOwnerElement().getLocalName();
boolean ignored = false;
if (elementName != null) {
if (elementName.equals("FullModel")) {
ignored = attr.getLocalName().equals("about");
}
}
return !ignored;
});
}
private static DiffBuilder selectingEquivalentSshObjects(DiffBuilder diffBuilder) {
QName about = new QName(RDF_NAMESPACE, "about");
ElementSelector elementSelector = ElementSelectors.conditionalBuilder()
// If element has rdf:about attribute and is not FullModel
.when(e -> !e.getAttributeNS(RDF_NAMESPACE, "about").isEmpty() && !e.getLocalName().equals("FullModel"))
// Then select the same elements based on content of rdf:about attribute
.thenUse(elementSelectorByAttributes(about))
.elseUse(ElementSelectors.byName)
.build();
return diffBuilder.withNodeMatcher(new DefaultNodeMatcher(elementSelector));
}
private static boolean bothNullOrEqual(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
private static boolean mapsEqualForKeys(Map<QName, String> control, Map<QName, String> test, Iterable<QName> keys) {
Iterator<QName> i = keys.iterator();
QName q;
do {
if (!i.hasNext()) {
return true;
}
q = i.next();
} while (bothNullOrEqual(control.get(q), test.get(q)));
return false;
}
private static ElementSelector elementSelectorByAttributes(QName... attribs) {
if (attribs == null) {
throw new IllegalArgumentException("attributes must not be null");
} else {
final Collection<QName> qs = Arrays.asList(attribs);
if (Linqy.any(qs, new IsNullPredicate())) {
throw new IllegalArgumentException("attributes must not contain null values");
} else {
return (controlElement, testElement) -> mapsEqualForKeys(Nodes.getAttributes(controlElement), Nodes.getAttributes(testElement), qs);
}
}
}
private static Diff compare(DiffBuilder diffBuilder) {
Diff diff = diffBuilder.build();
boolean hasDiff = diff.hasDifferences();
if (hasDiff && LOG.isErrorEnabled()) {
for (Difference d : diff.getDifferences()) {
if (d.getResult() == ComparisonResult.DIFFERENT) {
LOG.error("XML difference {}", d.getComparison());
}
}
}
return diff;
}
private static final Logger LOG = LoggerFactory.getLogger(ExportXmlCompare.class);
}