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 в моя bean. Възможно ли е това с помощта на анотации на 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

Проблемът (освен допълнителния код) е, че не винаги мога да гарантирам, че датата е зададена преди стойността на времето, но не мога да се сетя за конкретен пример, където това може да даде лоши резултати.

Оценяват се всякакви примери за базирано на анотации решение или подобрения/контра примери за кода по-горе.

Редактиране: Използвах втория отговор, даден от Blaise Doughan, но промених неговия DateAttributeTransformer да бъде по-гъвкав и да не очаква името на полето да съдържа низа "ДАТА". Имената на полетата са взети от анотациите на 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;

}

ElemA

Можем да използваме @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;
}

ElemB

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;

}

DateAttributeTransformer

Трансформаторът на атрибути отговаря за демаршалирането на обекта 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);
        }
    }

}

DateFieldTransformer

Полевите трансформатори са отговорни за сортирането на обекта 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);
    }

}

TimeFieldTransformer

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