diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java index f17d9b62c..9498dd112 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java @@ -78,6 +78,7 @@ import org.apache.fesod.sheet.converters.string.StringNumberConverter; import org.apache.fesod.sheet.converters.string.StringStringConverter; import org.apache.fesod.sheet.converters.url.UrlImageConverter; +import org.apache.fesod.sheet.converters.uuid.UUIDStringConverter; /** * Load default handler @@ -147,6 +148,7 @@ private static void initAllConverter() { putAllConverter(new StringNumberConverter()); putAllConverter(new StringStringConverter()); putAllConverter(new StringErrorConverter()); + putAllConverter(new UUIDStringConverter()); allConverter = Collections.unmodifiableMap(allConverter); } @@ -171,6 +173,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new ByteArrayImageConverter()); putWriteConverter(new BoxingByteArrayImageConverter()); putWriteConverter(new UrlImageConverter()); + putWriteConverter(new UUIDStringConverter()); // In some cases, it must be converted to string putWriteStringConverter(new BigDecimalStringConverter()); @@ -187,6 +190,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LongStringConverter()); putWriteStringConverter(new ShortStringConverter()); putWriteStringConverter(new StringStringConverter()); + putWriteStringConverter(new UUIDStringConverter()); defaultWriteConverter = Collections.unmodifiableMap(defaultWriteConverter); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/uuid/UUIDStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/uuid/UUIDStringConverter.java new file mode 100644 index 000000000..68c2a14a2 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/uuid/UUIDStringConverter.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.converters.uuid; + +import java.util.UUID; +import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; + +/** + * Converts UUID values to canonical lowercase strings and reads strings using {@link UUID#fromString(String)}. + * Blank strings are treated as missing values, like blank cells. Leading and trailing whitespace is trimmed before parsing. + */ +public class UUIDStringConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return UUID.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public UUID convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String value = cellData.getStringValue(); + return StringUtils.isBlank(value) ? null : UUID.fromString(value.trim()); + } + + @Override + public WriteCellData convertToExcelData( + UUID value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.toString()); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterRoundTripTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterRoundTripTest.java new file mode 100644 index 000000000..6a0c48881 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterRoundTripTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.converter; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import lombok.Data; +import org.apache.fesod.sheet.exception.ExcelDataConvertException; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.testkit.helpers.RoundTripHelper; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; + +@Tag(Tags.ROUND_TRIP) +class UUIDConverterRoundTripTest extends AbstractExcelTest { + + private static final String TEXT = "123e4567-e89b-12d3-a456-426614174000"; + + @ParameterizedTest + @ExcelFormatSource + void roundTripsUuidAndNullFieldsWithoutCustomRegistration(ExcelFormat format) throws Exception { + File file = createTempFile(format); + UuidData first = new UuidData(); + first.setId(UUID.fromString(TEXT)); + first.setLabel("populated"); + UuidData second = new UuidData(); + second.setLabel("null UUID"); + List expected = Arrays.asList(first, second); + Assertions.assertEquals(expected, RoundTripHelper.writeAndRead(file, UuidData.class, expected)); + List strings = RoundTripHelper.read(file, StringData.class); + Assertions.assertEquals(TEXT, strings.get(0).getId()); + } + + @ParameterizedTest + @ExcelFormatSource + void readsUppercaseAndBlankCells(ExcelFormat format) throws Exception { + File file = createTempFile(format); + StringData first = new StringData(); + first.setId(TEXT.toUpperCase(Locale.ROOT)); + first.setLabel("uppercase"); + StringData second = new StringData(); + second.setId(""); + second.setLabel("blank UUID"); + RoundTripHelper.write(file, StringData.class, Arrays.asList(first, second)); + List rows = RoundTripHelper.read(file, UuidData.class); + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals(UUID.fromString(TEXT), rows.get(0).getId()); + Assertions.assertNull(rows.get(1).getId()); + } + + @ParameterizedTest + @ExcelFormatSource + void reportsInvalidCellAsConversionFailure(ExcelFormat format) throws Exception { + File file = createTempFile(format); + StringData row = new StringData(); + row.setId("not-a-uuid"); + row.setLabel("invalid"); + RoundTripHelper.write(file, StringData.class, Collections.singletonList(row)); + ExcelDataConvertException error = Assertions.assertThrows( + ExcelDataConvertException.class, () -> RoundTripHelper.read(file, UuidData.class)); + Assertions.assertInstanceOf(IllegalArgumentException.class, error.getCause()); + } + + @Data + public static class UuidData { + private UUID id; + private String label; + } + + @Data + public static class StringData { + private String id; + private String label; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterTest.java new file mode 100644 index 000000000..71fbb1558 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/UUIDConverterTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.converter; + +import java.util.Locale; +import java.util.UUID; +import org.apache.fesod.sheet.converters.uuid.UUIDStringConverter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +@Tag(Tags.UNIT) +class UUIDConverterTest { + + private static final String TEXT = "123e4567-e89b-12d3-a456-426614174000"; + private final UUIDStringConverter converter = new UUIDStringConverter(); + private final GlobalConfiguration configuration = new GlobalConfiguration(); + + @Test + void supportsUuidStrings() { + Assertions.assertEquals(UUID.class, converter.supportJavaTypeKey()); + Assertions.assertEquals(CellDataTypeEnum.STRING, converter.supportExcelTypeKey()); + } + + @ParameterizedTest + @ValueSource( + strings = { + TEXT, + "123E4567-E89B-12D3-A456-426614174000", + "00000000-0000-0000-0000-000000000000", + "ffffffff-ffff-ffff-ffff-ffffffffffff" + }) + void readsAndWritesCanonicalStrings(String input) { + UUID value = converter.convertToJavaData(new ReadCellData<>(input), null, configuration); + WriteCellData cell = converter.convertToExcelData(value, null, configuration); + Assertions.assertEquals(CellDataTypeEnum.STRING, cell.getType()); + Assertions.assertEquals(input.toLowerCase(Locale.ROOT), cell.getStringValue()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "not-a-uuid", + "123e4567-e89b-12d3-a456-42661417400g", + "123e4567e89b12d3a456426614174000", + " not-a-uuid\t", + "123e4567-e89b-12d3-a456-42661417 4000" + }) + void rejectsInvalidInput(String input) { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> converter.convertToJavaData(new ReadCellData<>(input), null, configuration)); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t\r\n", "\u2003"}) + void readsBlankStringAsMissingValue(String input) { + ReadCellData cell = new ReadCellData<>(); + cell.setStringValue(input); + Assertions.assertNull(converter.convertToJavaData(cell, null, configuration)); + } + + @ParameterizedTest + @ValueSource(strings = {" " + TEXT, TEXT + " ", "\t" + TEXT + "\r\n"}) + void trimsWhitespaceBeforeParsing(String input) { + Assertions.assertEquals( + UUID.fromString(TEXT), converter.convertToJavaData(new ReadCellData<>(input), null, configuration)); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java index a30708a52..4b7278fb5 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java @@ -21,10 +21,12 @@ import java.time.LocalTime; import java.util.Map; +import java.util.UUID; import org.apache.fesod.sheet.converters.ConverterKeyBuild.ConverterKey; import org.apache.fesod.sheet.converters.localtime.LocalTimeDateConverter; import org.apache.fesod.sheet.converters.localtime.LocalTimeNumberConverter; import org.apache.fesod.sheet.converters.localtime.LocalTimeStringConverter; +import org.apache.fesod.sheet.converters.uuid.UUIDStringConverter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -67,6 +69,21 @@ void loadConvertersRegistersLocalTimeFamily() { writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.STRING))); } + @Test + void loadConvertersRegistersUuidFamily() { + Assertions.assertInstanceOf( + UUIDStringConverter.class, + DefaultConverterLoader.loadDefaultReadConverter() + .get(ConverterKeyBuild.buildKey(UUID.class, CellDataTypeEnum.STRING))); + Assertions.assertInstanceOf( + UUIDStringConverter.class, + DefaultConverterLoader.loadDefaultWriteConverter().get(ConverterKeyBuild.buildKey(UUID.class))); + Assertions.assertInstanceOf( + UUIDStringConverter.class, + DefaultConverterLoader.loadDefaultWriteConverter() + .get(ConverterKeyBuild.buildKey(UUID.class, CellDataTypeEnum.STRING))); + } + private static void assertLoadIsImmutableAndCopyIsMutable( Map> loaded, Map> copy) { Map.Entry> entry =