Статьи

Ужас полицейского: локали по умолчанию, кодировки по умолчанию и часовые пояса по умолчанию

Время для инструмента, чтобы предотвратить любые эффекты от них!


Вы когда-нибудь пытались запустить программное обеспечение, загруженное из сети, на компьютере с турецкой локалью? Я думаю, что большинство из вас никогда не делали этого. И если вы спросите турецких ИТ-специалистов, они скажут вам: «Лучше настроить ваш компьютер, используя любую другую локаль, но не
tr_TR ». Я думаю, вы понятия не имеете, о чем я говорю? Может быть, эта статья дает вам подсказку: «
Пропавшая точка мобильного телефона убивает двух человек, а в тюрьме еще трое ».

То, что вы видите во многих программах, — это так называемое нечувствительное к регистру сопоставление ключевых слов, таких как имена параметров или имена функций. В большинстве случаев это реализуется с помощью ввода текста в нижнем или верхнем регистре и сравнения его со списком элементов в нижнем или верхнем регистре. Это работает в большинстве случаев нормально, если вы находитесь в любой точке мира, кроме Турции! Поскольку большинство программистов не заботятся о запуске своего программного обеспечения в Турции, они не тестируют свое программное обеспечение под турецким языком.

Но что происходит с учетом без учета регистра, если он работает в Турции? Давайте возьмем пример:


Пользователь вводит «БИЛЛИ» в поле поиска вашего приложения.
Затем приложение использует представленный ранее подход и нижний регистр «BILLY», а затем сравнивает его с внутренней таблицей (например, наш индекс поиска, таблица параметров, таблица функций, …). Поэтому мы ищем в этой таблице «Билли». Пока все хорошо, отлично работает в США, Германии, Кении, почти везде — кроме Турции. Что происходит в турецком языке, когда мы строчными буквами «Билли»? После прочтения вышеупомянутой статьи вы можете ожидать этого:
оператор
«BILLY» .toLowerCase () в Java возвращает «bılly» (обратите внимание на то, что i: ‘ı’ без точек
U + 0131) . Вы можете попробовать это на своем локальном компьютере, не переконфигурируя его для использования турецкой локали, просто попробуйте следующий код Java:

assertEquals(“bılly”, “BILLY”.toLowerCase(new Locale(“tr_TR”)));

То же самое происходит и в обратном случае, если вы пишете «i» в верхнем регистре, он получает I с точкой («İ»
U + 0130) . Это действительно серьезно, миллионы строк кода на Java и других языках не заботятся о том, чтобы
методы String.toLowerCase () и
String.toUpperCase () могли опционально принимать определенный Locale (подробнее об этом позже). Некоторые примеры из проектов, в которых я участвую:

  • Попробуйте запустить таблицу стилей XSLT с использованием Apache XALAN-XSLTC (или внутреннего интерпретатора XSLT в Java 5) в турецкой локали. Он завершится неудачно с «неизвестной инструкцией», потому что XALAN-XSLTC компилирует XSLT в байт-код Java и каким-то образом помещает в нижний регистр код операции виртуальной машины, прежде чем компилировать его с помощью BCEL (см. XALANJ-2420 , ошибка BCEL # 38787 ).
  • Анализатор HTML SAX NekoHTML использует верхний / нижний регистр без локали для нормализации имен кодировок и имен элементов. Я открыл сообщение об ошибке (выпуск № 3544334 ).
  • Если вы используете PHP в качестве вашего любимого языка сценариев, который не учитывает регистр имен классов и других языковых конструкций, он выдаст ошибку компиляции, как только вы попытаетесь вызвать функцию с «i» в нем (см. PHP bug # 18556 ) , К сожалению, вряд ли эта серьезная ошибка исправлена ​​в PHP 5.3 или 5.4!

Вопрос сейчас: как это решить?

Самый правильный способ сделать это — не вводить строчные буквы вообще! Для сравнения без учета регистра Unicode определяет «сворачивание регистра», которое представляет собой так называемую каноническую форму текста, где все верхние / нижние регистры любого символа нормализуются. К сожалению, этот сложенный текст больше не может быть читаемым текстом (это зависит от реализации, но в большинстве случаев это так). Это просто гарантирует, что сложенный в регистр текст можно сравнивать друг с другом без учета регистра. К сожалению, Java не предлагает вам функцию для получения этой строки, но
ICU-4J может это сделать (см.
UCharacter # foldCase ). Но Java предлагает что-то гораздо лучшее:
String.equalsIgnoreCase (String)
, который внутренне обрабатывает складной корпус! Но во многих случаях вы не можете использовать этот фантастический метод, потому что вы хотите искать такие строки в
HashMap или другом словаре. Без изменения
HashMap для использования
equalsIgnoreCase это никогда бы не сработало. Итак, мы вернулись в нижнем корпусе! Как упоминалось ранее, вы можете передать локаль в
String.toLowerCase () , поэтому наивным подходом будет сообщить Java, что мы находимся в США или используем английский язык:
String.toLowerCase (Locale.US) или
String.toLowerCase (Locale.ENGLISH), Это дает идентичные результаты, но все еще не соответствует. Что произойдет, если правительство США решит использовать строчные / прописные буквы, как в Турции? — Хорошо, не используйте
Locale.US (это также слишком ориентировано на США).
Locale.ENGLISH хорош и очень универсален, но языки также меняются с годами (кто знает?), Но мы хотим, чтобы это было языковым инвариантом! Если вы используете Java 6, есть намного лучшая константа:
Locale.ROOT — вы должны использовать эту константу для нашего строчного примера:
String.toLowerCase (Locale.ROOT)
.

Вы должны начать прямо сейчас и выполнить глобальный поиск / замену во всех ваших Java-проектах (если вы не полагаетесь на специфичное для языка представление текста)! ДЕЙСТВИТЕЛЬНО!

String.toLowerCase — не единственный пример «автоматического использования локали по умолчанию» в Java API. Есть также такие вещи, как преобразование дат или чисел в строки. Если вы используете
Formatter класс, и запустить его где — то в другой стране,
string.Format ( «% F», 15.5f) не всегда может использовать период ( «») в качестве десятичного разделителя; большинство немцев будут знать это. Передача определенной локали здесь помогает в большинстве случаев. Если вы пишете GUI на английском языке, везде передавайте
Locale.ENGLISH , иначе текстовый вывод чисел или дат может не соответствовать языку вашего GUI! Если вы хотите
Formatter вести себя инвариантным образом, использование
Locale.ROOTтоже (тогда он наверняка отформатирует числа с точкой и без запятой для тысяч, как это
делает Float.toString (float) ).

Вторая проблема, затрагивающая множество программного обеспечения, — это две другие общесистемные настраиваемые настройки по умолчанию: кодировка / кодировка по умолчанию и часовой пояс. Если вы открываете текстовый файл с помощью
FileReader или конвертируете
InputStream в
Reader с помощью
InputStreamReaderJava автоматически предполагает, что ввод в кодировке платформы по умолчанию. Это может быть хорошо, если вы хотите, чтобы текст анализировался по умолчанию операционной системы — но если вы передаете текстовый файл вместе с вашим программным пакетом (возможно, как ресурс в вашем JAR-файле), а затем случайно прочитаете его, используя кодировка платформы по умолчанию … она сломает ваше приложение! Итак, моя вторая рекомендация:

Всегда передавайте набор символов любому методу, преобразующему байты в строки (например, InputStream <=> Reader , String.getBytes () , …). Если вы написали текстовый файл и отправили его вместе со своим приложением, только вы знаете его кодировку!

Для часовых поясов можно найти похожие примеры.

Как это влияет на Apache Lucene!

Apache Lucene — это система полнотекстового поиска, которая постоянно работает с текстом на разных языках;
Apache Solr — это поисковый сервер предприятия поверх Lucene, который обрабатывает входные документы на множестве различных кодировок и языков. Поэтому важно, чтобы поисковая библиотека, такая как Lucene, была максимально независимой от локальных настроек компьютера. Библиотека должна четко указывать, какой ввод она хочет получить. Поэтому нам нужны кодировки и локали во всех общедоступных и частных API (или мы используем, например,
java.io.Reader вместо
InputStream, если мы ожидаем поступления текста), поэтому пользователь должен позаботиться.

Мы с Робертом Мьюром рассмотрели исходный код Apache Lucene and Solr для
будущей версии 4.0(альфа-версия уже
доступна на домашней странице Lucene, документация
здесь ). Мы делали это довольно часто, но всякий раз, когда новый фрагмент кода добавляется в дерево исходных текстов, может случиться так, что неопределенные локали, наборы символов или подобные вещи появятся снова. В большинстве случаев это не ошибка коммиттера, это происходит потому, что автозаполнение в IDE автоматически перечисляет возможные методы и параметры разработчику. Часто вы выбираете самый простой вариант (например,
String.toLowerCase () ).

Использование локалей по умолчанию, кодировок и часовых поясов, на мой взгляд, является большой проблемой проектирования в таких языках программирования, как Java. Если существуют методы, чувствительные к локали, эти методы должны принимать локаль, если вы конвертируете поток byte [] в поток char [] , необходимо указать кодировку. Автоматический возврат к настройкам по умолчанию — запрет на использование в серверной среде. 

Если разработчик заинтересован в использовании языка пользователя по умолчанию на компьютере пользователя, он всегда может явно указать язык или кодировку. В нашем примере это будет
String.toLowerCase (Locale.getDefault ())
. Это более многословно, но очевидно, что разработчик намерен делать.

Мое предложение состоит в том, чтобы запретить все эти стандартные методы / классы charset и locale в Java API, объявив их устаревшими как можно скорее, чтобы пользователи перестали использовать их неявно!

Роберт и я собираемся автоматически завершить работу ночных сборок (или компиляции на компьютере разработчика), когда кто-нибудь использует один из вышеуказанных методов в исходном коде Lucene или Solr. Мы рассматривали различные решения, такие как
PMD или
FindBugs , но оба инструмента слишком неаккуратны, чтобы справляться с этим согласованным образом
(PMD не имеет никакого определения метода «charset по умолчанию», а Findbugs имеет только очень короткий список сигнатур методов) . Кроме того, PMD и FindBugs работают очень медленно и часто не могут правильно определить все проблемы. Для сборок Lucene нам нужен только инструмент, который просматривает байт-код всех сгенерированных Java-классов Apache Lucene и Solr и завершает сборку, если обнаружена какая-либо сигнатура, которая нарушает наши требования.

Новый инструмент для полицейского


Я начал взламывать инструмент как пользовательскую задачу ANT, используя
ASM 4.0
(облегченная среда манипулирования байт-кодом Java) . Идея заключалась в том, чтобы предоставить список сигнатур методов, имен полей и простых имен классов, которые должны завершить сборку, как только байт-код получит к нему доступ любым способом. Первая версия этой задачи была опубликована в выпуске
LUCENE-4199 , последующие улучшения заключались в добавлении поддержки полей (
LUCENE-4202 ) и усовершенствованного расширения сигнатур, чтобы также перехватывать вызовы подклассам данных сигнатур (
LUCENE-4206 ).

Тем временем Роберт работал над списком «запрещенных» API. Вот что вышло в первой версии:

java.lang.String#<init>(byte[])
java.lang.String#<init>(byte[],int)
java.lang.String#<init>(byte[],int,int)
java.lang.String#<init>(byte[],int,int,int)
java.lang.String#getBytes()
java.lang.String#getBytes(int,int,byte[],int) 
java.lang.String#toLowerCase()
java.lang.String#toUpperCase()
java.lang.String#format(java.lang.String,java.lang.Object[])
java.io.FileReader
java.io.FileWriter
java.io.ByteArrayOutputStream#toString()
java.io.InputStreamReader#<init>(java.io.InputStream)
java.io.OutputStreamWriter#<init>(java.io.OutputStream)
java.io.PrintStream#<init>(java.io.File)
java.io.PrintStream#<init>(java.io.OutputStream)
java.io.PrintStream#<init>(java.io.OutputStream,boolean)
java.io.PrintStream#<init>(java.lang.String)
java.io.PrintWriter#<init>(java.io.File)
java.io.PrintWriter#<init>(java.io.OutputStream)
java.io.PrintWriter#<init>(java.io.OutputStream,boolean)
java.io.PrintWriter#<init>(java.lang.String)
java.io.PrintWriter#format(java.lang.String,java.lang.Object[])
java.io.PrintWriter#printf(java.lang.String,java.lang.Object[])
java.nio.charset.Charset#displayName()
java.text.BreakIterator#getCharacterInstance()
java.text.BreakIterator#getLineInstance()
java.text.BreakIterator#getSentenceInstance()
java.text.BreakIterator#getWordInstance()
java.text.Collator#getInstance()
java.text.DateFormat#getTimeInstance()
java.text.DateFormat#getTimeInstance(int)
java.text.DateFormat#getDateInstance()
java.text.DateFormat#getDateInstance(int)
java.text.DateFormat#getDateTimeInstance()
java.text.DateFormat#getDateTimeInstance(int,int)
java.text.DateFormat#getInstance()
java.text.DateFormatSymbols#<init>()
java.text.DateFormatSymbols#getInstance()
java.text.DecimalFormat#<init>()
java.text.DecimalFormat#<init>(java.lang.String)
java.text.DecimalFormatSymbols#<init>()
java.text.DecimalFormatSymbols#getInstance()
java.text.MessageFormat#<init>(java.lang.String)
java.text.NumberFormat#getInstance()
java.text.NumberFormat#getNumberInstance()
java.text.NumberFormat#getIntegerInstance()
java.text.NumberFormat#getCurrencyInstance()
java.text.NumberFormat#getPercentInstance()
java.text.SimpleDateFormat#<init>()
java.text.SimpleDateFormat#<init>(java.lang.String)
java.util.Calendar#<init>()
java.util.Calendar#getInstance()
java.util.Calendar#getInstance(java.util.Locale)
java.util.Calendar#getInstance(java.util.TimeZone)
java.util.Currency#getSymbol()
java.util.GregorianCalendar#<init>()
java.util.GregorianCalendar#<init>(int,int,int)
java.util.GregorianCalendar#<init>(int,int,int,int,int)
java.util.GregorianCalendar#<init>(int,int,int,int,int,int)
java.util.GregorianCalendar#<init>(java.util.Locale)
java.util.GregorianCalendar#<init>(java.util.TimeZone)
java.util.Scanner#<init>(java.io.InputStream)
java.util.Scanner#<init>(java.io.File)
java.util.Scanner#<init>(java.nio.channels.ReadableByteChannel)
java.util.Formatter#<init>()
java.util.Formatter#<init>(java.lang.Appendable)
java.util.Formatter#<init>(java.io.File)
java.util.Formatter#<init>(java.io.File,java.lang.String)
java.util.Formatter#<init>(java.io.OutputStream)
java.util.Formatter#<init>(java.io.OutputStream,java.lang.String)
java.util.Formatter#<init>(java.io.PrintStream)
java.util.Formatter#<init>(java.lang.String)
java.util.Formatter#<init>(java.lang.String,java.lang.String)

Используя этот легко расширяемый список, сохраненный в текстовом файле
(в кодировке UTF-8!) , Вы можете очень легко вызвать мою новую задачу ANT (после регистрации в
<taskdef /> ) — взято из build.xml Lucene / Solr.
:

<taskdef resource="lucene-solr.antlib.xml">
  <classpath>
    <pathelement location="${custom-tasks.dir}/build/classes/java" />
    <fileset dir="${custom-tasks.dir}/lib" includes="asm-debug-all-4.0.jar" />
  </classpath>
</taskdef>
<forbidden-apis>
  <classpath refid="additional.dependencies"/>
  <apiFileSet dir="${custom-tasks.dir}/forbiddenApis">
    <include name="jdk.txt" />
    <include name="jdk-deprecated.txt" />
    <include name="commons-io.txt" />
  </apiFileSet>
  <fileset dir="${basedir}/build" includes="**/*.class" />
</forbidden-apis>
The classpath given is used to look up the API signatures (provided as
apiFileSet). Classpath is only needed if signatures are coming from 3rd party libraries. The inner fileset should list all class files to be checked. For running the task you also need
asm-all-4.0.jar available in the task’s classpath.

If you are interested, take the source code, it is open source and released as part of the tool set shipped with Apache Lucene & Solr:
Source,
API lists (revision number 1360240).

At the moment we are investigating other opportunities brought by that tool:

  • We want to ban System.out/err or things like horrible Eclipse-like try…catch…printStackTrace() auto-generated Exception stubs. We can just ban those fields from the java.lang.System class and of course, Throwable#printStackTrace().
  • Using optimized Lucene-provided replacements for JDK API calls. This can be enforced by failing on the JDK signatures.
  • Failing the build on deprecated calls to Java’s API. We can of course print warnings for deprecations, but failing the build is better. And: We use deprecation annotations in Lucene’s own library code, so javac-generated warnings don’t help. We can use the list of deprecated stuff from JDK Javadocs to trigger the failures.

I hope other projects take a similar approach to scan their binary/source code and free it from system dependent API calls, which are not predictable for production systems in the server environment.

Thanks to Robert Muir and Dawid Weiss for help and suggestions!