Всем добрый день, сегодня будем говорить о JAXB. Данная технология преобразования объектов в xml файл в java существует достаточно давно и не представляет особой сложности, но как и везде, существует ряд нюансов. Они появляются когда вы хотите сделать, что «нестандартное». В моем случае, требовалось заключить некоторые поля в блок CDATA, а содержимое оставить без изменений.

Давайте расмотрим пример. В его роли будет выступать класс Person. Небольшая ремарка — примеры будут на модном, молодежном kotlin‘e 😉

@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
class Parent {
    @XmlJavaTypeAdapter(CDATAAdapter::class)
    @XmlElement(name = "name")
    var name: String = ""
    @XmlElementWrapper(name = "children")
    @XmlElements(
            XmlElement(name = "boy", type = Boy::class),
            XmlElement(name = "girl", type = Girl::class))
    var children: Collection<Child> = emptyList()
}

open class Child {
    var name: String = ""
}

class Girl : Child()
class Boy : Child()

Поле name будет заключено в CDATA. Для этого мы добавили аннотацию java type adapter c собственноручно написанным классом CDATAAdapter. Взглянем на его реализацию:

class CDATAAdapter : XmlAdapter<String, String>() {
    override fun unmarshal(value: String?): String {
        return if (value == null || value.isBlank())
            return ""
        else
            value.trim().removePrefix("&lt![CDATA[").removeSuffix("]]&gt")
    }

    override fun marshal(value: String?): String {
        return if (value == null || value.isBlank())
            ""
        else
            "&lt![CDATA[$value]]&gt"
    }
}

Можем заметить, что ничего хитрого здесь нет — простые манипуляции со строками. Теперь требует научить сериализатор или про простому маршаллер, не изменять строки которые мы уже модифицировали CDATAAdapter‘ом .

private val MARSHALLER: Marshaller by lazy {
    val context = JAXBContext.newInstance(Parent::class.java)
    val marshaller = context.createMarshaller()
    marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.toString())
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
    marshaller.setProperty(com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler::class.java.canonicalName,
            CharacterEscapeHandler { chars, start, end, isAttribute, writer ->
                val value = String(chars, start, end).trim()
                if (value.startsWith("&lt![CDATA[") && value.endsWith("]]&gt"))
                    writer.write(chars, start, end)
                else
                    MinimumEscapeHandler.theInstance.escape(chars, start, end, isAttribute, writer)
            })

    marshaller
}

Если отбросить детали связанные кодировкой и красивым форматированием, то остается параметр выставляющий класс для обработки символов. Здесь мы выставляем свой кастомный обработчик.

Что он делает? Его логика проста. Он определяет — если строка ранее была изменена java type адаптером, то исходное значение оставляет без изменений, иначе же — делегирует обработку этой строки встроенному обработчику.

Теперь попробуем запустить данный код и посмотреть на полученный результат:

val parent = Parent().apply {
        this.name = "John"
        this.children = arrayListOf(
                Girl().apply {
                    this.name = "Margaret"
                },
                Boy().apply {
                    this.name = "Steve"
                }
        )
    }
 
MARSHALLER.marshal(parent, System.out)

После выполнения этого кода должна вывестись на экран xml следующего содержания:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<person>
    <name><![CDATA[John]]></name>
    <children>
        <girl>
            <name>Margarette</name>
        </girl>
        <boy>
            <name>Steve</name>
        </boy>
    </children>
</person>