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 b0372279a..749df201c 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 @@ -85,6 +85,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; import org.apache.fesod.sheet.converters.year.YearStringConverter; /** @@ -162,6 +163,8 @@ private static void initAllConverter() { putAllConverter(new StringStringConverter()); putAllConverter(new StringErrorConverter()); + putAllConverter(new UUIDStringConverter()); + putAllConverter(new YearStringConverter()); allConverter = Collections.unmodifiableMap(allConverter); } @@ -191,6 +194,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()); @@ -210,6 +214,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LongStringConverter()); putWriteStringConverter(new ShortStringConverter()); putWriteStringConverter(new StringStringConverter()); + putWriteStringConverter(new UUIDStringConverter()); putWriteStringConverter(new YearStringConverter()); 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..673636b11 --- /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 a9240a120..e6a9d2b5b 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 @@ -22,6 +22,7 @@ import java.sql.Time; 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; @@ -29,6 +30,7 @@ import org.apache.fesod.sheet.converters.sqltime.SqlTimeDateConverter; import org.apache.fesod.sheet.converters.sqltime.SqlTimeNumberConverter; import org.apache.fesod.sheet.converters.sqltime.SqlTimeStringConverter; +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; @@ -71,6 +73,20 @@ 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))); + Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); + Assertions.assertInstanceOf( + UUIDStringConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(UUID.class))); + Assertions.assertInstanceOf( + UUIDStringConverter.class, + writeConverter.get(ConverterKeyBuild.buildKey(UUID.class, CellDataTypeEnum.STRING))); + } + @Test void loadConvertersRegistersSqlTimeFamily() { Map> allConverter = DefaultConverterLoader.loadAllConverter();