diff --git a/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilter.java b/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilter.java new file mode 100644 index 000000000..d7e776c64 --- /dev/null +++ b/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilter.java @@ -0,0 +1,47 @@ +package ru.spcex.clearing.dbf.exporter.logic; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Objects; + +/** + * При перекодировании текста в кодировку, которая не поддерживает символы - они заменяются на знак "?". + * Надо чтобы они заменялись не на "?", а на пробел. + */ +public class CharsetFilter { + final Charset charset; + + public CharsetFilter(Charset dbfCharset) { + this.charset = dbfCharset; + Objects.requireNonNull(charset); + } + + public String filterUnsupportedChars(String text) { + if (text == null || text.isEmpty()) + return text; + CharBuffer textBuf = CharBuffer.wrap(text); + ByteBuffer bBuffer = charset.encode(textBuf); + CharBuffer cBuffer = charset.decode(bBuffer); + if (cBuffer.equals(textBuf)) { + return text; + } else { + char[] arr = text.toCharArray(); + if (arr.length != cBuffer.length()) + return text; // never + for (int i = 0; i < arr.length; i++) { + if (cBuffer.get(i) != arr[i]) { + arr[i] = ' '; + } + } + return String.valueOf(arr); + } + + /* Можно было так: + sun.nio.cs.ThreadLocalCoders.encoderFor(charset) + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .encode(textBuf); + но для этого надо добавить '--add-exports java.base/sun.nio.cs=ALL-UNNAMED' в опции*/ + } +} diff --git a/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/stages/ExportFromHazelcast.java b/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/stages/ExportFromHazelcast.java index eeaf67dd0..6b948f582 100644 --- a/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/stages/ExportFromHazelcast.java +++ b/clearing-parent/dbf-exporter/src/main/java/ru/spcex/clearing/dbf/exporter/logic/stages/ExportFromHazelcast.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Component; import ru.spcex.clearing.dbf.exporter.config.SFTPConfig; import ru.spcex.clearing.dbf.exporter.config.settings.ExportDBFServiceSettings; import ru.spcex.clearing.dbf.exporter.exceptions.ConfigException; +import ru.spcex.clearing.dbf.exporter.logic.CharsetFilter; import ru.spcex.clearing.dbf.exporter.logic.data.ResultContainer; import ru.spcex.clearing.dbf.exporter.logic.data.enums.StageResult; import ru.spcex.clearing.dbf.exporter.logic.data.enums.Table; @@ -68,6 +69,7 @@ public class ExportFromHazelcast extends Stage implements InitializingBean { String currency = null; String path = null; try (DBFWriter dbfWriter = new DBFWriter(dbfFile, dbfCharset)) { + CharsetFilter charsetFilter = new CharsetFilter(dbfWriter.getCharset()); Collection tableRows; if (resultContainer.getGroupId() != null) { Map queryParams = Map.of("generationId", resultContainer.getGroupId()); @@ -95,7 +97,13 @@ public class ExportFromHazelcast extends Stage implements InitializingBean { currency = currentConverter.getCurrency(value); dbfWriter.setFields(currentConverter.getDBFHeaders()); } - dbfWriter.addRecord(currentConverter.toObjectArray(value)); + Object[] rawRow = currentConverter.toObjectArray(value); + for (int i = 0; i < rawRow.length; i++) { + if (rawRow[i] instanceof String rowText) { + rawRow[i] = charsetFilter.filterUnsupportedChars(rowText); + } + } + dbfWriter.addRecord(rawRow); } if (CurrencyCode.isRub(currency)) path = settings.getStore().getOutPayValDir().get(ruCurrency); diff --git a/clearing-parent/dbf-exporter/src/test/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilterTest.java b/clearing-parent/dbf-exporter/src/test/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilterTest.java new file mode 100644 index 000000000..4b46eee03 --- /dev/null +++ b/clearing-parent/dbf-exporter/src/test/java/ru/spcex/clearing/dbf/exporter/logic/CharsetFilterTest.java @@ -0,0 +1,99 @@ +package ru.spcex.clearing.dbf.exporter.logic; + +import org.junit.jupiter.api.Test; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import static org.junit.jupiter.api.Assertions.*; + +class CharsetFilterTest { + + @Test + void filterUnsupportedChars_utf8() { + CharsetFilter filter = new CharsetFilter(Charset.forName("utf-8")); + assertEquals(null, filter.filterUnsupportedChars(null)); + assertEquals("", filter.filterUnsupportedChars("")); + String empty = ""; + assertSame(empty, filter.filterUnsupportedChars(empty)); + assertEquals("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));", filter.filterUnsupportedChars("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));")); + assertEquals("Test обычные АБВЭЮЯёЁ ABCXYZ", filter.filterUnsupportedChars("Test обычные АБВЭЮЯёЁ ABCXYZ")); + assertEquals("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=", filter.filterUnsupportedChars("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=")); + assertEquals(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.", + filter.filterUnsupportedChars(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.")); + assertEquals("Акционерный «Абсо\nлют» ", filter.filterUnsupportedChars("Акционерный «Абсо\nлют» ")); + assertEquals("К«а»вычки '\" и п\160р о б\u2060е л\u8199ы. И иные символы: …„‘\n" + + "2018 \t’\n2019 \t“\n201C \t”\n201D \t•\n2022 \t–\n2013 \t—\n2014 \t\t™\n2122 \tљ\n459 \t›", + filter.filterUnsupportedChars("К«а»вычки '\" и п\160р о б\u2060е л\u8199ы. И иные символы: …„‘\n" + + "2018 \t’\n2019 \t“\n201C \t”\n201D \t•\n2022 \t–\n2013 \t—\n2014 \t\t™\n2122 \tљ\n459 \t›")); + } + + @Test + void filterUnsupportedChars_windows1251() { + CharsetFilter filter = new CharsetFilter(Charset.forName("windows-1251")); + assertEquals(null, filter.filterUnsupportedChars(null)); + assertEquals("", filter.filterUnsupportedChars("")); + String empty = ""; + assertSame(empty, filter.filterUnsupportedChars(empty)); + assertEquals("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));", filter.filterUnsupportedChars("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));")); + assertEquals("Test обычные АБВЭЮЯёЁ ABCXYZ", filter.filterUnsupportedChars("Test обычные АБВЭЮЯёЁ ABCXYZ")); + assertEquals("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=", filter.filterUnsupportedChars("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=")); + assertEquals(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.", + filter.filterUnsupportedChars(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.")); + assertEquals("Акционерный «Абсо\nлют» ", filter.filterUnsupportedChars("Акционерный «Абсо\nлют» ")); + assertEquals("К«а»вычки '\" и пpр о б е л ы. И иные символы: …„‘\n" + + "2018 \t’\n2019 \t“\n201C \t”\n201D \t•\n2022 \t–\n2013 \t—\n2014 \t\t™\n2122 \tљ\n459 \t›", + filter.filterUnsupportedChars("К«а»вычки '\" и п\160р о б\u2060е л\u8199ы. И иные символы: …„‘\n" + + "2018 \t’\n2019 \t“\n201C \t”\n201D \t•\n2022 \t–\n2013 \t—\n2014 \t\t™\n2122 \tљ\n459 \t›")); + } + + @Test + void filterUnsupportedChars_cp866() { + CharsetFilter filter = new CharsetFilter(Charset.forName("cp866")); + assertEquals(null, filter.filterUnsupportedChars(null)); + assertEquals("", filter.filterUnsupportedChars("")); + String empty = ""; + assertSame(empty, filter.filterUnsupportedChars(empty)); + assertEquals("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));", filter.filterUnsupportedChars("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));")); + assertEquals("Test обычные АБВЭЮЯёЁ ABCXYZ", filter.filterUnsupportedChars("Test обычные АБВЭЮЯёЁ ABCXYZ")); + assertEquals("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=", filter.filterUnsupportedChars("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=")); + assertEquals(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.", + filter.filterUnsupportedChars(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.")); + assertEquals("Акционерный Абсо\nлют ", filter.filterUnsupportedChars("Акционерный «Абсо\nлют» ")); + assertEquals("К а вычки '\" и пpр о б е л ы. И иные символы: \n" + + "2018 \t \n2019 \t \n201C \t \n201D \t \n2022 \t \n2013 \t \n2014 \t\t \n2122 \t \n459 \t ", + filter.filterUnsupportedChars("К«а»вычки '\" и п\160р о б\u2060е л\u8199ы. И иные символы: …„‘\n" + + "2018 \t’\n2019 \t“\n201C \t”\n201D \t•\n2022 \t–\n2013 \t—\n2014 \t\t™\n2122 \tљ\n459 \t›")); + } + + @Test + void canCorrectEncode() throws UnsupportedEncodingException { + String sample = " 0123456789/*-+:;,. .,~!@#$%^&*()_+` '\"\\(…„‘) К«а»в’ы“ч™кљи п\160р о б\u2060е л\u8199ы qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.ёЁ"; + sample = "Test aaabbbccc АААБББВВВЯЯЯяяяЁ: " + sample + sample + sample; + for (String charset : new String[]{"utf-8", "windows-1251", "cp866"}) { + CharsetFilter filter = new CharsetFilter(Charset.forName(charset)); + String toEnc = filter.filterUnsupportedChars(sample); + byte[] b = toEnc.getBytes(charset); + String returned = new String(b, charset); + assertEquals(toEnc, returned, charset); + } + } + + + /** + * Кодировка не используется, но она применяется по умолчанию в библиотеке DBFStandardCharsets + */ + @Test + void filterUnsupportedChars_ISO_8859_1() { + CharsetFilter filter = new CharsetFilter(Charset.forName("ISO_8859_1")); //DBFStandardCharsets.ISO_8859_1 + assertEquals(null, filter.filterUnsupportedChars(null)); + assertEquals("", filter.filterUnsupportedChars("")); + assertEquals("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));", filter.filterUnsupportedChars("assertEquals(\"\", filter.filterUnsupportedChars(\"\"));")); + assertEquals("Test ABCXYZ", filter.filterUnsupportedChars("Test обычные АБВЭЮЯёЁ ABCXYZ")); + assertEquals("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=", filter.filterUnsupportedChars("/*-+:;'\\/.,~!@#$%^&*()_+`1234567890-=")); + assertEquals(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM .", + filter.filterUnsupportedChars(" 0123456789/*-+:;,.'\"\\()qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮйцукенгшщзхъфывапролджэячсмитьбю.")); + assertEquals(" « \n » ", filter.filterUnsupportedChars("Акционерный «Абсо\nлют» ")); + } + +} \ No newline at end of file