diff --git a/pom.xml b/pom.xml index 2a8b2fb..8b08366 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,11 @@ hamcrest test + + org.assertj + assertj-core + test + com.powsybl powsybl-iidm-test diff --git a/src/main/java/org/gridsuite/filter/AbstractFilter.java b/src/main/java/org/gridsuite/filter/AbstractFilter.java index 41942dd..b8457de 100644 --- a/src/main/java/org/gridsuite/filter/AbstractFilter.java +++ b/src/main/java/org/gridsuite/filter/AbstractFilter.java @@ -8,7 +8,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.expertfilter.ExpertFilter; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -43,7 +45,7 @@ public abstract class AbstractFilter implements IFilterAttributes { private UUID id; - private Date modificationDate; + private Date modificationDate; // TODO use Instant like in servers (client not on same timezone than server) private EquipmentType equipmentType; diff --git a/src/main/java/org/gridsuite/filter/IFilterAttributes.java b/src/main/java/org/gridsuite/filter/IFilterAttributes.java index d4799fb..b4d668e 100644 --- a/src/main/java/org/gridsuite/filter/IFilterAttributes.java +++ b/src/main/java/org/gridsuite/filter/IFilterAttributes.java @@ -10,13 +10,16 @@ import org.gridsuite.filter.utils.EquipmentType; import org.gridsuite.filter.utils.FilterType; +import java.util.Date; +import java.util.UUID; + /** * @author Jacques Borsenberger */ public interface IFilterAttributes { - java.util.UUID getId(); + UUID getId(); - java.util.Date getModificationDate(); + Date getModificationDate(); // TODO use Instant like in servers (client not on same timezone than server) FilterType getType(); diff --git a/src/main/java/org/gridsuite/filter/expertfilter/ExpertFilter.java b/src/main/java/org/gridsuite/filter/expertfilter/ExpertFilter.java index 2a9b8f3..3bf4217 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/ExpertFilter.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/ExpertFilter.java @@ -8,10 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.iidm.network.TopologyKind; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.AbstractFilter; import org.gridsuite.filter.expertfilter.expertrule.AbstractExpertRule; @@ -24,11 +21,11 @@ /** * @author Antoine Bouhours */ -@Getter -@Setter @SuperBuilder @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public class ExpertFilter extends AbstractFilter { private AbstractExpertRule rules; diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/AbstractExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/AbstractExpertRule.java index 72ccd07..ab046e0 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/AbstractExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/AbstractExpertRule.java @@ -12,9 +12,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.powsybl.iidm.network.Identifiable; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -46,8 +45,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @NoArgsConstructor @AllArgsConstructor -@Getter -@Setter +@Data @SuperBuilder public abstract class AbstractExpertRule { diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/BooleanExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/BooleanExpertRule.java index 2687960..16a1f83 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/BooleanExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/BooleanExpertRule.java @@ -9,10 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -30,8 +27,9 @@ */ @AllArgsConstructor @NoArgsConstructor -@Getter -@Setter +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class BooleanExpertRule extends AbstractExpertRule { private Boolean value; diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/CombinatorExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/CombinatorExpertRule.java index c869828..2c39f70 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/CombinatorExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/CombinatorExpertRule.java @@ -9,7 +9,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -23,6 +26,9 @@ * @author Antoine Bouhours */ @NoArgsConstructor +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class CombinatorExpertRule extends AbstractExpertRule { @Override diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/EnumExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/EnumExpertRule.java index 332425e..7f0ac99 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/EnumExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/EnumExpertRule.java @@ -9,7 +9,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -24,6 +27,9 @@ * @author Antoine Bouhours */ @NoArgsConstructor +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class EnumExpertRule extends StringExpertRule { @Override diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/FilterUuidExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/FilterUuidExpertRule.java index d551edd..4632ffe 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/FilterUuidExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/FilterUuidExpertRule.java @@ -9,12 +9,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; +import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; import org.gridsuite.filter.utils.expertfilter.DataType; import org.gridsuite.filter.utils.expertfilter.ExpertFilterUtils; -import org.gridsuite.filter.FilterLoader; import java.util.Map; import java.util.UUID; @@ -25,6 +28,9 @@ * @author Franck Lecuyer */ @NoArgsConstructor +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class FilterUuidExpertRule extends StringExpertRule { @Override diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/NumberExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/NumberExpertRule.java index 72b8760..0a22199 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/NumberExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/NumberExpertRule.java @@ -10,10 +10,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -31,8 +28,9 @@ */ @AllArgsConstructor @NoArgsConstructor -@Getter -@Setter +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class NumberExpertRule extends AbstractExpertRule { private Double value; diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/PropertiesExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/PropertiesExpertRule.java index 7c052e4..f729f02 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/PropertiesExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/PropertiesExpertRule.java @@ -9,10 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; @@ -30,8 +27,9 @@ */ @AllArgsConstructor @NoArgsConstructor -@Getter -@Setter +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class PropertiesExpertRule extends AbstractExpertRule { private String propertyName; @@ -64,12 +62,4 @@ public DataType getDataType() { public String getStringValue() { return this.getPropertyName(); } - - public String getPropertyName() { - return this.propertyName; - } - - public List getPropertyValues() { - return this.propertyValues; - } } diff --git a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/StringExpertRule.java b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/StringExpertRule.java index 82feea3..adf3303 100644 --- a/src/main/java/org/gridsuite/filter/expertfilter/expertrule/StringExpertRule.java +++ b/src/main/java/org/gridsuite/filter/expertfilter/expertrule/StringExpertRule.java @@ -10,10 +10,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.Identifiable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.apache.commons.lang3.StringUtils; import org.gridsuite.filter.FilterLoader; @@ -34,8 +31,9 @@ */ @AllArgsConstructor @NoArgsConstructor -@Getter -@Setter +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @SuperBuilder public class StringExpertRule extends AbstractExpertRule { private String value; diff --git a/src/main/java/org/gridsuite/filter/globalfilter/AbstractGlobalFilterService.java b/src/main/java/org/gridsuite/filter/globalfilter/AbstractGlobalFilterService.java new file mode 100644 index 0000000..6eeb68a --- /dev/null +++ b/src/main/java/org/gridsuite/filter/globalfilter/AbstractGlobalFilterService.java @@ -0,0 +1,46 @@ +package org.gridsuite.filter.globalfilter; + +import com.powsybl.iidm.network.Network; +import lombok.NonNull; +import org.apache.commons.lang3.ObjectUtils; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.FilterLoader; +import org.gridsuite.filter.utils.EquipmentType; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +public abstract class AbstractGlobalFilterService implements FilterLoader { + protected abstract Network getNetwork(@NonNull UUID networkUuid, @NonNull String variantId); + + /** @see #getFilteredIds(UUID, String, GlobalFilter, List) */ + protected List getFilteredIds(@NonNull final UUID networkUuid, @NonNull final String variantId, + @NonNull final GlobalFilter globalFilter, @NonNull final List equipmentTypes) { + return getFilteredIds(networkUuid, variantId, globalFilter, equipmentTypes, null); + } + + /** + * Get filtered equipment IDs. + * @param networkUuid the network to load + * @param variantId the network variant to work on + * @param globalFilter the filter(s) to apply + * @param equipmentTypes the {@link EquipmentType equipment types} to filter + * @param genericFilters additional generic filters to apply + * @return the {@link List list} of {@link UUID IDs} of filtered {@link EquipmentType equipments}. + */ + protected List getFilteredIds(@NonNull final UUID networkUuid, @NonNull final String variantId, + @NonNull final GlobalFilter globalFilter, @NonNull final List equipmentTypes, + @Nullable final List genericFilters) { + final Network network = getNetwork(networkUuid, variantId); + return GlobalFilterUtils.applyGlobalFilterOnNetwork(network, globalFilter, ObjectUtils.getIfNull(genericFilters, List::of), equipmentTypes, this) + // Filter equipments by type + .values() + .stream() + .filter(Objects::nonNull) + // Combine all results into one list + .flatMap(List::stream) + .toList(); + } +} diff --git a/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilter.java b/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilter.java new file mode 100644 index 0000000..981ce96 --- /dev/null +++ b/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilter.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024, 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 org.gridsuite.filter.globalfilter; + +import com.powsybl.iidm.network.Country; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; +import lombok.experimental.SuperBuilder; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * @author maissa Souissi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@FieldNameConstants +// TODO convert to record when loadflow-server and computation lib stop to extends it +public class GlobalFilter { + private List nominalV; + private List countryCode; + private List genericFilter; + private Map> substationProperty; + + /** + * @return {@code true} if all filter parameters are empty, else {@code false}. + */ + public boolean isEmpty() { + return CollectionUtils.isEmpty(this.nominalV) + && CollectionUtils.isEmpty(this.countryCode) + && CollectionUtils.isEmpty(this.genericFilter) + && MapUtils.isEmpty(this.substationProperty) + && this.substationProperty.values().stream().allMatch(CollectionUtils::isEmpty); + } +} diff --git a/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilterUtils.java b/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilterUtils.java new file mode 100644 index 0000000..7fba83f --- /dev/null +++ b/src/main/java/org/gridsuite/filter/globalfilter/GlobalFilterUtils.java @@ -0,0 +1,214 @@ +package org.gridsuite.filter.globalfilter; + +import com.powsybl.iidm.network.Country; +import com.powsybl.iidm.network.Identifiable; +import com.powsybl.iidm.network.Network; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.FilterLoader; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.*; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.FiltersUtils; +import org.gridsuite.filter.utils.TimeUtils; +import org.gridsuite.filter.utils.UuidUtils; +import org.gridsuite.filter.utils.expertfilter.CombinatorType; +import org.gridsuite.filter.utils.expertfilter.ExpertFilterUtils; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Predicate; + +public final class GlobalFilterUtils { + private GlobalFilterUtils() { + throw new IllegalCallerException("Utility class should not be instantiated"); + } + + @Nonnull + public static List getNominalVoltageFieldType(@Nonnull final EquipmentType equipmentType) { + return switch (equipmentType) { + case LINE, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.NOMINAL_VOLTAGE_1, FieldType.NOMINAL_VOLTAGE_2); + case VOLTAGE_LEVEL -> List.of(FieldType.NOMINAL_VOLTAGE); + default -> List.of(); + }; + } + + /** + * Builds nominal voltage rules combining all relevant field types + * @see GlobalFilter#getNominalV() + */ + @Nonnull + public static Optional buildNominalVoltageRules( + @Nonnull final List nominalVoltages, @Nonnull final EquipmentType equipmentType) { + final List fields = getNominalVoltageFieldType(equipmentType); + return ExpertFilterUtils.buildOrCombination(nominalVoltages.stream() + .filter(Predicate.not(String::isBlank)) + .map(Double::valueOf) + .mapMulti((value, accumulator) -> { + for (final FieldType field : fields) { + accumulator.accept(NumberExpertRule.builder() + .value(value) + .field(field) + .operator(OperatorType.EQUALS) + .build()); + } + }).toList()); + } + + @Nonnull + public static List getCountryCodeFieldType(@Nonnull final EquipmentType equipmentType) { + return switch (equipmentType) { + case VOLTAGE_LEVEL, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.COUNTRY); + case LINE -> List.of(FieldType.COUNTRY_1, FieldType.COUNTRY_2); + default -> List.of(); + }; + } + + /** + * Builds country code rules combining all relevant field types + */ + @Nonnull + public static Optional buildCountryCodeRules( + @Nonnull final List countryCodes, @Nonnull final EquipmentType equipmentType) { + final List fields = getCountryCodeFieldType(equipmentType); + return ExpertFilterUtils.buildOrCombination(countryCodes.stream() + .map(Country::name) + .mapMulti((countryCode, accumulator) -> { + for (final FieldType field : fields) { + accumulator.accept(EnumExpertRule.builder() + .value(countryCode) + .field(field) + .operator(OperatorType.EQUALS) + .build()); + } + }) + .toList()); + } + + @Nonnull + public static List getSubstationPropertiesFieldTypes(@Nullable final EquipmentType equipmentType) { + return equipmentType == EquipmentType.LINE + ? List.of(FieldType.SUBSTATION_PROPERTIES_1, FieldType.SUBSTATION_PROPERTIES_2) + : List.of(FieldType.SUBSTATION_PROPERTIES); + } + + /** + * Builds substation property rules combining all relevant field types + */ + @Nonnull + public static Optional buildSubstationPropertyRules( + @Nonnull final Map> properties, @Nonnull final EquipmentType equipmentType) { + final List fields = getSubstationPropertiesFieldTypes(equipmentType); + return ExpertFilterUtils.buildOrCombination(properties.entrySet() + .stream() + .mapMulti((entry, accumulator) -> { + for (final FieldType field : fields) { + accumulator.accept(PropertiesExpertRule.builder() + .combinator(CombinatorType.OR) + .operator(OperatorType.IN) + .field(field) + .propertyName(entry.getKey()) + .propertyValues(entry.getValue()) + .build()); + } + }) + .toList()); + } + + /** + * Builds expert filter from a {@link GlobalFilter global filter} for an {@link EquipmentType equipment type}. + */ + @Nullable + public static ExpertFilter buildExpertFilter(@Nonnull final GlobalFilter globalFilter, @Nonnull final EquipmentType equipmentType) { + final List andRules = new ArrayList<>(); + + // Nominal voltage rules + if (globalFilter.getNominalV() != null) { + buildNominalVoltageRules(globalFilter.getNominalV(), equipmentType).ifPresent(andRules::add); + } + + // Country code rules + if (globalFilter.getCountryCode() != null) { + buildCountryCodeRules(globalFilter.getCountryCode(), equipmentType).ifPresent(andRules::add); + } + + // Substation property rules + if (globalFilter.getSubstationProperty() != null) { + buildSubstationPropertyRules(globalFilter.getSubstationProperty(), equipmentType).ifPresent(andRules::add); + } + + return andRules.isEmpty() ? null : new ExpertFilter(UuidUtils.generateUUID(), TimeUtils.nowAsDate(), equipmentType, + CombinatorExpertRule.builder().combinator(CombinatorType.AND).rules(andRules).build()); + } + + @Nonnull + private static List filterNetwork(@Nonnull final AbstractFilter filter, @Nonnull final Network network, @Nonnull final FilterLoader filterLoader) { + return FiltersUtils.getIdentifiables(filter, network, filterLoader) + .stream() + .map(Identifiable::getId) + .toList(); + } + + /** + * Extracts {@link Identifiable#getId() equipment ID}s from a generic filter based on {@link EquipmentType equipment type}. + */ + @Nonnull + public static List applyFilterOnNetwork(@Nonnull final AbstractFilter filter, @Nonnull final EquipmentType targetEquipmentType, + @Nonnull final Network network, @Nonnull final FilterLoader filterLoader) { + if (filter.getEquipmentType() == targetEquipmentType) { + return filterNetwork(filter, network, filterLoader); + } else if (filter.getEquipmentType() == EquipmentType.VOLTAGE_LEVEL) { + return filterNetwork(ExpertFilterUtils.buildExpertFilterWithVoltageLevelIdsCriteria(filter.getId(), targetEquipmentType), network, filterLoader); + } + return List.of(); + } + + /** + * Extracts filtered {@link Identifiable#getId() equipment ID}s by applying {@link ExpertFilter expert} + * and {@link AbstractFilter generic filter}s. + */ + @Nonnull + public static List applyGlobalFilterOnNetwork(@Nonnull final Network network, + @Nonnull final GlobalFilter globalFilter, @Nonnull final List genericFilters, + @Nonnull final EquipmentType equipmentType, @Nonnull final FilterLoader filterLoader) { + List> allFilterResults = new ArrayList<>(1 + genericFilters.size()); + + // Extract IDs from expert filter + final ExpertFilter expertFilter = buildExpertFilter(globalFilter, equipmentType); + if (expertFilter != null) { + allFilterResults.add(filterNetwork(expertFilter, network, filterLoader)); + } + + // Extract IDs from generic filters + for (final AbstractFilter filter : genericFilters) { + final List filterResult = applyFilterOnNetwork(filter, equipmentType, network, filterLoader); + if (!filterResult.isEmpty()) { + allFilterResults.add(filterResult); + } + } + + // Combine results with appropriate logic + // Expert filters use OR between them, generic filters use AND + return FiltersUtils.combineFilterResults(allFilterResults, !genericFilters.isEmpty()); + } + + /** + * Filters equipments by {@link EquipmentType type} + * @return map of {@link Identifiable#getId() equipment ID}s grouped by {@link EquipmentType equipment type} + */ + @Nonnull + public static Map> applyGlobalFilterOnNetwork(@Nonnull final Network network, + @Nonnull final GlobalFilter globalFilter, @Nonnull final List genericFilters, + @Nonnull final List equipmentTypes, @Nonnull final FilterLoader filterLoader) { + Map> result = new EnumMap<>(EquipmentType.class); + for (final EquipmentType equipmentType : equipmentTypes) { + final List filteredIds = applyGlobalFilterOnNetwork(network, globalFilter, genericFilters, equipmentType, filterLoader); + if (!filteredIds.isEmpty()) { + result.put(equipmentType, filteredIds); + } + } + return result; + } +} diff --git a/src/main/java/org/gridsuite/filter/identifierlistfilter/IdentifierListFilter.java b/src/main/java/org/gridsuite/filter/identifierlistfilter/IdentifierListFilter.java index 1f32490..eb49f4e 100644 --- a/src/main/java/org/gridsuite/filter/identifierlistfilter/IdentifierListFilter.java +++ b/src/main/java/org/gridsuite/filter/identifierlistfilter/IdentifierListFilter.java @@ -9,10 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.powsybl.iidm.network.IdentifiableType; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.experimental.SuperBuilder; import org.gridsuite.filter.AbstractFilter; import org.gridsuite.filter.utils.EquipmentType; @@ -20,16 +17,15 @@ import java.util.*; - /** * @author Seddik Yengui */ -@NoArgsConstructor -@Getter -@Setter @SuperBuilder +@NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public class IdentifierListFilter extends AbstractFilter { private List filterEquipmentsAttributes; diff --git a/src/main/java/org/gridsuite/filter/utils/FiltersUtils.java b/src/main/java/org/gridsuite/filter/utils/FiltersUtils.java index 77255fe..191705e 100644 --- a/src/main/java/org/gridsuite/filter/utils/FiltersUtils.java +++ b/src/main/java/org/gridsuite/filter/utils/FiltersUtils.java @@ -14,6 +14,8 @@ import org.gridsuite.filter.identifierlistfilter.IdentifierListFilter; import org.gridsuite.filter.identifierlistfilter.IdentifierListFilterEquipmentAttributes; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; import java.util.function.Predicate; import java.util.stream.Stream; @@ -237,4 +239,30 @@ public static List> getIdentifiables(AbstractFilter filter, Netw case SUBSTATION -> getSubstationList(network, filter, filterLoader); }; } + + /** + * Combines multiple filter results using AND or OR logic. + */ + @Nonnull + public static List combineFilterResults(@Nullable final List> filterResults, final boolean useAndLogic) { + if (filterResults == null || filterResults.isEmpty()) { + return List.of(); + } + if (filterResults.size() == 1) { + return filterResults.getFirst(); + } + if (useAndLogic) { + // Intersection of all results + Set result = new HashSet<>(filterResults.getFirst()); + for (int i = 1; i < filterResults.size(); i++) { + result.retainAll(filterResults.get(i)); + } + return new ArrayList<>(result); + } else { + // Union of all results + Set result = new HashSet<>(); + filterResults.forEach(result::addAll); + return new ArrayList<>(result); + } + } } diff --git a/src/main/java/org/gridsuite/filter/utils/TimeUtils.java b/src/main/java/org/gridsuite/filter/utils/TimeUtils.java new file mode 100644 index 0000000..e1662ca --- /dev/null +++ b/src/main/java/org/gridsuite/filter/utils/TimeUtils.java @@ -0,0 +1,32 @@ +package org.gridsuite.filter.utils; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.Setter; + +import java.time.Clock; +import java.time.Instant; +import java.util.Date; + +/** + * Utility class for datetime to permit during tests to mock time related operations. + * @apiNote This class is to permit tests to intercept date/time + */ + +public final class TimeUtils { + private TimeUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + @Setter(onMethod_ = {@VisibleForTesting}) + @Getter + private static Clock clock = Clock.systemUTC(); + + public static Instant now() { + return Instant.now(clock); + } + + public static Date nowAsDate() { + return Date.from(now()); + } +} diff --git a/src/main/java/org/gridsuite/filter/utils/UuidUtils.java b/src/main/java/org/gridsuite/filter/utils/UuidUtils.java new file mode 100644 index 0000000..4419e4c --- /dev/null +++ b/src/main/java/org/gridsuite/filter/utils/UuidUtils.java @@ -0,0 +1,88 @@ +package org.gridsuite.filter.utils; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Setter; + +import java.util.UUID; +import java.util.function.Supplier; + +/** + * Utility class for UUID to permit during tests to mock {@link UUID} generation. + * @apiNote This class is to permit tests to intercept {@link UUID} generation + */ +public final class UuidUtils { + private UuidUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + @Setter(onMethod_ = {@VisibleForTesting}) + private static Supplier uuidSupplier = UUID::randomUUID; + + public static UUID generateUUID() { + return uuidSupplier.get(); + } + + /** + * Like {@link UUID#randomUUID()} generate UUID v4 variant 2, but not random.
+ * This utility function is intended to be used in tests to have predictable UUID. + *

Some information on UUID: + *

+     * textual format: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
+     *          bytes:  0 1 2 3  4 5  6 7  8 9 10 ↓12 ↓14 ↓
+     *                                           11  13  15
+     * size: 4b-2b-2b-2b-6b
+     * 
+ * Where
    + *
  • M = version field (4 bits).
  • + *
  • N = variant field (3 bits).
  • + *
  • All other bits = random (for v4).

+ * + *

UUID bytes structure: + *

+     *  /-------mostSigBits-------\   /--------leastSigBits---------\
+     *  b0 b1 b2 b3   b4 b5   b6 b7   b8 b9   b10 b11 b12 b13 b14 b15
+     * |-----------| |-----| |-----| |-----| |-----------------------|
+     *    random    random v4+random variant+random    random
+     * b6 = [0100 LLLL] ⇒ version 4
+     * b8 = [10xL LLLL] → [1000 LLLL] ⇒ RFC 4122 / IETF / Leach–Salz variant
+     * 

+ * + * @param b0to3 the bits from b0 to b3 + * @param b4to5 the bits from b4 and b5 + * @param b7 the bits of b7 + * @param nibblesB6lowB8low the upper nibble is for the lower half of b6 + * and the lower nibble is for the lower half of b8 + * @param b9 the bits of b9 + * @param b10to11 the bits for b10 and b11 + * @param b12to15 the bits for b12 to b15 + * @return a UUID v4 in variant 2 with the bits passed + * @implNote Bits 5–4 of b8 are left to {@code 0} for simplicity, + * so the UUID is of the form {@code xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx}. + */ + // regexp: ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-8[0-9a-f]{3}-[0-9a-f]{12}$ + @VisibleForTesting + public static UUID createUUID(final int b0to3, final short b4to5, final byte b7, + final byte nibblesB6lowB8low, + final byte b9, final short b10to11, final int b12to15) { + return new UUID( + // Construct mostSigBits directly + (b0to3 & 0xFFFFFFFFL) << 32 + | (b4to5 & 0xFFFFL) << 16 + | (0x40L | ((nibblesB6lowB8low >>> 4) & 0x0FL)) << 8 // version 4 + | (b7 & 0xFFL), + // Construct leastSigBits directly + (0x80L | (nibblesB6lowB8low & 0xFL)) << 56 // variant 2 + | (b9 & 0xFFL) << 48 + | (b10to11 & 0xFFFFL) << 32 + | (b12to15 & 0xFFFFFFFFL) + ); + } + + /** + * @see #createUUID(int, short, byte, byte, byte, short, int) + */ + @VisibleForTesting + public static UUID createUUID(final int b12to15) { + return createUUID(0, (short) 0, (byte) 0, (byte) 0, (byte) 0, (short) 0, b12to15); + } +} diff --git a/src/main/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtils.java b/src/main/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtils.java index c9a22ec..db70edd 100644 --- a/src/main/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtils.java +++ b/src/main/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtils.java @@ -13,10 +13,13 @@ import com.powsybl.iidm.network.extensions.StandbyAutomaton; import org.apache.commons.collections4.CollectionUtils; import org.gridsuite.filter.FilterLoader; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.AbstractExpertRule; +import org.gridsuite.filter.expertfilter.expertrule.CombinatorExpertRule; +import org.gridsuite.filter.expertfilter.expertrule.FilterUuidExpertRule; import org.gridsuite.filter.identifierlistfilter.FilterEquipments; import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; -import org.gridsuite.filter.utils.FilterServiceUtils; -import org.gridsuite.filter.utils.RegulationType; +import org.gridsuite.filter.utils.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -614,7 +617,7 @@ public static List getFilterEquipments(Network network, Set FilterCycleDetector.checkNoCycle(filter, filterLoader)); List filterEquipments = FilterServiceUtils.getFilterEquipmentsFromUuid(network, uuid, filterLoader); - cachedUuidFilters.put(uuid, !CollectionUtils.isEmpty(filterEquipments) ? filterEquipments.getFirst() : null); + cachedUuidFilters.put(uuid, CollectionUtils.isNotEmpty(filterEquipments) ? filterEquipments.getFirst() : null); res.addAll(filterEquipments); } }); @@ -626,4 +629,31 @@ public static boolean isPartOf(Network network, String value, Set uuids, return equipments.stream().flatMap(e -> e.getIdentifiableAttributes().stream() .map(IdentifiableAttributes::getId)).collect(Collectors.toSet()).contains(value); } + + /** + * Build an "OR" rule from the rules passed. + * @param rules the rule(s) to be applied + * @return {@link Optional#empty() Empty} if no rule is passed, + * the {@link AbstractExpertRule rule} if the list has only 1 rule inside, + * otherwise an {@link CombinatorExpertRule OR combinator} with the rules. + */ + @Nonnull + public static Optional buildOrCombination(@Nullable final List rules) { + if (rules == null || rules.isEmpty()) { + return Optional.empty(); + } + return Optional.of(rules.size() > 1 ? CombinatorExpertRule.builder().combinator(CombinatorType.OR).rules(rules).build() : rules.getFirst()); + } + + /** + * Builds expert filter with {@link VoltageLevel voltage level} IDs criteria. + */ + @Nonnull + public static ExpertFilter buildExpertFilterWithVoltageLevelIdsCriteria(@Nonnull final UUID filterUuid, @Nonnull final EquipmentType equipmentType) { + return new ExpertFilter(UuidUtils.generateUUID(), TimeUtils.nowAsDate(), equipmentType, + CombinatorExpertRule.builder().combinator(CombinatorType.OR).rules(List.of( + FilterUuidExpertRule.builder().operator(OperatorType.IS_PART_OF).field(FieldType.VOLTAGE_LEVEL_ID_1).values(Set.of(filterUuid.toString())).build(), + FilterUuidExpertRule.builder().operator(OperatorType.IS_PART_OF).field(FieldType.VOLTAGE_LEVEL_ID_2).values(Set.of(filterUuid.toString())).build() + )).build()); + } } diff --git a/src/test/java/org/gridsuite/filter/globalfilter/GlobalFilterUtilsTest.java b/src/test/java/org/gridsuite/filter/globalfilter/GlobalFilterUtilsTest.java new file mode 100644 index 0000000..f6fcded --- /dev/null +++ b/src/test/java/org/gridsuite/filter/globalfilter/GlobalFilterUtilsTest.java @@ -0,0 +1,355 @@ +package org.gridsuite.filter.globalfilter; + +import com.powsybl.iidm.network.Country; +import com.powsybl.iidm.network.Identifiable; +import com.powsybl.iidm.network.Network; +import org.assertj.core.api.*; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.FilterLoader; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.*; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.FiltersUtils; +import org.gridsuite.filter.utils.UuidUtils; +import org.gridsuite.filter.utils.expertfilter.CombinatorType; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/* Methods dependencies: + * applyGlobalFilterOnNetwork + * ↳ applyGlobalFilterOnNetwork + * ↳ applyFilterOnNetwork + * ⇃ ↳ filterNetwork + * ↳ buildExpertFilter + * ↳ buildNominalVoltageRules + * ⇃ ↳ getNominalVoltageFieldType + * ↳ buildCountryCodeRules + * ⇃ ↳ getCountryCodeFieldType + * ↳ buildSubstationPropertyRules + * ↳ getSubstationPropertiesFieldTypes + */ +class GlobalFilterUtilsTest implements WithAssertions { + private static void testVariableOrCombinationRules(final Optional result, final int inputSize, + final Class ruleClass, final ThrowingConsumer[] singleRuleAsserts, + final Iterable multiAssertElements) { + final OptionalAssert assertion = Assertions.assertThat(result).as("result"); + if (inputSize <= 0) { + assertion.isEmpty(); + } else { + assertion.isPresent(); + if (inputSize == 1) { + assertion.get(InstanceOfAssertFactories.type(ruleClass)) + .as("expert rule") + .satisfies(singleRuleAsserts); + } else { + assertion.get(InstanceOfAssertFactories.type(CombinatorExpertRule.class)) + .as("combinator expert rule") + .satisfies(cer -> Assertions.assertThat(cer.getCombinator()).as("combinator").isEqualTo(CombinatorType.OR)) + .extracting(CombinatorExpertRule::getRules, InstanceOfAssertFactories.list(AbstractExpertRule.class)).as("expert rules") + .containsExactlyInAnyOrderElementsOf(multiAssertElements); + } + } + } + + /** Trick Java to create generic array */ + @SafeVarargs + private static ThrowingConsumer[] createAssertArray(final ThrowingConsumer... assertions) { + return assertions; + } + + @Nested + @DisplayName("buildNominalVoltageRules(...)") + class BuildNominalVoltageRules { + @ParameterizedTest + @MethodSource({"expertRulesData"}) + void shouldCreateExpertRules(final List nominalVoltages) { + testVariableOrCombinationRules( + GlobalFilterUtils.buildNominalVoltageRules(nominalVoltages, EquipmentType.VOLTAGE_LEVEL), + nominalVoltages.size(), + NumberExpertRule.class, + createAssertArray( + ner -> assertThat(ner.getValue()).as("value").hasToString(nominalVoltages.getFirst()), + ner -> assertThat(ner.getField()).as("field").isEqualTo(FieldType.NOMINAL_VOLTAGE), + ner -> assertThat(ner.getOperator()).as("operator").isEqualTo(OperatorType.EQUALS) + ), + nominalVoltages.stream().map(nv -> NumberExpertRule.builder().value(Double.valueOf(nv)) + .field(FieldType.NOMINAL_VOLTAGE).operator(OperatorType.EQUALS).build()).collect(Collectors.toUnmodifiableList())); + } + + private static Stream expertRulesData() { + return Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of("300.0")), + Arguments.of(List.of("400.0", "225.0")) + ); + } + } + + @Nested + @DisplayName("buildCountryCodeRules(...)") + class BuildCountryCodeRules { + @ParameterizedTest + @MethodSource({"enumRulesData"}) + void shouldCreateEnumRules(final List countries) { + testVariableOrCombinationRules( + GlobalFilterUtils.buildCountryCodeRules(countries, EquipmentType.VOLTAGE_LEVEL), + countries.size(), + EnumExpertRule.class, + createAssertArray( + ner -> assertThat(ner.getValue()).as("value").hasToString(countries.getFirst().name()), + ner -> assertThat(ner.getField()).as("field").isEqualTo(FieldType.COUNTRY), + ner -> assertThat(ner.getOperator()).as("operator").isEqualTo(OperatorType.EQUALS) + ), + (List) countries.stream().map(c -> EnumExpertRule.builder().value(c.name()) + .field(FieldType.COUNTRY).operator(OperatorType.EQUALS).build()).toList()); + } + + private static Stream enumRulesData() { + return Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of(Country.YT)), + Arguments.of(List.of(Country.FR, Country.DE)) + ); + } + } + + @Nested + @DisplayName("buildSubstationPropertyRules(...)") + class BuildSubstationPropertyRules { + @ParameterizedTest + @MethodSource({"propertiesRulesData"}) + void shouldCreateCorrectPropertiesRules(final Map> properties) { + testVariableOrCombinationRules( + GlobalFilterUtils.buildSubstationPropertyRules(properties, EquipmentType.VOLTAGE_LEVEL), + properties.size(), + PropertiesExpertRule.class, + createAssertArray( + per -> assertThat(per.getCombinator()).as("combinator").isEqualTo(CombinatorType.OR), + per -> assertThat(per.getOperator()).as("operator").isEqualTo(OperatorType.IN), + per -> assertThat(per.getField()).as("field").isEqualTo(FieldType.SUBSTATION_PROPERTIES), + per -> assertThat(per.getPropertyName()).as("property name").isEqualTo("prop1"), + per -> assertThat(per.getPropertyValues()).as("property values").containsExactlyInAnyOrderElementsOf(properties.values().iterator().next()) + ), + (List) properties.entrySet().stream().map(e -> PropertiesExpertRule.builder().combinator(CombinatorType.OR) + .operator(OperatorType.IN).field(FieldType.SUBSTATION_PROPERTIES).propertyName(e.getKey()).propertyValues(e.getValue()).build()).toList()); + } + + private static Stream propertiesRulesData() { + return Stream.of( + Arguments.of(Map.of()), + Arguments.of(Map.of("prop1", List.of("value0"))), + Arguments.of(Map.of("prop1", List.of("value1", "value2"), "prop2", List.of("value3"))) + ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("nominalVoltageFieldTypeData") + void shouldReturnCorrectNominalVoltageFieldTypes(final EquipmentType equipmentType, final List expectedFields) { + assertThat(GlobalFilterUtils.getNominalVoltageFieldType(equipmentType)) + .as("result").containsExactlyInAnyOrderElementsOf(expectedFields); + } + + private static Stream nominalVoltageFieldTypeData() { + return Stream.of( + // Nominal voltage + Arguments.of(EquipmentType.LINE, List.of(FieldType.NOMINAL_VOLTAGE_1, FieldType.NOMINAL_VOLTAGE_2)), + Arguments.of(EquipmentType.TWO_WINDINGS_TRANSFORMER, List.of(FieldType.NOMINAL_VOLTAGE_1, FieldType.NOMINAL_VOLTAGE_2)), + Arguments.of(EquipmentType.VOLTAGE_LEVEL, List.of(FieldType.NOMINAL_VOLTAGE)), + Arguments.of(EquipmentType.GENERATOR, Collections.emptyList()) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("countryCodeFieldTypeData") + void shouldReturnCorrectCountryCodeFieldTypes(final EquipmentType equipmentType, final List expectedFields) { + assertThat(GlobalFilterUtils.getCountryCodeFieldType(equipmentType)) + .as("result").containsExactlyInAnyOrderElementsOf(expectedFields); + } + + private static Stream countryCodeFieldTypeData() { + return Stream.of( + // Country code + Arguments.of(EquipmentType.VOLTAGE_LEVEL, List.of(FieldType.COUNTRY)), + Arguments.of(EquipmentType.TWO_WINDINGS_TRANSFORMER, List.of(FieldType.COUNTRY)), + Arguments.of(EquipmentType.LINE, List.of(FieldType.COUNTRY_1, FieldType.COUNTRY_2)), + Arguments.of(EquipmentType.GENERATOR, Collections.emptyList()) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("substationPropertyFieldTypeData") + void shouldReturnCorrectSubstationPropertyFieldTypes(final EquipmentType equipmentType, final List expectedFields) { + assertThat(GlobalFilterUtils.getSubstationPropertiesFieldTypes(equipmentType)) + .as("result").containsExactlyInAnyOrderElementsOf(expectedFields); + } + + private static Stream substationPropertyFieldTypeData() { + return Stream.of( + // Substation properties + Arguments.of(EquipmentType.LINE, List.of(FieldType.SUBSTATION_PROPERTIES_1, FieldType.SUBSTATION_PROPERTIES_2)), + Arguments.of(EquipmentType.GENERATOR, List.of(FieldType.SUBSTATION_PROPERTIES)) + ); + } + + @Nested + @DisplayName("buildExpertFilter(...)") + class BuildExpertFilter { + @Test + void shouldReturnNullWhenNoExpertFiltersProvided() { + final GlobalFilter globalFilter = new GlobalFilter(null, null, null, null); + assertThat(GlobalFilterUtils.buildExpertFilter(globalFilter, EquipmentType.GENERATOR)) + .as("result").isNull(); + } + + @Test + void shouldReturnNullWhenNoRules() { + final GlobalFilter globalFilter = new GlobalFilter(List.of(), List.of(), List.of(), Map.of()); + assertThat(GlobalFilterUtils.buildExpertFilter(globalFilter, EquipmentType.GENERATOR)) + .as("result").isNull(); + } + + @Test + void shouldCreateFilterWhenRulesExist() { + final List nv = List.of("400.0"); + final GlobalFilter globalFilter = Mockito.spy(new GlobalFilter(nv, null, null, null)); + try (final MockedStatic mockedGFU = Mockito.mockStatic(GlobalFilterUtils.class, Mockito.CALLS_REAL_METHODS)) { + mockedGFU.when(() -> GlobalFilterUtils.buildNominalVoltageRules(nv, EquipmentType.GENERATOR)) + .thenReturn(Optional.of(Mockito.mock(AbstractExpertRule.class))); + mockedGFU.clearInvocations(); //important because stubbing static method counts as call + assertThat(GlobalFilterUtils.buildExpertFilter(globalFilter, EquipmentType.GENERATOR)).as("result") + .isNotNull().satisfies( + ef -> assertThat(ef.getEquipmentType()).as("equipment type").isEqualTo(EquipmentType.GENERATOR), + ef -> assertThat(ef.getRules()).as("rule combinator") + .asInstanceOf(InstanceOfAssertFactories.type(CombinatorExpertRule.class)) + .extracting(CombinatorExpertRule::getRules, InstanceOfAssertFactories.list(AbstractExpertRule.class)) + .as("rules").hasSize(1) + ); + Mockito.verify(globalFilter, Mockito.atLeastOnce()).getNominalV(); + Mockito.verify(globalFilter, Mockito.atLeastOnce()).getCountryCode(); + Mockito.verify(globalFilter, Mockito.atLeastOnce()).getSubstationProperty(); + mockedGFU.verify(() -> GlobalFilterUtils.buildNominalVoltageRules(Mockito.anyList(), any(EquipmentType.class)), Mockito.times(1)); + mockedGFU.verify(() -> GlobalFilterUtils.buildExpertFilter(any(GlobalFilter.class), any(EquipmentType.class)), Mockito.times(1)); + mockedGFU.verifyNoMoreInteractions(); //check if forget to mock a method + Mockito.verifyNoMoreInteractions(globalFilter); + } + } + } + + @Nested + @DisplayName("filterNetwork(...)") + class FilterNetwork { + @Test + void shouldReturnIdsFromFilteredNetwork() { + final Network network = Mockito.mock(Network.class); + final FilterLoader loader = Mockito.mock(FilterLoader.class); + final AbstractFilter filter = Mockito.mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.GENERATOR); + try (final MockedStatic mockedFU = Mockito.mockStatic(FiltersUtils.class, Mockito.CALLS_REAL_METHODS)) { + final Identifiable i1 = Mockito.mock(Identifiable.class); + when(i1.getId()).thenReturn("id1"); + final Identifiable i2 = Mockito.mock(Identifiable.class); + when(i2.getId()).thenReturn("id2"); + final List> attributes = List.of(i1, i2); + mockedFU.when(() -> FiltersUtils.getIdentifiables(filter, network, loader)).thenReturn(attributes); + mockedFU.clearInvocations(); //important because stubbing static method counts as call + assertThat(GlobalFilterUtils.filterNetwork(filter, network, loader)).as("result") + .containsExactlyInAnyOrder("id1", "id2"); + Mockito.verify(i1, Mockito.atLeastOnce()).getId(); + Mockito.verify(i2, Mockito.atLeastOnce()).getId(); + Mockito.verify(filter, Mockito.atLeastOnce()).getEquipmentType(); + Mockito.verifyNoMoreInteractions(filter, network, loader, i1, i2); + mockedFU.verify(() -> FiltersUtils.getIdentifiables(eq(filter), eq(network), eq(loader)), Mockito.times(1)); + mockedFU.verifyNoMoreInteractions(); //check if forget to mock a method + } + } + } + + @Nested + @DisplayName("applyFilterOnNetwork(...)") + class ApplyFilterOnNetwork { + @Test + void shouldReturnFilteredNetworkWhenSameEquipmentType() { + final Network network = Mockito.mock(Network.class); + final FilterLoader loader = Mockito.mock(FilterLoader.class); + final AbstractFilter filter = Mockito.mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.GENERATOR); + try (final MockedStatic mockedFU = Mockito.mockStatic(FiltersUtils.class, Mockito.CALLS_REAL_METHODS)) { + final Identifiable gen1 = Mockito.mock(Identifiable.class); + when(gen1.getId()).thenReturn("gen1"); + final Identifiable gen2 = Mockito.mock(Identifiable.class); + when(gen2.getId()).thenReturn("gen2"); + final List> attributes = List.of(gen1, gen2); + mockedFU.when(() -> FiltersUtils.getIdentifiables(filter, network, loader)).thenReturn(attributes); + mockedFU.clearInvocations(); //important because stubbing static method counts as call + assertThat(GlobalFilterUtils.applyFilterOnNetwork(filter, EquipmentType.GENERATOR, network, loader)) + .as("result").containsExactlyInAnyOrder("gen1", "gen2"); + Mockito.verify(filter, Mockito.atLeastOnce()).getEquipmentType(); + Mockito.verify(gen1, Mockito.atLeastOnce()).getId(); + Mockito.verify(gen2, Mockito.atLeastOnce()).getId(); + Mockito.verifyNoMoreInteractions(filter, network, loader, gen1, gen2); + mockedFU.verify(() -> FiltersUtils.getIdentifiables(eq(filter), eq(network), eq(loader)), Mockito.atLeastOnce()); + mockedFU.verifyNoMoreInteractions(); //check if forget to mock a method + } + } + + @Test + void shouldBuildVoltageLevelFilterWhenVoltageLevelType() { + final Network network = Mockito.mock(Network.class); + final FilterLoader loader = Mockito.mock(FilterLoader.class); + final AbstractFilter filter = Mockito.mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.VOLTAGE_LEVEL); + final UUID filterUuid = UuidUtils.createUUID(0); + when(filter.getId()).thenReturn(filterUuid); + try (final MockedStatic mockedFU = Mockito.mockStatic(FiltersUtils.class, Mockito.CALLS_REAL_METHODS)) { + final Identifiable line1 = Mockito.mock(Identifiable.class); + when(line1.getId()).thenReturn("line1"); + final Identifiable line2 = Mockito.mock(Identifiable.class); + when(line2.getId()).thenReturn("line2"); + final List> attributes = List.of(line1, line2); + mockedFU.when(() -> FiltersUtils.getIdentifiables(any(ExpertFilter.class), eq(network), eq(loader))).thenReturn(attributes); + mockedFU.clearInvocations(); //important because stubbing static method counts as call + assertThat(GlobalFilterUtils.applyFilterOnNetwork(filter, EquipmentType.LINE, network, loader)) + .as("result").containsExactlyInAnyOrder("line1", "line2"); + Mockito.verify(filter, Mockito.atLeastOnce()).getEquipmentType(); + Mockito.verify(filter, Mockito.atLeastOnce()).getId(); + Mockito.verify(line1, Mockito.atLeastOnce()).getId(); + Mockito.verify(line2, Mockito.atLeastOnce()).getId(); + Mockito.verifyNoMoreInteractions(filter, network, loader, line1, line2); + mockedFU.verify(() -> FiltersUtils.getIdentifiables(any(ExpertFilter.class), eq(network), eq(loader)), Mockito.atLeastOnce()); + mockedFU.verifyNoMoreInteractions(); //check if forget to mock a method + } + } + + @Test + void shouldReturnEmptyWhenDifferentEquipmentType() { + final FilterLoader loader = Mockito.mock(FilterLoader.class); + final Network network = Mockito.mock(Network.class); + final AbstractFilter filter = Mockito.mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.LOAD); + assertThat(GlobalFilterUtils.applyFilterOnNetwork(filter, EquipmentType.GENERATOR, network, loader)) + .as("result").isEmpty(); + Mockito.verify(filter, Mockito.atLeastOnce()).getEquipmentType(); + Mockito.verifyNoMoreInteractions(loader, network, filter); + } + } + + //TODO applyGlobalFilterOnNetwork(single) + //TODO applyGlobalFilterOnNetwork(list) +} diff --git a/src/test/java/org/gridsuite/filter/utils/FiltersUtilsTest.java b/src/test/java/org/gridsuite/filter/utils/FiltersUtilsTest.java index 6a9f279..f657ddc 100644 --- a/src/test/java/org/gridsuite/filter/utils/FiltersUtilsTest.java +++ b/src/test/java/org/gridsuite/filter/utils/FiltersUtilsTest.java @@ -9,42 +9,32 @@ import com.powsybl.iidm.network.*; import com.powsybl.iidm.network.test.*; import org.apache.commons.collections4.CollectionUtils; +import org.assertj.core.api.WithAssertions; import org.gridsuite.filter.FilterLoader; import org.gridsuite.filter.expertfilter.ExpertFilter; import org.gridsuite.filter.expertfilter.expertrule.AbstractExpertRule; import org.gridsuite.filter.expertfilter.expertrule.CombinatorExpertRule; import org.gridsuite.filter.expertfilter.expertrule.StringExpertRule; -import org.gridsuite.filter.identifierlistfilter.FilterEquipments; -import org.gridsuite.filter.identifierlistfilter.FilteredIdentifiables; -import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; -import org.gridsuite.filter.identifierlistfilter.IdentifierListFilter; -import org.gridsuite.filter.identifierlistfilter.IdentifierListFilterEquipmentAttributes; +import org.gridsuite.filter.identifierlistfilter.*; import org.gridsuite.filter.utils.expertfilter.CombinatorType; import org.gridsuite.filter.utils.expertfilter.OperatorType; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.*; -import static org.gridsuite.filter.utils.expertfilter.FieldType.*; -import static org.gridsuite.filter.utils.expertfilter.OperatorType.*; +import static org.gridsuite.filter.utils.expertfilter.FieldType.ID; +import static org.gridsuite.filter.utils.expertfilter.FieldType.NAME; +import static org.gridsuite.filter.utils.expertfilter.OperatorType.IS; import static org.junit.jupiter.api.Assertions.*; /** * @author Franck Lecuyer */ -class FiltersUtilsTest { - private Network network; - private Network network2; - private Network network3; - private Network network4; - private Network network5; - - private FilterLoader filterLoader; - - @BeforeEach - void setUp() { - network = EurostagTutorialExample1Factory.createWithMoreGenerators(); +class FiltersUtilsTest implements WithAssertions { + private final FilterLoader filterLoader = uuids -> null; + + private static Network prepareNetwork() { + Network network = EurostagTutorialExample1Factory.createWithMoreGenerators(); network.getSubstation("P1").setProperty("region", "north"); network.getSubstation("P2").setProperty("region", "south"); network.getGenerator("GEN").setProperty("region", "north"); @@ -54,17 +44,18 @@ void setUp() { network.getTwoWindingsTransformer("NHV2_NLOAD").setProperty("region", "south"); network.getLine("NHV1_NHV2_1").setProperty("region", "south"); network.getLine("NHV1_NHV2_2").setProperty("region", "south"); + return network; + } - network2 = HvdcTestNetwork.createVsc(); + private static Network prepareHvdcNetwork() { + Network network2 = HvdcTestNetwork.createVsc(); network2.getSubstation("S2").setProperty("region", "north"); - network3 = SvcTestCaseFactory.createWithMoreSVCs(); - network4 = ShuntTestCaseFactory.create(); - network5 = ThreeWindingsTransformerNetworkFactory.create(); - filterLoader = uuids -> null; + return network2; } @Test void testSubstationFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -95,6 +86,7 @@ void testSubstationFilter() { @Test void testVoltageLevelFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -124,6 +116,7 @@ void testVoltageLevelFilter() { @Test void testLineFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -155,6 +148,7 @@ void testLineFilter() { @Test void testTwoWindingsTransformerFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -184,6 +178,7 @@ void testTwoWindingsTransformerFilter() { @Test void testThreeWindingsTransformerFilter() { + final Network network5 = ThreeWindingsTransformerNetworkFactory.create(); // expert filter ExpertFilter expertFilter = new ExpertFilter( @@ -200,6 +195,7 @@ void testThreeWindingsTransformerFilter() { @Test void testGeneratorFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -232,6 +228,7 @@ void testGeneratorFilter() { @Test void testLoadFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -261,6 +258,7 @@ void testLoadFilter() { @Test void testBatteryFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -288,6 +286,7 @@ void testBatteryFilter() { @Test void testShuntCompensatorFilter() { + final Network network4 = ShuntTestCaseFactory.create(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -317,6 +316,7 @@ void testShuntCompensatorFilter() { @Test void testStaticVarCompensatorFilter() { + final Network network3 = SvcTestCaseFactory.createWithMoreSVCs(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -349,6 +349,7 @@ void testStaticVarCompensatorFilter() { @Test void testDanglingLineFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -376,6 +377,7 @@ void testDanglingLineFilter() { @Test void testBusbarSectionFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -403,6 +405,8 @@ void testBusbarSectionFilter() { @Test void testBusFilter() { + final Network network = prepareNetwork(); + // expert filter only for bus ExpertFilter expertFilter = new ExpertFilter( UUID.randomUUID(), @@ -417,6 +421,7 @@ void testBusFilter() { @Test void testLccConverterStationFilter() { + final Network network = prepareNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -444,6 +449,7 @@ void testLccConverterStationFilter() { @Test void testHvdcLineFilter() { + final Network network2 = prepareHvdcNetwork(); // identifier list filter List filterEquipmentAttributes = List.of( @@ -474,6 +480,7 @@ void testHvdcLineFilter() { @Test void testIdentifierListFilter() { + final Network network = prepareNetwork(); List filterEquipmentAttributes = List.of( new IdentifierListFilterEquipmentAttributes("GEN", 30.), new IdentifierListFilterEquipmentAttributes("notFound1", 50.), @@ -513,6 +520,7 @@ void testIdentifierListFilter() { @Test void testFilterLoader() { + final Network network = prepareNetwork(); // with identifier list filter List filterEquipmentAttributes = List.of( new IdentifierListFilterEquipmentAttributes("GEN", 30.), @@ -539,6 +547,7 @@ void testFilterLoader() { @Test void testEquipmentNameFilterNoMatch() { + final Network network2 = prepareHvdcNetwork(); List rules = new ArrayList<>(); rules.add(StringExpertRule.builder().field(NAME).operator(IS).value("unexisting name").build()); AbstractExpertRule parentRule = CombinatorExpertRule.builder().combinator(CombinatorType.AND).rules(rules).build(); @@ -555,6 +564,7 @@ void testEquipmentNameFilterNoMatch() { @Test void testEquipmentNameFilterWithMatch() { + final Network network2 = prepareHvdcNetwork(); List rules = new ArrayList<>(); rules.add(StringExpertRule.builder().field(NAME).operator(IS).value("Converter1").build()); AbstractExpertRule parentRule = CombinatorExpertRule.builder().combinator(CombinatorType.AND).rules(rules).build(); @@ -570,6 +580,7 @@ void testEquipmentNameFilterWithMatch() { @Test void testEquipmentNameFilterWithNullValueInEquipments() { + final Network network = prepareNetwork(); List rules = new ArrayList<>(); rules.add(StringExpertRule.builder().field(NAME).operator(IS).value("some name").build()); AbstractExpertRule parentRule = CombinatorExpertRule.builder().combinator(CombinatorType.AND).rules(rules).build(); @@ -583,4 +594,27 @@ void testEquipmentNameFilterWithNullValueInEquipments() { // in this network, VL equipments have null name => no match assertEquals(0, identifiables.size()); } + + @Test + void shouldReturnEmptyWhenCombineFilterResultsInputIsEmpty() { + assertThat(FiltersUtils.combineFilterResults(List.of(), true)).as("result").isEmpty(); + } + + @Test + void shouldReturnIntersectionWhenUsingAndLogic() { + assertThat(FiltersUtils.combineFilterResults(Arrays.asList( + List.of("item1", "item2", "item3"), + List.of("item2", "item3", "item4"), + List.of("item2", "item5")), true)) + .as("result").singleElement().isEqualTo("item2"); + } + + @Test + void shouldReturnUnionWhenUsingOrLogic() { + assertThat(FiltersUtils.combineFilterResults(Arrays.asList( + List.of("item1", "item2"), + List.of("item3", "item4"), + List.of("item5")), false)) + .as("result").containsExactlyInAnyOrder("item1", "item2", "item3", "item4", "item5"); + } } diff --git a/src/test/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtilsTest.java b/src/test/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtilsTest.java new file mode 100644 index 0000000..2de5c5c --- /dev/null +++ b/src/test/java/org/gridsuite/filter/utils/expertfilter/ExpertFilterUtilsTest.java @@ -0,0 +1,71 @@ +package org.gridsuite.filter.utils.expertfilter; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.OptionalAssert; +import org.assertj.core.api.WithAssertions; +import org.gridsuite.filter.expertfilter.expertrule.AbstractExpertRule; +import org.gridsuite.filter.expertfilter.expertrule.CombinatorExpertRule; +import org.gridsuite.filter.expertfilter.expertrule.FilterUuidExpertRule; +import org.gridsuite.filter.utils.EquipmentType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.Mockito.mock; + +class ExpertFilterUtilsTest implements WithAssertions { + @ParameterizedTest + @MethodSource("orCombinationData") + void shouldCreateOrCombination(final List rules, final boolean expectEmpty, final boolean expectSingle) { + final OptionalAssert assertOpt = assertThat(ExpertFilterUtils.buildOrCombination(rules)).as("result"); + if (expectEmpty) { + assertOpt.isEmpty(); + } else { + assertOpt.isPresent(); + final var assertRule = assertOpt.get().as("rule"); + if (expectSingle) { + assertRule.isEqualTo(rules.getFirst()); + } else { + assertRule.asInstanceOf(InstanceOfAssertFactories.type(CombinatorExpertRule.class)).satisfies( + cer -> assertThat(cer.getCombinator()).as("combinator").isEqualTo(CombinatorType.OR), + cer -> assertThat(cer.getRules()).as("rules").containsExactlyInAnyOrderElementsOf(rules) + ); + } + } + } + + private static Stream orCombinationData() { + return Stream.of( + Arguments.of(List.of(), true, false), + Arguments.of(List.of(mock(AbstractExpertRule.class)), false, true), + Arguments.of(List.of(mock(AbstractExpertRule.class), mock(AbstractExpertRule.class)), false, false) + ); + } + + @Test + void shouldCreateExpertFilterWithVoltageLevelIdsCriteria() { + final UUID filterUuid = UUID.randomUUID(); + final EquipmentType equipmentType = EquipmentType.LINE; + assertThat(ExpertFilterUtils.buildExpertFilterWithVoltageLevelIdsCriteria(filterUuid, equipmentType)).as("result").isNotNull().satisfies( + ef -> assertThat(ef.getEquipmentType()).as("equipmentType").isEqualTo(equipmentType), + ef -> assertThat(ef.getRules()).as("expert rules").isNotNull() + .asInstanceOf(InstanceOfAssertFactories.type(CombinatorExpertRule.class)) + .satisfies( + cer -> assertThat(cer.getCombinator()).as("combinator").isEqualTo(CombinatorType.OR), + cer -> assertThat(cer.getRules()).as("combinator rules").hasSize(2) + .allSatisfy(er -> assertThat(er).as("expert rule") + .asInstanceOf(InstanceOfAssertFactories.type(FilterUuidExpertRule.class)) + .satisfies(fuer -> assertThat(fuer.getOperator()).as("operator").isEqualTo(OperatorType.IS_PART_OF)) + .extracting(FilterUuidExpertRule::getValues, InstanceOfAssertFactories.set(String.class)).as("UUIDs") + .containsExactly(filterUuid.toString())) + .map(AbstractExpertRule::getField).as("fields") + .containsExactly(FieldType.VOLTAGE_LEVEL_ID_1, FieldType.VOLTAGE_LEVEL_ID_2) + ) + ); + } +}