Поиск по этому блогу

четверг, 6 февраля 2014 г.

Преобразование данных при маршаллизации и демаршаллизации


Иногда нам необходимо, чтобы в java классах тип полей отличался от типа полей описанных в xsd схеме. Например, в xsd мы имеем такое описание:
<xs:element name="date" type="xs:string"/>
Этот элемент имеет тип String, хотя реально туда передается отформатированная дата:
<date>06.02.2014 13:02:50</date>
В результате генерации классов java мы получим поле date типа String:
@XmlElement(name="date")
private String date;

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

public String getDate(){
    return date;
}
Но что, если нам необходимо чтобы поле date имело тип java.utl.Date?

Для решения этого класса задач в java есть аннотация @XmlJavaTypeAdapter. Эта аннотация может быть применена для полей, методов, параметров, классов (интерфейсов, пречислений) и пакетов. У данной аннотации есть 2 атрибута:
  • value - указывает на класс адаптера который должен раcширять XmlAdapter. В нем и будет реализована лоигика конвертации типов. Этот атрибут является обязательным;
  • type - является обязательным, если аннотация применена на пакет. Указывает на класс который мы хотим преобразовать. Например если мы хотим преобразовать все поля типа XMLGregorianCalendar в поля типа Date, то в качестве значения атрибута type, мы должны передать туда XMLGregorianCalendar.class;
Рассмотрим аннотации на уровне поля, класса и пакета. Предположим у нас есть исходный xml:
<?xml version="1.0" encoding="UTF-8"?>

<film>
    <title>The Great Dictator</title>
    <description>
        Dictator Adenoid Hynkel tries to expand his empire while a 
        poor Jewish barber tries to avoid persecution from Hynkel's regime.
    </description>
    <releaseDate>07.03.1941</releaseDate>
    <actors>
        <actor>
            <name>Charles</name>
            <surname>Chaplin</surname>
            <character>
                Hynkel - Dictator of Tomania / A Jewish Barber
            </character>
        </actor>
        <actor>
            <name>Jack</name>
            <surname>Oakie</surname>
            <character>Napaloni - Dictator of Bacteria</character>
        </actor>
         <actor>
            <name>Reginald</name>
            <surname>Gardiner</surname>
            <character>Schultz</character>
        </actor>
         <actor>
            <name>Henry</name>
            <surname>Daniell</surname>
            <character>Garbitsch</character>
        </actor>
         <actor>
            <name>Billy</name>
            <surname>Gilbert</surname>
            <character>Herring</character>
        </actor>
    </actors>
</film>
И набор классов с xml аннотациями:
Actor.class
package com.vinichenkosa.javaxmladapter.models;

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

@XmlAccessorType(XmlAccessType.NONE)
public class Actor {
    @XmlElement(name = "name")
    private String name;
    @XmlElement(name = "surname")
    private String surname;
    @XmlElement(name = "character")
    private String character;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public String getCharacter() {
        return character;
    }

    public void setCharacter(String character) {
        this.character = character;
    }

    @Override
    public String toString() {
        return "Actor{" + "name=" + name + ", surname=" + surname + 
        ", character=" + character + '}';
    }
}
Film.class
package com.vinichenkosa.javaxmladapter.models;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="film")
@XmlAccessorType(XmlAccessType.NONE)
public class Film {

    @XmlElement(name = "title")
    private String title;
    @XmlElement(name = "description")
    private String description;
    @XmlElement(name = "releaseDate")
    private String releaseDate;
    @XmlElementWrapper(name="actors")
    @XmlElement(name="actor")
    private List<Actor> actors;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getReleaseDate() {
        return releaseDate;
    }

    public void setReleaseDate(String releaseDate) {
        this.releaseDate = releaseDate;
    }

    public List<Actor> getActors() {
        if(actors == null){
            actors = new ArrayList();
        }
        return actors;
    }

    @Override
    public String toString() {
        return "Film{" + "title=" + title + ", description=" + 
        description + ", releaseDate=" + releaseDate + ", actors=" +
        actors.toString() + '}';
    } 
}
XmlUtils.class
package com.vinichenkosa.javaxmladapter.utils;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

public class XmlUtils {

    @SuppressWarnings("unchecked")
    public static <T> T unmarshall(InputStream xml, Class<T> cl) 
                                    throws JAXBException{
        JAXBContext jaxbCtx = JAXBContext.newInstance(cl);
        Unmarshaller unmarshaller = jaxbCtx.createUnmarshaller();
        return (T) unmarshaller.unmarshal(xml);
    }
}
App.class
public class App 
{
    public static void main( String[] args ) throws JAXBException
    {
        InputStream xml = App.class.getResourceAsStream("/films.xml");
        Film film = XmlUtils.unmarshall(xml, Film.class);
        System.out.println(film.toString());
    }
}
Если мы запустить сейчас приложение, то мы увидим:
Film{
    title=The Great Dictator, 
    description=Dictator Adenoid Hynkel tries to expand his ...,
    releaseDate=07.03.1941, 
    actors=[
        Actor{name=Charles, surname=Chaplin, character=Hynkel ...},
        Actor{name=Jack, surname=Oakie, character=Napaloni ...},
        Actor{name=Reginald, surname=Gardiner, character=Schultz},
        Actor{name=Henry, surname=Daniell, character=Garbitsch},
        Actor{name=Billy, surname=Gilbert, character=Herring}
    ]
}
Все хорошо, но что если мы хотим чтобы поле releaseDate имело тип java.util.Date? Без проблем. Создадим адаптер, который будет преобразовывать строку определенного формата в дату:
StringToDateAdapter.class:
package com.vinichenkosa.javaxmladapter.adapters;

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

public class StringToDateAdapter extends XmlAdapter<String, Date> {

    private final SimpleDateFormat dateFormatter;

    public StringToDateAdapter() {
        this.dateFormatter = new SimpleDateFormat("dd.MM.yyyy");
    }

    @Override
    public Date unmarshal(String dateAsString) throws Exception {

        return dateFormatter.parse(dateAsString);
    }

    @Override
    public String marshal(Date date) throws Exception {
        return dateFormatter.format(date);
    }
}
Добавим аннотацию @XmlJavaTypeAdapter к полю releaseDate и изменим тип поля на Date:
@XmlJavaTypeAdapter(StringToDateAdapter.class)
@XmlElement(name = "releaseDate")
private Date releaseDate;
Запустим приложение:
Film{
    title=The Great Dictator, 
    description=Dictator Adenoid Hynkel tries to expand his ...,
    release date=Fri Mar 07 00:00:00 MSK 1941
    actors=[
        Actor{name=Charles, surname=Chaplin, character=Hynkel ...},
        Actor{name=Jack, surname=Oakie, character=Napaloni ...},
        Actor{name=Reginald, surname=Gardiner, character=Schultz},
        Actor{name=Henry, surname=Daniell, character=Garbitsch},
        Actor{name=Billy, surname=Gilbert, character=Herring}
    ]
}
Из вывода видно, что теперь у нас полноценная дата вместо строки. Круто. Но что если у нас много полей, которые надо преобразовать к определенному типу?
Давайте посмотрим что делает адаптер на уровне пакета. Наш адапетр будет переводить все строки в верхний регистр. На самом деле тип тут не меняется, но это не сложно сделать по аналогии с первым примером. Создадим файл адаптера и package-info.java:
UppercaseAdapter.java:
package com.vinichenkosa.javaxmladapter.adapters;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class UppercaseAdapter extends XmlAdapter<String, String>{

    @Override
    public String unmarshal(String v) throws Exception {
        return v.toUpperCase();
    }

    @Override
    public String marshal(String v) throws Exception {
        return v;
    }
}
package-info.java

@XmlJavaTypeAdapter(
    value = UppercaseAdapter.class, type = String.class
)
package com.vinichenkosa.javaxmladapter.models;

import com.vinichenkosa.javaxmladapter.adapters.UppercaseAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
Как видно мы воспользовались аннотацией @XmlJavaTypeAdapter для нашего пакета. Но что если надо определить несколько адаптеров для пакета? Для этого можно воспользоваться аннотацией @XmlJavaTypeAdapters(@XmlJavaTypeAdapter()[]), которая позволяет объединить несколько адапетров. Например, мы можем перенести наш адаптер StringToDate с поля releaseDate на уровень пакета и убедиться, что он по прежнему работает:
@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(
        value = UppercaseAdapter.class, 
        type = String.class
    ),
    @XmlJavaTypeAdapter(
        value = StringToDateAdapter.class, 
        type = Date.class
    )
})
package com.vinichenkosa.javaxmladapter.models;

import com.vinichenkosa.javaxmladapter.adapters.StringToDateAdapter;
import com.vinichenkosa.javaxmladapter.adapters.UppercaseAdapter;
import java.util.Date;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
Запустим программу:
Film{
    title=THE GREAT DICTATOR, 
    description=DICTATOR ADENOID HYNKEL TRIES TO EXPAND HIS ..., 
    releaseDate=Fri Mar 07 00:00:00 MSK 1941, 
    actors=[
        Actor{name=CHARLES, surname=CHAPLIN, character=HYNKEL ...},
        Actor{name=JACK, surname=OAKIE, character=NAPALONI ...},
        Actor{name=REGINALD, surname=GARDINER, character=SCHULTZ}, 
        Actor{name=HENRY, surname=DANIELL, character=GARBITSCH}, 
        Actor{name=BILLY, surname=GILBERT, character=HERRING}
    ]
}
Как видно, значение всех полей имевщих тип String, было приведено к верхнему регистру, а значение поля releaseDate не изменилось.
Добавим данные о составе команды, принимавшей участие в съемках фильма:

<film> 

    … 

        <crew> 

            <member>Carter DeHaven</member> 

            <member>Meredith Willson</member> 

            <member>Karl Struss</member> 

            <member>>Roland Totheroh</member> 

            <member>Willard Nico</member> 

            <member>>Harold Rice</member> 

        </crew> 

    </film>

Как видите это просо набор строк, но нам надо преобразовать их в объекты типа Member. Для этого создадим класс Member с адапетром уровня класса. Адаптер будет вызываться для всех полей имеющих тип Member:
package com.vinichenkosa.javaxmladapter.models;

import com.vinichenkosa.javaxmladapter.adapters.CrewMemberAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlJavaTypeAdapter(CrewMemberAdapter.class)
public class Member {

    private String name;
    private String surname;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Member{" + "name=" + name + ", surname=" + surname + '}';
    }
}
В самом адаптере мы будем принимать строку и создавать объект Member:
package com.vinichenkosa.javaxmladapter.adapters;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import com.vinichenkosa.javaxmladapter.models.Member;

public class CrewMemberAdapter extends XmlAdapter<String, Member> {

    @Override
    public Member unmarshal(String v) throws Exception {
        Member member = new Member();
        String[] a = v.split(" ");
        member.setName(a[0]);
        member.setSurname(a[1]);
        return member;
    }

    @Override
    public String marshal(Member v) throws Exception {
        return v.getName()+" "+v.getSurname();
    } 
}
В результате мы должны из строк, получить объекты типа Member. Проверим:
Film{
    title=THE GREAT DICTATOR, 
    description=DICTATOR ADENOID HYNKEL TRIES TO EXPAND HIS ..., 
    releaseDate=Fri Mar 07 00:00:00 MSK 1941, 
    actors=[...], 
    crew=[
        Member{name=Carter, surname=DeHaven}, 
        Member{name=Meredith, surname=Willson}, 
        Member{name=Karl, surname=Struss}, 
        Member{name=Roland, surname=Totheroh}, 
        Member{name=Willard, surname=Nico}, 
        Member{name=Harold, surname=Rice}
    ]
}
Адапетры могут быть написаны не только для простых типов, но и для сложных. Вообще xml элемент вначале преобразуется в экземпляр класса по умолчанию, а потом уже передается в метод unmarshall адапетра. Например, мы можем написать адапетр для класса Actor, чтобы каждый раз когда создается экземпляр этого класса вызывался метод адапетра.
package com.vinichenkosa.javaxmladapter.adapters;

import com.vinichenkosa.javaxmladapter.models.Actor;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class ActorAdapter extends XmlAdapter<Actor, Actor>{

    @Override
    public Actor unmarshal(Actor v) throws Exception {
        v.setFullname(v.getName() + " " + v.getSurname());
        return v;
    }

    @Override
    public Actor marshal(Actor v) throws Exception {
        return v;
    }
}
Как видите этот адапетр принимает уже созданный и заполненный данными объект типа Actor, заполняет в нем поле fullName на основаии данных из xml, которого в самом xml нет и возвращает его. Интересно, а можно ли принимать объект одного типа, а возвращать в класс совсем другого? Можно, но об этом в другой раз :)))

Код на  GitHub: https://github.com/winechess/JavaXmlAdapter

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

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