четверг, 11 февраля 2010 г.

Java: Read escaped quote as escaped quote from xml

Всё началось с того, что на проекте мы используем написанный нами фреймворк для автоматического тестирования. Многое в нём завязано на xml. В частности очень важная задача сравнение двух xml, "сравнитель" оброс большим количеством всевозможных плюшек. Вот понадобилось ещё одну реализовать. Подробно о ней написала моя коллега: StackoverFlow:Read escaped quote as escaped quote from xml. Ничего дельного ей не ответили до её ухода в отпуск. Задача мне показалась интересной, поэтому решил побаловаться с ней. Вкратце, проблема состоит в том, что когда DOM парсер строит дерево, к значениям элементов применяет unescaping. Нам бы хотелось, чтобы он так не делал, потому что нам надо сравнить два DOM дерева построенных по двум xml файлам, и убедиться именно в их идентичности, что эскейпинг проведен правильно.
На предыдущем проекте пришлось много сражаться с особенностями open source библиотек, которые мы использовали. Причём на форумах тоже было очень молчаливо, а задания делать надо было. Тогда я и заразился любовью в копании в исходниках open source библиотеки, чтобы решить свою проблему, очевидный и очень эффективный метод :)


Было найдено, что DocmentBuilderImpl использует объект интерфейса XMLParserConfiguration, для построения дерева объектов. Вызывая у него метод parse. И заменить его на свою реализацию не так уж сложно, просто определить системное свойство.
  1. System.setProperty("org.apache.xerces.xni.parser.XMLParserConfiguration""a.MyConfig");  
Вообще, это очень удобный способ по моему. Я его тоже использую в своих фабриках объектов, позволяет не только в коде настраивать реализацию класса, но и при запуске приложения. Про IOC я тоже знаю, но для небольшой библиотечки тащить даже Guice очень странно по моему.
Теперь осталось только написать этот MyConfig. Писать с нуля лениво и несколько глупо. Поэтому в качестве базиса я выбрал класс NonValidatingConfiguration. Да и тут очередная абстракция :) он использует объект интерфейса XMLDocumentScanner для своего рода сканера-sax-парсера. Его мы только и заменим в этом классе конфига, путем переопределения метода configurePipeline.
  1. public class MyConfig extends NonValidatingConfiguration {  
  2.   
  3.     private MyScanner myScanner;  
  4.   
  5.     @Override  
  6.     @SuppressWarnings("unchecked")  
  7.     protected void configurePipeline() {  
  8.         if (myScanner == null) {  
  9.             myScanner = new MyScanner();  
  10.             addComponent((XMLComponent) myScanner);  
  11.         }  
  12.         super.fProperties.put(DOCUMENT_SCANNER, myScanner);  
  13.         super.fScanner = myScanner;  
  14.         super.fScanner.setDocumentHandler(this.fDocumentHandler);  
  15.         super.fLastComponent = fScanner;  
  16.     }  
  17. }  
Ох, теперь придется реализовать свой сканер, но тоже не с нуля, а воспользуемся классом XMLDocumentScannerImpl. Тут нам придется переопределить метод scanEntityReference, который вызывается, когда сканер встречает эскейпинг набор. В нём вычисляется имя эскейпнутого символа и заменяется на сам символ. Билдер об этом узнаёт, когда мы вызовём соответствующий метод у DocumentHandler поля сканера. Поэтому мы вместо того, чтобы хендлеру передавать правильный символ, передадим эскейп набор.
  1. private static class MyScanner extends XMLDocumentScannerImpl {  
  2.     @Override  
  3.     protected void scanEntityReference() throws IOException, XNIException {  
  4.         // name  
  5.         String name = super.fEntityScanner.scanName();  
  6.         if (name == null) {  
  7.            reportFatalError("NameRequiredInReference"null);  
  8.            return;  
  9.         }  
  10.   
  11.         super.fDocumentHandler.characters(new XMLString(("&" + name + ";")  
  12.              .toCharArray(), 0, name.length() + 2), null);  
  13.   
  14.         // end  
  15.         if (!super.fEntityScanner.skipChar(';')) {  
  16.              reportFatalError("SemicolonRequiredInReference",  
  17.                      new Object[] { name });  
  18.         }  
  19.         fMarkupDepth--;  
  20.     }  
  21. }  
В целом ничего сложного. Этот же код можно найти в моём ответе на стековерфлоу.
Ну и в качестве небольшого бонуса расскажу продолжение истории. Есть у нас DOM дерево объектов и мы его решим сохранить в xml файл, как у нас это делается через класс Transformer.
  1. Document doc = ....  
  2. TransformerFactory tFactory = TransformerFactory.newInstance();  
  3. Transformer transformer = tFactory.newTransformer();  
  4. StringWriter sw = new StringWriter();  
  5. transformer.transform(new DOMSource(doc), new StreamResult(sw));  
То узнаем интересный факт, никто в java комьюнити не умеет писать нормальных эскейперов xml. Ни apache.common.lang, ни saxon. Вообщем проблема в том, что если в текстовых нодах был эскейпнутый текст, то он дважды эскейпниться, например &квот; замениться на &амп;квот; (Пришлось написать русским буквами, потому что блогспот тоже из себя умного строит). Бред какой-то. Вообщем, чтобы заставить Трансформер меньше думать и сохранять как есть, в метод newTransformer необходимо передать xslt преобразование.
  1. <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">  
  2.     <xsl:output method="xml">  
  3.   
  4.     <xsl:template match="@*|node()">  
  5.         <xsl:choose>  
  6.             <xsl:when test="self::text()">  
  7.                 <xsl:value-of disable-output-escaping="yes" select=".">  
  8.             </xsl:value-of></xsl:when>  
  9.             <xsl:otherwise>  
  10.                 <xsl:copy>  
  11.                     <xsl:apply-templates select="@*|node()"></xsl:apply-templates>  
  12.                 </xsl:copy>  
  13.             </xsl:otherwise>  
  14.         </xsl:choose>  
  15.     </xsl:template>  
  16. </xsl:output></xsl:stylesheet>  

Комментариев нет:

Отправить комментарий