From ecd0054e9a4b9c5d00edef7e535d605cd70c9c7d Mon Sep 17 00:00:00 2001 From: e551763 Date: Wed, 2 Oct 2024 17:40:30 +0200 Subject: [PATCH] fix: options mapping refactoring Refs: SchweizerischeBundesbahnen/ch.sbb.polarion.extension.excel-importer#43 --- .../StringToEnumOptionConverter.java | 45 +++++++------------ .../generic/util/OptionsMappingUtils.java | 41 +++++++++++++++++ .../fields/CustomFieldEnumConverterTest.java | 5 --- .../generic/util/OptionsMappingUtilsTest.java | 39 ++++++++++++++++ 4 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtils.java create mode 100644 app/src/test/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtilsTest.java diff --git a/app/src/main/java/ch/sbb/polarion/extension/generic/fields/converters/StringToEnumOptionConverter.java b/app/src/main/java/ch/sbb/polarion/extension/generic/fields/converters/StringToEnumOptionConverter.java index 8343ae7..0dc2558 100644 --- a/app/src/main/java/ch/sbb/polarion/extension/generic/fields/converters/StringToEnumOptionConverter.java +++ b/app/src/main/java/ch/sbb/polarion/extension/generic/fields/converters/StringToEnumOptionConverter.java @@ -9,20 +9,17 @@ import ch.sbb.polarion.extension.generic.fields.model.FieldMetadata; import ch.sbb.polarion.extension.generic.service.PolarionService; import ch.sbb.polarion.extension.generic.util.EnumUtils; -import com.polarion.alm.shared.util.StringUtils; +import ch.sbb.polarion.extension.generic.util.OptionsMappingUtils; import com.polarion.alm.tracker.model.IWorkItem; import com.polarion.platform.persistence.IEnumOption; import com.polarion.platform.persistence.IEnumeration; import com.polarion.subterra.base.data.model.IEnumType; +import lombok.SneakyThrows; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; +import java.util.function.Supplier; public class StringToEnumOptionConverter implements IConverter { @@ -36,6 +33,7 @@ public String convertBack(@NotNull IEnumOption value, @NotNull ConverterContext return value.getName(); } + @SneakyThrows @Nullable @SuppressWarnings({"squid:S5852", "squid:S3776"}) // regex is safe here, ignore cognitive complexity warning private IEnumOption getEnumerationOptionForField(@NotNull FieldMetadata fieldMetadata, @NotNull String initialValue, @NotNull ConverterContext context) { @@ -46,29 +44,16 @@ private IEnumOption getEnumerationOptionForField(@NotNull FieldMetadata fieldMet if (FieldType.unwrapIfListType(fieldMetadata.getType()) instanceof IEnumType enumType) {//attempt to unwrap type coz this converter may be used from ListConverter IEnumeration enumeration = new PolarionService().getEnumeration(enumType, context.getContextId()); - Map enumMapping = Optional.ofNullable(context.getEnumsMapping()) //mapping may be null - .orElse(new HashMap<>()) - .getOrDefault(fieldMetadata.getId(), new HashMap<>()) - .entrySet().stream() - .filter(entry -> !StringUtils.isEmptyTrimmed(entry.getValue())) //remove entries with null/empty/blank values - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - List existingOptionIds = enumeration.getAllOptions().stream().map(EnumUtils::getEnumId).toList(); - for (Map.Entry entry : enumMapping.entrySet()) { - //enumMapping may contain non-actual data coz enum elements may be changed after enumMapping definition - //therefore we have to check whether this particular option still exists or not - if (existingOptionIds.contains(entry.getKey()) && Arrays.asList(StringUtils.getNotNull(entry.getValue()).trim().split("\\s*,\\s*")).contains(value) || entry.getKey().equals(value)) { - return enumeration.getAllOptions().stream() - .filter(o -> entry.getKey().equals(EnumUtils.getEnumId(o))) - .findFirst() - .orElseThrow(); - } + // first we attempt to find the option using custom mapping + String mappingOptionKey = OptionsMappingUtils.getMappedOptionKey(fieldMetadata.getId(), value, context.getEnumsMapping()); + if (mappingOptionKey != null) { + return enumeration.getAllOptions().stream().filter(o -> mappingOptionKey.equals(EnumUtils.getEnumId(o))) + .findFirst().orElseThrow((Supplier) () -> new IllegalArgumentException("Value %s mapped to unknown key %s".formatted(value, mappingOptionKey))); } try { - //the rest unmapped options will be processed using keys/values - List unmappedOptions = enumeration.getAllOptions().stream().filter(o -> !enumMapping.containsKey(EnumUtils.getEnumId(o))).toList(); - return findEnumOption(value, enumType, unmappedOptions); + // as a last stand try to find the option using keys/values + return findEnumOption(value, enumType, enumeration); } catch (EnumOptionNotFoundException e) { if (value.isBlank()) { return handleEmptyValue(fieldMetadata, enumType, enumeration.getAllOptions()); @@ -106,16 +91,16 @@ private IEnumOption findEnumDefaultValue(@NotNull IEnumType enumType, @NotNull L @NotNull @SuppressWarnings("squid:S1166") // no need to log or rethrow exception by design - private static IEnumOption findEnumOption(@NotNull String value, @NotNull IEnumType enumType, @NotNull List options) throws EnumOptionNotFoundException { + private static IEnumOption findEnumOption(@NotNull String value, @NotNull IEnumType enumType, @NotNull IEnumeration enumeration) throws EnumOptionNotFoundException { try { //first we try to find value by ID - return options.stream() + return enumeration.getAllOptions().stream() .filter(option -> EnumUtils.getEnumId(option).equals(value)) .findFirst() .orElseThrow(() -> new EnumOptionNotFoundByIdException(String.format("EnumOption with id '%s' not found", value))); } catch (EnumOptionNotFoundByIdException e) { - //if nothing found then we peek first option by name ignore case - return options.stream() + // peek first option by name ignore case + return enumeration.getAllOptions().stream() .filter(option -> option.getName().trim().equalsIgnoreCase(value)) // enum names in Polarion can have spaces at the end .findFirst() .orElseThrow(() -> new EnumOptionNotFoundException(String.format("Unsupported value '%s' for enum '%s'", value, enumType.getEnumerationId()))); diff --git a/app/src/main/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtils.java b/app/src/main/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtils.java new file mode 100644 index 0000000..fe34785 --- /dev/null +++ b/app/src/main/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtils.java @@ -0,0 +1,41 @@ +package ch.sbb.polarion.extension.generic.util; + +import com.polarion.alm.shared.util.StringUtils; +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@UtilityClass +public class OptionsMappingUtils { + + String EMPTY_VALUE = "(empty)"; + + @NotNull + public Map getMappingForFieldId(String fieldId, Map> commonMapping) { + return Optional.ofNullable(commonMapping) + .orElse(new HashMap<>()) + .getOrDefault(fieldId, new HashMap<>()) + .entrySet().stream() + .filter(entry -> !StringUtils.isEmptyTrimmed(entry.getValue())) //remove entries with null/empty/blank values + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Nullable + public String getMappedOptionKey(String fieldId, @Nullable Object initialValue, @Nullable Map> commonMapping) { + if (initialValue == null || initialValue instanceof String) { + String value = StringUtils.isEmptyTrimmed((String) initialValue) ? EMPTY_VALUE : ((String) initialValue); + for (Map.Entry entry : getMappingForFieldId(fieldId, commonMapping).entrySet()) { + if (Stream.of(StringUtils.getNotNull(entry.getValue()).split(",")).anyMatch(v -> StringUtils.areEqualTrimmed(v, value))) { + return entry.getKey(); + } + } + } + return null; + } +} diff --git a/app/src/test/java/ch/sbb/polarion/extension/generic/fields/CustomFieldEnumConverterTest.java b/app/src/test/java/ch/sbb/polarion/extension/generic/fields/CustomFieldEnumConverterTest.java index b5c17f6..a481560 100644 --- a/app/src/test/java/ch/sbb/polarion/extension/generic/fields/CustomFieldEnumConverterTest.java +++ b/app/src/test/java/ch/sbb/polarion/extension/generic/fields/CustomFieldEnumConverterTest.java @@ -137,11 +137,6 @@ void testGetEnumOptionByMapping() { assertEquals(new EnumOption(YES_NO_ENUM_ID, YES.left()), ConverterTestUtils.process("yes", context, fieldMetadata)); - //default key/value option identification hidden by the implicit mapping - assertThrows(IllegalArgumentException.class, - () -> ConverterTestUtils.process("Ja", context, fieldMetadata), - "Unsupported value 'Ja' for enum ''"); - //just an unknown option assertThrows(IllegalArgumentException.class, () -> ConverterTestUtils.process("Probably", context, fieldMetadata), diff --git a/app/src/test/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtilsTest.java b/app/src/test/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtilsTest.java new file mode 100644 index 0000000..08bac49 --- /dev/null +++ b/app/src/test/java/ch/sbb/polarion/extension/generic/util/OptionsMappingUtilsTest.java @@ -0,0 +1,39 @@ +package ch.sbb.polarion.extension.generic.util; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class OptionsMappingUtilsTest { + + @Test + void testGetMappingForFieldId() { + assertEquals(new HashMap<>(), OptionsMappingUtils.getMappingForFieldId("fieldId", null)); + assertEquals(new HashMap<>(), OptionsMappingUtils.getMappingForFieldId("fieldId", Map.of())); + assertEquals(new HashMap<>(), OptionsMappingUtils.getMappingForFieldId("fieldId", Map.of("fieldId", new HashMap<>()))); + assertEquals(Map.of("key3", "someValue"), OptionsMappingUtils.getMappingForFieldId("fieldId", Map.of("fieldId", Map.of("key1", "", "key2", " ", "key3", "someValue")))); + } + + @Test + void testGetMappedOptionKey() { + assertNull(OptionsMappingUtils.getMappedOptionKey("field", null, null)); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", Boolean.TRUE, null)); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", 42, Map.of("field", Map.of("someKey", "42")))); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", "", null)); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", " ", null)); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", " ", Map.of("field", Map.of()))); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", null, Map.of("field", Map.of("someKey", "someValue")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", "someValue", Map.of("field", Map.of("someKey", "someValue")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", " someValue ", Map.of("field", Map.of("someKey", "someValue")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", "someValue", Map.of("field", Map.of("someKey", " someValue")))); + assertNull(OptionsMappingUtils.getMappedOptionKey("field", "", Map.of("field", Map.of("someKey", "")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", "", Map.of("field", Map.of("someKey", "(empty),someValue")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", " ", Map.of("field", Map.of("someKey", "(empty) ,someValue")))); + assertEquals("someKey", OptionsMappingUtils.getMappedOptionKey("field", null, Map.of("field", Map.of("someKey", "(empty) ,someValue")))); + } + +} \ No newline at end of file