Tagged: xmladapter
extensible XmlAdapter for Map bound types
References
- Java Tip of the Day: Generic JAXB Map XmlAdapter
- XmlAdapter – JAXB’s Secret Weapon
- JAXB and java.util.Map
- JAXB, Custom XML to Java Map using XMLAdapter
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