XmlAdapter for Map BoundType


이런 저런 프로젝트에서 XmlAdapter를 확장해서 쓰던 중 기능을 좀 모아볼까 하는 생각에 클래스를 몆 개 만들어 봤습니다.
XmlAdapter에 대한 개념 탑재는 다음 링크에서… :)
XmlAdapter in JAXB RI EA
Using JAXB 2.0’s XmlJavaTypeAdapter
XmlAdapter – JAXB’s Secret Weapon


자 우선 Map을 BoundType으로 가질 수 있는 추상 클래스 하나 만들고,

public abstract class MapBoundTypeAdapter<T, K, V>
    extends XmlAdapter<T, Map<K, V>> {

    protected abstract Map<K, V> newBoundType(int valueTypeSize);

    protected abstract K getKey(V value);
}

잠깐 삼천포로 빠져서 배열형을 위한 추상 클래서 하나 빼고,

public abstract class ArrayMapAdapter<K, V>
    extends MapBoundTypeAdapter<V[], K, V> {

    public ArrayMapAdapter(final Class<V> valueElementType) {
        super();
        if (valueElementType == null) {
            throw new NullPointerException("null valueElementType");
        }
        this.valueElementType = valueElementType;
    }

    @Override
    public V[] marshal(final Map<K, V> boundType) throws Exception {
        @SuppressWarnings("unchecked")
        final V[] valueType =
            (V[]) Array.newInstance(valueElementType, boundType.size());
        boundType.values().toArray(valueType);
        return valueType;
    }

    @Override
    public Map<K, V> unmarshal(final V[] valueType) throws Exception {
        final Map<K, V> boundType = newBoundType(valueType.length);
        for (V value : valueType) {
            boundType.put(getKey(value), value);
        }
        return boundType;
    }

    @Override
    protected Map<K, V> newBoundType(final int valueTypeSize) {
        return new HashMap<K, V>(valueTypeSize);
    }

    private final Class<V> valueElementType;

JAXB가 알아먹을 수 있는 ValueType을 위한 추상 클래스를 만든 다음,

public abstract class ListValueType<V> {

    public List<V> getValues() {
        if (values == null) {
            values = new ArrayList<V>();
        }
        return values;
    }

    private List<V> values;
}

마지막으로 ListValueType(ValueType)과 Map(BoundType)를 위한 XmlAdapter를 만들었습니다.

public abstract class ListMapAdapter<L extends ListValueType<V>, K, V>
    extends MapBoundTypeAdapter<L, K, V> {

    public ListMapAdapter(final Class<L> valueTypeClass) {
        super();
        if (valueTypeClass == null) {
            throw new NullPointerException("null valueTypeClass");
        }
        this.valueTypeClass = valueTypeClass;
    }

    @Override
    public L marshal(final Map<K, V> boundType) throws Exception {
        final L valueType = newValueType(boundType.size());
        valueType.getValues().addAll(boundType.values());
        return valueType;
    }

    @Override
    public Map<K, V> unmarshal(final L valueType) throws Exception {
        final Map<K, V> boundType = newBoundType(valueType.getValues().size());
        for (V value : valueType.getValues()) {
            boundType.put(getKey(value), value);
        }
        return boundType;
    }

    protected L newValueType(int boundTypeSize) {
        try {
            return valueTypeClass.newInstance();
        } catch (InstantiationException ie) {
            throw new RuntimeException(
                "failed to create a new instance of " + valueTypeClass, ie);
        } catch (IllegalAccessException iae) {
            throw new RuntimeException(
                "failed to create a new instance of " + valueTypeClass, iae);
        }
    }

    @Override
    protected Map<K, V> newBoundType(int valueTypeSize) {
        return new HashMap<K, V>(valueTypeSize);
    }

    protected final Class<L> valueTypeClass;

아래 예시에서는 이해도를 높히기 위해 코드의 상당 부분을 삭제했습니다.
다음과 같은 Staff 클래스가 있다고 합시다.

public class Staff {

    @XmlAttribute(required = true)
    private long id;

    @XmlValue
    private String name;
}

위 Staff클래스를 줄줄이 모아둘 수 있는 클래스입니다. 이 클래스는 ListValueType을 학장합니다. getValues()를 오버라이드함으로써 별도의 이름(“staff”)을 지정(@XmlElement)할 수 있습니다.

public class Crowd extends ListValueType<Staff> {

    @XmlElement(name = "staff")
    @Override
    public List<Staff> getValues() {
        return super.getValues();
    }
}

여기 Crowd와 Map의 상호 변환을 담당하는 XmlAdapter를 만들었습니다. TypeParameter의 개수와 순서에 주목해주세요.

public class CrowdAdapter extends ListMapAdapter<Crowd, Long, Staff> {

    public CrowdAdapter() {
        super(Crowd.class);
    }

    @Override
    protected Long getKey(final Staff value) {
        return value.getId();
    }
}

마지막으로, Department입니다.
물론 Department에서 Staff를 모아두는 Collection으로 List를 사용할 수도 있습니다. 굳이 Map를 사용하는 이유는 편의성을 위해서 입니다. 다음 소스에 예시를 두개 정도 넣어 놨습니다.

public class Department {

    /**
     * 기분 전환이 필요할 때 아무나 골라서 짤라버린다.
     *
     * @param id 짜를 놈의 사번
     * @return 짤린 놈; 눈에 안보이면 null
     */
    public Staff fire(final long id) {
        return getCrowd().remove(id);
    }
 
    /**
     * 주어진 id를 가지는 Staff를 반한한다.
     *
     * @param id Staff's id (사번?)
     * @return the Staff whose id is equals to specified <code>id</code>
     *         or null if not found
     */
    public Staff getStaff(final long id) {
        return getCrowd().get(id);
    }

    @XmlElement(required = true)
    private String name;

    @XmlJavaTypeAdapter(CrowdAdapter.class)
    private Map<Long, Staff> crowd;
}

자 이제 테스트를 해 봅시다. Department를 하나 만들고 세명의 Staff를 추가했습니다. JAXB를 이용해서 marshal/unmarshal 을 해보겠습니다.

public class DepartmentTest {

    private static final JAXBContext JAXB_CONTEXT;

    static {
        try {
            JAXB_CONTEXT = JAXBContext.newInstance(
                Department.class, Crowd.class);
        } catch (JAXBException jaxbe) {
            throw new InstantiationError(jaxbe.getMessage());
        }
    }

    @Test(enabled = false)
    public byte[] marshal() throws JAXBException, IOException {
        final Department department = Department.newInstance(
            "IT",
            Staff.newInstance(0, "Roy Trenneman"),
            Staff.newInstance(1, "Maurice Moss"),
            Staff.newInstance(2, "Jen Barber"));
        final Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        marshaller.marshal(department, baos);
        baos.flush();
        final byte[] bytes = baos.toByteArray();
        final String charsetName =
            (String) marshaller.getProperty(Marshaller.JAXB_ENCODING);
        System.out.println(new String(bytes, charsetName));
        return bytes;
    }

    @Test
    public void unmarshal() throws JAXBException, IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(marshal());
        final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
        final Department department = unmarshaller.unmarshal(
            new StreamSource(bais), Department.class).getValue();
        System.out.println("department.name: " + department.getName());
        for (Staff person : department.getCrowd().values()) {
            System.out.println("department.staff: " + person);
        }
    }
}

짜잔! The IT Crowd 재밌져?

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<department>
    <name>IT</name>
    <crowd>
        <staff id="0">Roy Trenneman</staff>
        <staff id="1">Maurice Moss</staff>
        <staff id="2">Jen Barber</staff>
    </crowd>
</department>

department.name: IT
department.staff: ...Staff@2ec84a61 id=0, name=Roy Trenneman
department.staff: ...Staff@25c65d76 id=1, name=Maurice Moss
department.staff: ...Staff@d1ef993c id=2, name=Jen Barber
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s