Tagged: map

extensible XmlAdapter for Map bound types


References

Java Sources

When keys are not related to values

When keys can be derived from values

Apache Maven

<dependency>
  <groupId>com.googlecode.jinahya</groupId>
  <artifactId>jinahya-se</artifactId>
  <version>@@?</version>
<dependency>

Example Classes

Here are Employee and Department we gonna use.

public class Employee {

    @XmlElement
    private long id;

    @XmlElement
    private String name;

    @XmlElement
    private int age;
}
@XmlTransient
public abstract class Department {

    public Map<Long, Employee> getEmployees() {
        if (employees == null) {
            employees = new HashMap<>();
        }
        return employees;
    }

    private Map<Long, Employee> employees;
}

Canonical Approach

There are three abstract classes for extension.

@XmlTransient
public abstract class MapEntry<K, V> {

    protected K getKey();

    protected void setKey(K key);

    protected V getValue();

    protected void setValue(V value);
}
@XmlTransient
public abstract class MapEntries<E extends MapEntry<K, V>, K, V> {

    public MapEntries(final Class<E> entryType);

    protected List<E> getEntries();
}
public abstract class MapEntriesAdapter<T extends MapEntries<?, K, V>, K, V>
    extends XmlAdapter<T, Map<K, V>> {

    public MapEntriesAdapter(final Class<T> entriesType);
}

Note that both MapEntry and MapEntries are both annotated with @XmlTransient.

public class EmployeeEntry extends MapEntry<Long, Employee> {

    @XmlAttribute
    public Long getId() {
        return getKey();
    }

    public void setId(final Long id) {
        setKey(id);
    }

    @XmlElement
    public Employee getEmployee() {
        return getValue();
    }

    public void setEmployee(final Employee employee) {
        setValue(employee);
    }
}
public class EmployeeEntries extends MapEntries<EmployeeEntry, Long, Employee> {

    public EmployeeEntries() {
        super(EmployeeEntry.class);
    }

    @XmlElement(name = "employeeEntry")
    public List<EmployeeEntry> getEmployeeEntries() {
        return getEntries();
    }
}
public class EmployeesAdapter0
    extends MapEntriesAdapter<EmployeeEntries, Long, Employee> {

    public EmployeesAdapter0() {
        super(EmployeeEntries.class);
    }
}
@XmlRootElement
public class Department0 extends Department {

    @XmlElement
    @XmlJavaTypeAdapter(EmployeesAdapter0.class)
    @Override
    public Map<Long, Employee> getEmployees() {
        return super.getEmployees();
    }
}
<department0 xmlns="http://jinahya.googlecode.com/xml/bind/test/map">
    <employees>
        <employeeEntry>
            <id>0</id>
            <employee>
                <id>0</id>
                <name>name0</name>
                <age>20</age>
            </employee>
        </employeeEntry>
        <employeeEntry>
            <id>1</id>
            <employee>
                <id>1</id>
                <name>name1</name>
                <age>21</age>
            </employee>
        </employeeEntry>
        <employeeEntry>
            <id>2</id>
            <employee>
                <id>2</id>
                <name>name2</name>
                <age>22</age>
            </employee>
        </employeeEntry>
    </employees>
</department0>

When keys can be derived from values

Why <employeeEntry> elements are there? Because above Canonical Approach assumed that those keys and values are very different types. So what if those keys can be derived from values?
There are another abstract classes for this case.

@XmlTransient
public abstract class MapValues<V> {

    protected List<V> getValues();
}
public abstract class MapValuesAdapter<T extends MapValues<V>, K, V>
    extends XmlAdapter<T, Map<K, V>> {

    public MapValuesAdapter(final Class<T> mapValuesType);

    protected abstract K getKey(V value);
}
public class EmployeeValues extends MapValues<Employee> {

    @XmlElement(name = "employee")
    public List<Employee> getEmployees() {
        return super.getValues();
    }
}

Now we can create another type of XmlAdapter.

public class EmployeesAdapter2
    extends MapValuesAdapter<EmployeeValues, Long, Employee> {

    public EmployeesAdapter2() {
        super(EmployeeValues.class);
    }

    @Override
    protected Long getKey(final Employee value) {
        return value.getId();
    }
}
@XmlRootElement
public class Department2 extends Department {

    @XmlElement
    @XmlJavaTypeAdapter(EmployeesAdapter2.class)
    @Override
    public Map<Long, Employee> getEmployees() {
        return super.getEmployees();
    }
}

Now we got this.

<department2 xmlns="http://jinahya.googlecode.com/xml/bind/test/map">
    <employees>
        <employee>
            <id>0</id>
            <name>name0</name>
            <age>20</age>
        </employee>
        <employee>
            <id>1</id>
            <name>name1</name>
            <age>21</age>
        </employee>
        <employee>
            <id>2</id>
            <name>name2</name>
            <age>22</age>
        </employee>
    </employees>
</department2>

When there are no siblings

Note that those classes explained so far are for general properties with siblings. When a Department doesn’t have any other properties but the Employee list, there is no need to work with XmlAdapter at all.

@XmlRootElement
public class Department7 extends AbstractDepartment {

    @XmlElement(name = "employee")
    private List<Employee> getEmployeeList() {
        final Collection<Employee> employees = getEmployees().values();
        if (employees instanceof List) {
            return (List<Employee>) employees;
        }
        return new ArrayList<>(employees);
    }

    private void setEmployeeList(final List<Employee> employeeList) {
        for (Employee employee : employeeList) {
            getEmployees().put(employee.getId(), employee);
        }
    }

    // not even need to be overridden; just informed.
    @XmlTransient
    @Override
    public Map<Long, Employee> getEmployees() {
        return super.getEmployees();
    }
}

Here comes a little bit more concise version.

<department7 xmlns="http://jinahya.googlecode.com/xml/bind/test/map">
    <employee>
        <id>0</id>
        <name>name0</name>
        <age>20</age>
    </employee>
    <employee>
        <id>1</id>
        <name>name1</name>
        <age>21</age>
    </employee>
    <employee>
        <id>2</id>
        <name>name2</name>
        <age>22</age>
    </employee>
</department7>

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