Как сгенерировать блок CDATA с помощью JAXB?
Я использую JAXB для сериализации моих данных в XML. Код класса прост, как указано ниже. Я хочу создать XML, который содержит блоки CDATA для значения некоторых аргументов. Например, текущий код производит этот XML:
<command>
<args>
<arg name="test_id">1234</arg>
<arg name="source"><html>EMAIL</html></arg>
</args>
</command>
Я хочу обернуть "исходный" аргумент в CDATA так, чтобы он выглядел следующим образом:
<command>
<args>
<arg name="test_id">1234</arg>
<arg name="source"><[![CDATA[<html>EMAIL</html>]]></arg>
</args>
</command>
Как я могу добиться этого в приведенном ниже коде?
@XmlRootElement(name="command")
public class Command {
@XmlElementWrapper(name="args")
protected List<Arg> arg;
}
@XmlRootElement(name="arg")
public class Arg {
@XmlAttribute
public String name;
@XmlValue
public String value;
public Arg() {};
static Arg make(final String name, final String value) {
Arg a = new Arg();
a.name=name; a.value=value;
return a; }
}
10 ответов
Примечание: я являюсь лидером EclipseLink JAXB (MOXy) и являюсь членом экспертной группы JAXB (JSR-222).
Если вы используете MOXy в качестве поставщика JAXB, вы можете использовать @XmlCDATA
расширение:
package blog.cdata;
import javax.xml.bind.annotation.XmlRootElement;
import org.eclipse.persistence.oxm.annotations.XmlCDATA;
@XmlRootElement(name="c")
public class Customer {
private String bio;
@XmlCDATA
public void setBio(String bio) {
this.bio = bio;
}
public String getBio() {
return bio;
}
}
Для дополнительной информации
Используйте JAXB Marshaller#marshal(ContentHandler)
маршал в ContentHandler
объект. Просто переопределить characters
метод в реализации ContentHandler, которую вы используете (например, JDOM SAXHandler
Apache's XMLSerializer
, так далее):
public class CDataContentHandler extends (SAXHandler|XMLSerializer|Other...) {
// see http://www.w3.org/TR/xml/#syntax
private static final Pattern XML_CHARS = Pattern.compile("[<>&]");
public void characters(char[] ch, int start, int length) throws SAXException {
boolean useCData = XML_CHARS.matcher(new String(ch,start,length)).find();
if (useCData) super.startCDATA();
super.characters(ch, start, length);
if (useCData) super.endCDATA();
}
}
Это намного лучше, чем при использовании XMLSerializer.setCDataElements(...)
метод, потому что вам не нужно жестко кодировать любой список элементов. Он автоматически выводит блоки CDATA только тогда, когда они необходимы.
Обзор решения:
- Ответ fred - это просто обходной путь, который завершится неудачно при проверке содержимого, когда Marshaller связан со схемой, потому что вы изменяете только строковый литерал и не создаете разделы CDATA. Поэтому, если вы переписываете только строку из foo в , длина строки распознается Xerces с 15 вместо 3.
- Решение MOXy зависит от конкретной реализации и не работает только с классами JDK.
- Решение с помощью getSerializer ссылается на устаревший класс XMLSerializer.
- Решение LSSerializer - это просто боль.
Я изменил решение a2ndrade с помощью реализации XMLStreamWriter. Это решение работает очень хорошо.
XMLOutputFactory xof = XMLOutputFactory.newInstance();
XMLStreamWriter streamWriter = xof.createXMLStreamWriter( System.out );
CDataXMLStreamWriter cdataStreamWriter = new CDataXMLStreamWriter( streamWriter );
marshaller.marshal( jaxbElement, cdataStreamWriter );
cdataStreamWriter.flush();
cdataStreamWriter.close();
Это реализация CDataXMLStreamWriter. Класс делегата просто делегирует все вызовы методов данной реализации XMLStreamWriter.
import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
/**
* Implementation which is able to decide to use a CDATA section for a string.
*/
public class CDataXMLStreamWriter extends DelegatingXMLStreamWriter
{
private static final Pattern XML_CHARS = Pattern.compile( "[&<>]" );
public CDataXMLStreamWriter( XMLStreamWriter del )
{
super( del );
}
@Override
public void writeCharacters( String text ) throws XMLStreamException
{
boolean useCData = XML_CHARS.matcher( text ).find();
if( useCData )
{
super.writeCData( text );
}
else
{
super.writeCharacters( text );
}
}
}
Вот пример кода, на который ссылается сайт, упомянутый выше:
import java.io.File;
import java.io.StringWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
public class JaxbCDATASample {
public static void main(String[] args) throws Exception {
// unmarshal a doc
JAXBContext jc = JAXBContext.newInstance("...");
Unmarshaller u = jc.createUnmarshaller();
Object o = u.unmarshal(...);
// create a JAXB marshaller
Marshaller m = jc.createMarshaller();
// get an Apache XMLSerializer configured to generate CDATA
XMLSerializer serializer = getXMLSerializer();
// marshal using the Apache XMLSerializer
m.marshal(o, serializer.asContentHandler());
}
private static XMLSerializer getXMLSerializer() {
// configure an OutputFormat to handle CDATA
OutputFormat of = new OutputFormat();
// specify which of your elements you want to be handled as CDATA.
// The use of the '^' between the namespaceURI and the localname
// seems to be an implementation detail of the xerces code.
// When processing xml that doesn't use namespaces, simply omit the
// namespace prefix as shown in the third CDataElement below.
of.setCDataElements(
new String[] { "ns1^foo", // <ns1:foo>
"ns2^bar", // <ns2:bar>
"^baz" }); // <baz>
// set any other options you'd like
of.setPreserveSpace(true);
of.setIndenting(true);
// create the serializer
XMLSerializer serializer = new XMLSerializer(of);
serializer.setOutputByteStream(System.out);
return serializer;
}
}
По тем же причинам, что и Майкл Эрнст, я не был так рад большинству ответов здесь. Я не мог использовать его решение, так как мое требование состояло в том, чтобы поместить теги CDATA в определенный набор полей - как в решении OutputFormat от raiglstorfer.
Мое решение состоит в том, чтобы выполнить маршализацию документа DOM, а затем выполнить нулевое XSL-преобразование для вывода. Трансформеры позволяют вам задавать, какие элементы будут заключены в теги CDATA.
Document document = ...
jaxbMarshaller.marshal(jaxbObject, document);
Transformer nullTransformer = TransformerFactory.newInstance().newTransformer();
nullTransformer.setOutputProperty(OutputKeys.INDENT, "yes");
nullTransformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "myElement {myNamespace}myOtherElement");
nullTransformer.transform(new DOMSource(document), new StreamResult(writer/stream));
Дополнительная информация здесь: http://javacoalface.blogspot.co.uk/2012/09/outputting-cdata-sections-with-jaxb.html
Следующий простой метод добавляет поддержку CDATA в JAX-B, который изначально не поддерживает CDATA:
- объявить пользовательский простой тип расширяющей строки CDataString для определения полей, которые должны обрабатываться через CDATA
- Создайте собственный CDataAdapter, который анализирует и печатает содержимое в CDataString
- используйте привязки JAXB для связи CDataString и вашего CDataAdapter. CdataAdapter будет добавлять / удалять CdataStrings во время Marshall/Unmarshall
- Объявите пользовательский обработчик экранирования символа, который не экранирует символ при печати строк CDATA, и установите его как Marshaller CharacterEscapeEncoder
В общем, любой элемент CDataString будет инкапсулирован во время Маршалла. В нерабочее время, оно будет автоматически удалено.
Дополнение @a2ndrade
ответ.
Я нахожу один класс для расширения в JDK 8. Но отметил, что класс находится в com.sun
пакет. Вы можете сделать одну копию кода на случай, если этот класс может быть удален в будущем JDK.
public class CDataContentHandler extends com.sun.xml.internal.txw2.output.XMLWriter {
public CDataContentHandler(Writer writer, String encoding) throws IOException {
super(writer, encoding);
}
// see http://www.w3.org/TR/xml/#syntax
private static final Pattern XML_CHARS = Pattern.compile("[<>&]");
public void characters(char[] ch, int start, int length) throws SAXException {
boolean useCData = XML_CHARS.matcher(new String(ch, start, length)).find();
if (useCData) {
super.startCDATA();
}
super.characters(ch, start, length);
if (useCData) {
super.endCDATA();
}
}
}
Как пользоваться:
JAXBContext jaxbContext = JAXBContext.newInstance(...class);
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter sw = new StringWriter();
CDataContentHandler cdataHandler = new CDataContentHandler(sw,"utf-8");
marshaller.marshal(gu, cdataHandler);
System.out.println(sw.toString());
Пример результата:
<?xml version="1.0" encoding="utf-8"?>
<genericUser>
<password><![CDATA[dskfj>><<]]></password>
<username>UNKNOWN::UNKNOWN</username>
<properties>
<prop2>v2</prop2>
<prop1><![CDATA[v1><]]></prop1>
</properties>
<timestamp/>
<uuid>cb8cbc487ee542ec83e934e7702b9d26</uuid>
</genericUser>
Начиная с версии Xerxes-J 2.9, XMLSerializer устарел. Предлагаем заменить его на DOM Level 3 LSSerializer или JAXP Transformation API для XML. Кто-нибудь пробовал подход?
Просто предупреждение: в соответствии с документацией javax.xml.transform.Transformer.setOutputProperty(...) вы должны использовать синтаксис квалифицированных имен при указании элемента из другого пространства имен. Согласно JavaDoc (Java 1.6 rt.jar):
"(...) Например, если URI и локальное имя были получены из элемента, определенного с, то квалифицированное имя будет" { http://xyz.foo.com/yada/baz.html} foo. Обратите внимание, что префикс не используется. "
Ну, это не работает - реализующий класс из Java 1.6 rt.jar, что означает com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl, только тогда правильно интерпретирует элементы, принадлежащие другому пространству имен, когда они объявлены как " http://xyz.foo.com/yada/baz.html:foo", потому что в реализации кто-то анализирует его в поисках последнего двоеточия. Так что вместо того, чтобы ссылаться:
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{http://xyz.foo.com/yada/baz.html}foo")
который должен работать в соответствии с JavaDoc, но в итоге будет проанализирован как "http" и "//xyz.foo.com/yada/baz.html", вы должны вызвать
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "http://xyz.foo.com/yada/baz.html:foo")
По крайней мере, в Java 1.6.
Следующий код не позволит кодировать элементы CDATA:
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
DataWriter dataWriter = new DataWriter(printWriter, "UTF-8", new CharacterEscapeHandler() {
@Override
public void escape(char[] buf, int start, int len, boolean b, Writer out) throws IOException {
out.write(buf, start, len);
}
});
marshaller.marshal(data, dataWriter);
System.out.println(stringWriter.toString());
Это также будет держать UTF-8
как ваша кодировка.