11. XML/XSL
При разработке формата XML особое внимание уделялось поддержке различных кодировок символов.
Для указания того, какая кодировка была использована используется заголовок XML-документа.
Пример:
<?xml version="1.0" encoding="Windows-1251"?>
Если кодировка указана не была, то по умолчанию предполагается кодировка UTF-8.
На XML-парсер возложена обязанность корректно прочитать заголовок и использовать соответствующую кодировку для получения Unicode-символов.
Разные парсеры могут поддерживать разные наборы кодировок, но UTF-8 обязаны поддерживать все.
Здесь также, как и в случае с JavaMail наименования кодировок, описанные в стандарте XML могут расходится с наименованиями, принятыми в Java.
Разные парсеры по разному выходят из положения.
Crimson просто использует некоторое кол-во дополнительных синонимов, а в остальном полагается на синонимы кодировок из Java.
Xerces же по умолчанию использует внутреннюю таблицу (класс org.apache.xerces.readers.MIME2Java), а если не находит там кодировку, то бросает исключение о неподдерживаемой кодировке.
В Xerces версии 1.4.0 русских кодировок там всего две - KOI8-R и ISO-8859-5.
Однако это поведение по умолчанию можно изменить при помощи разрешения у парсера специального feature "http://apache.org/xml/features/allow-java-encodings".
Если этот feature разрешён (при помощи метода setFeature()), то парсер после поиска в таблице будет пытаться использовать стандартный Java-вский механизм и соответственно Java-вский набор кодировок.
В случае использования интерфейса SAX сделать это можно таким, например, образом (при использовании JAXP):
SAXParserFactory parserFactory=SAXParserFactory.newInstance();
SAXParser parser=parserFactory.newSAXParser();
parser.getXMLReader().setFeature(
"http://apache.org/xml/features/allow-java-encodings",true);
Для DOM, к сожалению, подобного механизма feature-ов не предусмотрено, но можно вместо JAXP для создания DOM напрямую использовать класс org.apache.xerces.parsers.DOMParser, у которого уже есть метод setFeature().
Если же Xerces используется не напрямую, а посредством другого пакета, то необходимо настроить этот пакет дабы он сам выставлял этот feature.
Если же такой возможности не предусмотрено, то остаётся только один выход - править ручками.
Для этого можно или подправить список кодировок в классе org.apache.xerces.readers.MIME2Java или установить указанный feature как true по умолчанию.
Для чтения документа XML из потока данных обычно используется класс org.xml.sax.InputSource.
Собственно сам поток может быть представлен или в виде байтового потока (java.io.InputStream) или в виде потока символов (java.io.Reader).
Соответственно ответственность за корректное распознавание кодировки возлагается или на парсер или на того, кто создаёт объект Reader.
У класса InputSource есть так же метод setEncoding(), при помощи которого можно явно задать кодировку в случае использования потока байтов.
Работает это всё таким образом:
- Если был задан поток символов (Reader), то он будет использован для чтения данных.
Кодировка, установленная методом setEncoding() при этом игнорируется, как игнорируется и кодировка, указанная в заголовке XML-документа.
- Если вместо потока символов был задан поток байтов (InputStream), то используется он.
Если установлена кодировка методом setEncoding(), то используется она, а если нет - то парсер использует кодировку, указанную в заголовке XML-документа.
Если при чтении заголовка XML-документа обнаруживается расхождение между заданной кодировкой и кодировкой из заголовка, то парсеры могут поступать по разному.
Crimson, например, при этом выдаёт предупреждение, а Xerces молча пропускает.
С чтением XML-документов мы разобрались, теперь перейдём к их созданию.
Единого стандарта на создание документов, в отличии от чтения, пока нет.
Предполагается, что, следующая версия рекомендаций комитета
W3C будет включать в себя и создание документов, но пока что создатели парсеров делают кто во что горазд.
В случае с Crimson сохранить созданный документ DOM можно при помощи метода write() у класса org.apache.crimson.tree.XmlDocument.
В качестве аргумента можно передать или поток символов (Writer) или поток байтов (OutputStream).
Вместе с потоком можно передать и необходимую кодировку.
Если использован поток байтов, а кодировка указана не была, то используется UTF-8.
Если использован поток символов вместе с именем кодировки, то имя используется только для записи в заголовок документа.
Если Writer передан без кодировки, то делается проверка - если это экземляр OutputStreamWriter, то для выяснения что писать в заголовок зовётся его метод getEncoding().
Если же это другой Writer, то кодировка в заголовок записана не будет, что по стандарту означает кодировку UTF-8.
Пример:
XmlDocument doc=...;
OutputStream os=...;
doc.write(os,"Windows-1251");
В Xerces для создания документов используются классы из пакета org.apache.xml.serialize.
Собственно для записи используется класс XMLSerializer, а для настройки выходного формата - класс OutputFormat.
В конструкторе XMLSerializer можно передавать как потоки байтов, так и потоки символов.
В случае потоков символов используемая кодировка должна совпадать с заданной в OutputFormat.
Важно не забыть задать используемую кодировку в OutputFormat - в противном случае русские буквы будут представлены в виде кодов, типа такого: "АБВ" для символов "АБВ".
Пример:
OutputStream os=...;
OutputFormat format=
new OutputFormat(Method.XML,"Windows-1251",true);
XMLSerializer serializer=new XMLSerializer(os,format);
serializer.serialize(doc);
Castor XML
Пакет
Castor предназначен для решения проблем долговременного хранения объектов.
В числе прочего он содержит в себе подсистему Castor XML, которая по сути дела является надстройкой над XML-парсером и позволяет автоматизировать чтение и запись XML-файлов.
Castor XML по умолчанию использует парсер Xerces, поэтому проблемы Xerces перекочёвывают и сюда.
В документации к Castor в примерах используются потоки символов (Reader и Writer), а это может привести к рассогласованности между используемой в потоке кодировки и реальной кодировки XML-файла.
Как уже говорилось выше, чтобы прочитать при помощи Xerces XML-файл в произвольной кодировке нужно, во первых, использовать потоки байтов, а во вторых, установить специальный feature.
К счастью эта возможность предусмотрена в Castor.
Для этого нужно скопировать файл castor.properties (взять его можно из каталога org/exolab/castor в файле castor-0.9.3-xml.jar) в подкаталог lib в JRE, и установить там переменную org.exolab.castor.sax.features.
Пример:
# Comma separated list of SAX 2 features that should be enabled
# for the default parser.
#
#org.exolab.castor.features=
org.exolab.castor.sax.features=
http://apache.org/xml/features/allow-java-encodings
Стоит отметить, что по умолчанию там стоит переменная org.exolab.castor.features, но это, очевидно, опечатка - если посмотреть в исходники, то там анализируется org.exolab.castor.sax.features (это справедливо для Castor версии 0.9.3 от 03.07.2001).
Пример чтения с использованием потоков байтов:
public static Object load(Class cls,
String mappingFile, InputStream is)throws Exception{
Mapping mapping=loadMapping(cls,mappingFile);
Unmarshaller unmarshaller=new Unmarshaller(cls);
unmarshaller.setMapping(mapping);
return unmarshaller.unmarshal(new InputSource(is));
}
Для создания XML-файлов необходимо правильно указать формат для Xerces.
Пример:
public static void save(Object obj, String mappingFile,
OutputStream os, String encoding)throws Exception{
Mapping mapping=loadMapping(obj.getClass(),mappingFile);
try{
XMLSerializer serializer=new XMLSerializer(os,
new OutputFormat(Method.XML,encoding,true));
Marshaller marshaller=new Marshaller(serializer);
marshaller.setMapping(mapping);
marshaller.marshal(obj);
}finally{
os.flush();
}
}
Для загрузки файлов маппинга в этих примерах можно использовать такой код:
private static Mapping loadMapping(Class cls,String mappingFile)
throws Exception{
ClassLoader loader=cls.getClassLoader();
Mapping mapping=new Mapping(loader);
mapping.loadMapping(
new InputSource(loader.getResourceAsStream(
mappingFile)));
return mapping;
}
XSL
Спецификация XSL описывает стандарт на преобразование XML-документов.
Когда при помощи XSL выполняется преобразование из одного XML-документа в другой, особых причин для беспокойства нет - и тот и другой являются Unicode-документами, поэтому нет преобразований из символов в байты и обратно, могущих повлиять на результат.
Другое дело, когда выполняется преобразование из XML в HTML или вообще в текстовый файл.
Формат выходного файла задаётся настройкой тега
xsl:output, в котором можно задать используемую кодировку.
Пример:
<xsl:output encoding="Windows-1251" method="html" indent="yes"/>
Если XSLT-процессор не знает указанной кодировки, то он должен или выдать ошибку или использовать UTF-8 (или UTF-16).
Если формируется HTML, то XSLT-процессор должен добавить тег meta, в котором будет указана реально использованная кодировка:
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
Всё бы хорошо, но некоторые XSLT-процессоры не поддерживают данный тег (по спецификации они и не обязаны).
В частности пакет
Cocoon его не поддерживает, т.к.
по словам разработчиков он противоречит внутренней архитектуре этого пакета.
Вместо этого там поддерживается указание выходного формата при помощи инструкции препроцессора cocoon-format.
Пример вставки этой инструкции в XSL:
<xsl:processing-instruction name="cocoon-format">
type="text/html"
</xsl:processing-instruction>
Таким образом можно динамически менять выходной формат.
Если это не требуется, то можно записать инструкцию и статически (в исходном XML-документе):
<?cocoon-format type="text/html"?>
Собственно используемая кодировка настраивается для каждого формата отдельно в файле cocoon.properties.
Новая версия
Cocoon 2.0 кроме управления кодировками позволяет сделать в плане локализации уже гараздо больше.
Подробности Вы можете узнать на их
сайте.
В случае использования JAXP для генерации выходного потока (пакет javax.xml.transform) кроме использования тега xsl:output можно использовать методы setOutputProperty объекта Transformer.
Пример сохранения документа в нужной кодировке:
TransformerFactory trFactory=TransformerFactory.newInstance();
Transformer transformer=trFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,docPublic);
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,docSystem);
transformer.setOutputProperty(OutputKeys.INDENT,"yes");
transformer.setOutputProperty(OutputKeys.ENCODING,encoding);
OutputStream os=...;
StreamResult result=new StreamResult(os);
transformer.transform(source, result);
Тут есть один подводный камень - реализация Transformer должна поддерживать нужную кодировку.
Xalan из состава JDK 1.4.0_x и 1.4.1_x поддерживает только две русские кодировки - KOI8-R и ISO-8859-5.
Если хочется использовать Windows-1251, то можно воспользоваться механизмом endorsed:
- Создаёте каталог %JAVA_HOME%/jre/lib/endorsed
- Копируете туда jar с пропатченым классом: XalanRusChars.jar
В JDK 1.4.2 Beta включена новая версия Xalan, которая вроде как уже поддерживает кодировку 1251.