JAXB: сопоставление отдельных элементов даты и времени с одним свойством

Я работаю со структурой XML, которая выглядит так:

<ROOT>
    <ELEM_A>
        <A_DATE>20100825</A_DATE>
        <A_TIME>141500</A_TIME>
        <!-- other elements, maybe also other or date/time combinations -->
        <STRING>ABC</STRING>
    <ELEM_A>
    <ELEM_B>
        <B_DATE>20100825</B_DATE>
        <B_TIME>153000</B_TIME>
        <NUM>123</NUM>
        <C_DATE>20100825</C_DATE>
        <C_TIME>154500</C_TIME>
    </ELEM_B>
</ROOT>

И я хочу сопоставить дату и время с одним свойством Date или Calendar в моем компоненте. Возможно ли это с помощью аннотаций jaxb? Похоже, что класс javax.xml.bind.annotation.adapters.XmlAdapter может это сделать, но я должен признать, что не до конца понимаю его javadoc.

В качестве обходного пути можно было бы создать дополнительные установщики для строк даты и времени, которые устанавливают соответствующие значения в свойстве Calendar следующим образом:

private Calendar calendar;

public void setDate(String date) {
    if (calendar == null) {
        calendar = new GregorianCalendar();
    }
    calendar.set(YEAR, Integer.parseIn(date.substring(0, 4)));
    calendar.set(MONTH, Integer.parseIn(date.substring(4, 6))-1);
    calendar.set(DAY_OF_MONTH, Integer.parseIn(date.substring(6, 8)));
}

// Similar code for setTime

Проблема (кроме дополнительного кода) заключается в том, что я не всегда могу гарантировать, что дата будет установлена ​​раньше значения времени, но я не могу придумать конкретного примера, где это могло бы привести к изношенным результатам.

Приветствуются любые примеры решения на основе аннотаций или улучшений/контрпримеров для приведенного выше кода.

Редактировать: я выбрал второй ответ, данный Блейзом Доганом, но изменил свой DateAttributeTransformer чтобы быть более гибким и не ожидать, что имя поля будет содержать строку «DATE». Имена полей взяты из аннотаций XmlWriterTransformer в поле:

@Override
public Object buildAttributeValue(Record record, Object instance, Session session) {
    try {
        String dateString = null;
        String timeString = null;

        String dateFieldName = null;
        String timeFieldName = null;
        // TODO: Proper Exception handling
        try {
            XmlWriteTransformers wts = instance.getClass().getDeclaredField(mapping.getAttributeName()).getAnnotation(XmlWriteTransformers.class);
            for (XmlWriteTransformer wt : wts.value()) {
                String fieldName = wt.xpath();
                if (wt.transformerClass() == DateFieldTransformer.class) {
                    dateFieldName = fieldName;
                } else {
                    timeFieldName = fieldName;
                }
            }
        } catch (NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }

        for(DatabaseField field : mapping.getFields()) {
            XMLField xfield = (XMLField)field;
            if(xfield.getXPath().equals(dateFieldName)) {
                dateString = (String) record.get(field);
            } else {
                timeString = (String) record.get(field);
            }
        }
        return yyyyMMddHHmmss.parseObject(dateString + timeString);
    } catch(ParseException e) {
        throw new RuntimeException(e);
    }
}

person Jörn Horstmann    schedule 25.08.2010    source источник


Ответы (2)


Вместо использования JAXB RI (Metro) вы можете использовать реализацию MOXy JAXB (я технический руководитель). У него есть некоторые расширения, которые упростят сопоставление этого сценария.

jaxb.properties

Чтобы использовать MOXy в качестве реализации JAXB, вам нужно добавить файл с именем jaxb.properties в тот же пакет, что и классы вашей модели, со следующей записью:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Корень

import javax.xml.bind.annotation.*;

@XmlRootElement(name="ROOT")
@XmlAccessorType(XmlAccessType.FIELD)
public class Root {

    @XmlElement(name="ELEM_A")
    private ElemA elemA;

    @XmlElement(name="ELEM_B")
    private ElemB elemB;

}

ЭлемА

Мы можем использовать @XmlTransformation. По своей концепции он похож на XmlAdapter, но его проще использовать для сопоставлений.

import java.util.Date;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;

import org.eclipse.persistence.oxm.annotations.XmlReadTransformer;
import org.eclipse.persistence.oxm.annotations.XmlTransformation;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformer;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformers;

@XmlAccessorType(XmlAccessType.FIELD)
public class ElemA {

    @XmlTransformation
    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xpath="A_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xpath="A_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    public Date aDate;

    @XmlElement(name="STRING")
    private String string;
}

ЭлемБ

import java.util.Date;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;

import org.eclipse.persistence.oxm.annotations.XmlReadTransformer;
import org.eclipse.persistence.oxm.annotations.XmlTransformation;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformer;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformers;

@XmlAccessorType(XmlAccessType.FIELD)
public class ElemB {

    @XmlTransformation
    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xpath="B_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xpath="B_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date bDate;

    @XmlElement(name="NUM")
    private int num;

    @XmlTransformation
    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xpath="C_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xpath="C_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date cDate;

}

Трансформатор DateAttribute

Преобразователь атрибута отвечает за деупорядочение объекта Date.

import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.AttributeTransformer;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;

public class DateAttributeTransformer implements AttributeTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat yyyyMMddHHmmss = new SimpleDateFormat("yyyyMMddHHmmss");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildAttributeValue(Record record, Object instance, Session session) {
        try {
            String dateString = null;
            String timeString = null;

            for(DatabaseField field : mapping.getFields()) {
                if(field.getName().contains("DATE")) {
                    dateString = (String) record.get(field);
                } else {
                    timeString = (String) record.get(field);
                }
            }
            return yyyyMMddHHmmss.parseObject(dateString + timeString);
        } catch(ParseException e) {
            throw new RuntimeException(e);
        }
    }

}

Трансформатор DateField

Преобразователи поля отвечают за упорядочение объекта Date.

import java.text.SimpleDateFormat;
import java.util.Date;

import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.FieldTransformer;
import org.eclipse.persistence.sessions.Session;

public class DateFieldTransformer implements FieldTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMdd");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildFieldValue(Object instance, String xPath, Session session) {
        Date date = (Date) mapping.getAttributeValueFromObject(instance);
        return yyyyMMdd.format(date);
    }

}

Трансформер TimeField

import java.text.SimpleDateFormat;
import java.util.Date;

import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.FieldTransformer;
import org.eclipse.persistence.sessions.Session;

public class TimeFieldTransformer implements FieldTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat HHmmss = new SimpleDateFormat("HHmmss");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildFieldValue(Object instance, String xPath, Session session) {
        Date date = (Date) mapping.getAttributeValueFromObject(instance);
        return HHmmss.format(date);
    }

}

Пример программы

import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);
        System.out.println(jc);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum41/input.xml");
        Root root = (Root) unmarshaller.unmarshal(xml);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }

}

XML-документ

<ROOT> 
    <ELEM_A> 
        <A_DATE>20100825</A_DATE> 
        <A_TIME>141500</A_TIME>
        <!-- other elements, maybe also other or date/time combinations --> 
        <STRING>ABC</STRING> 
    </ELEM_A> 
    <ELEM_B> 
        <B_DATE>20100825</B_DATE> 
        <B_TIME>153000</B_TIME>
        <NUM>123</NUM> 
        <C_DATE>20100825</C_DATE> 
        <C_TIME>154500</C_TIME>
    </ELEM_B> 
</ROOT> 

Для показанного выше кода требуется EclipseLink 2.2, который в настоящее время находится в разработке. Ночные сборки доступны здесь:

Текущая выпущенная версия EclipseLink 2.1 поддерживает вышеуказанное, но с немного другой конфигурацией. Мы можем обсудить соответствующую настройку, если вы заинтересованы в изучении этого варианта.

person bdoughan    schedule 25.08.2010
comment
Еще раз спасибо за развернутый ответ, я попробую EclipseLink MOXy. - person Jörn Horstmann; 26.08.2010
comment
Некоторые дополнительные сведения доступны в моем блоге: bdoughan.blogspot.com/2010 /08/ - person bdoughan; 26.08.2010
comment
Нашел время, чтобы попробовать это, и это отлично работает в моих тестах. Я внес некоторые изменения в DateAttributeTransformer, чтобы заменить проверку contains чем-то столь же хакерским, но более гибким. Я добавлю измененный источник к вопросу. - person Jörn Horstmann; 01.09.2010

XmlAdapter - правильный подход:

Класс со свойством даты

import java.util.Date;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement(name="ROOT")
public class Root {

    private Date date;

    @XmlElement(name="ELEM")
    @XmlJavaTypeAdapter(DateAdapter.class)
    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

}

Реализация XmlAdapter

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class DateAdapter extends XmlAdapter<AdaptedDate, Date> {

    private SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMdd");
    private SimpleDateFormat HHmmss = new SimpleDateFormat("HHmmss");
    private SimpleDateFormat yyyyMMddHHmmss = new SimpleDateFormat("yyyyMMddHHmmss");

    @Override
    public Date unmarshal(AdaptedDate v) throws Exception {
        String dateString = v.getDate() + v.getTime();
        return yyyyMMddHHmmss.parse(dateString);
    }

    @Override
    public AdaptedDate marshal(Date v) throws Exception {
        AdaptedDate adaptedDate = new AdaptedDate();
        adaptedDate.setDate(yyyyMMdd.format(v));
        adaptedDate.setTime(HHmmss.format(v));
        return adaptedDate;
    }

}

Адаптированный объект Date

import javax.xml.bind.annotation.XmlElement;

public class AdaptedDate {

    private String date;
    private String time;

    @XmlElement(name="DATE")
    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @XmlElement(name="TIME")
    public String getTime() {
        return time;
    }

    public void setTime(String time) {
        this.time = time;
    }

}

Пример программы

import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        File xml = new File("input.xml");
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Root root = (Root) unmarshaller.unmarshal(xml);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }
}

XML-документ

<?xml version="1.0" encoding="UTF-8"?>
<ROOT>
    <ELEM>
        <DATE>20100825</DATE>
        <TIME>141500</TIME>
    </ELEM>
</ROOT> 

Для получения дополнительной информации см.:

person bdoughan    schedule 25.08.2010
comment
Спасибо, отличный пример XmlAdapter, намного понятнее, чем пример HashMap из javadoc. Мой пример xml был немного упрощенным, я действительно могу иметь другие элементы и другие комбинации даты/времени в этом элементе. Можно ли адаптировать два элемента даты/времени, если они не заключены в другой элемент? Я обновлю вопрос более полным примером. - person Jörn Horstmann; 25.08.2010
comment
Я отправил второй ответ, в котором описывается, как это можно сделать с помощью расширений MOXy JAXB stackoverflow.com/questions/3565621/. - person bdoughan; 25.08.2010