Tagged: jaxb

XML Schema strings with JAXB


References

package com.googlecode.jinahya.test;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.NormalizedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;

/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlRootElement(name = "bind")
@XmlAccessorType(XmlAccessType.NONE)
@XmlType(propOrder = {"token", "norma", "strin"})
public class StringBind {

    public static void main(final String[] args)
        throws JAXBException, IOException {

        final JAXBContext context = JAXBContext.newInstance(StringBind.class);

        // XML Schema ----------------------------------------------------------
        final StringWriter schemaWriter = new StringWriter();
        context.generateSchema(new SchemaOutputResolver() {
            @Override
            public Result createOutput(final String namespaceUri,
                                       final String suggestedFileName)
                throws IOException {
                return new StreamResult(schemaWriter) {
                    @Override
                    public String getSystemId() {
                        return suggestedFileName;
                    }
                };
            }
        });
        schemaWriter.flush();
        System.out.println("-------------------------------------- XML Schema");
        System.out.println(schemaWriter.toString());

        final String unprocessed = "\t ab\r\n   c\t \r \n";

        // marshal -------------------------------------------------------------
        final StringBind marshalling = new StringBind();
        marshalling.token = unprocessed;
        marshalling.norma = unprocessed;
        marshalling.strin = unprocessed;
        final Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        final StringWriter writer = new StringWriter();
        marshaller.marshal(marshalling, writer);
        writer.flush();
        System.out.println("-------------------------------------- marshalled");
        System.out.println(writer.toString());

        // unmarshal -----------------------------------------------------------
        final Unmarshaller unmarshaller = context.createUnmarshaller();
        final StringBind unmarshalled = (StringBind) unmarshaller.unmarshal(
            new StringReader(writer.toString()));
        System.out.println("------------------------------------ unmarshalled");
        System.out.println(unmarshalled.toString());
    }

    @Override
    public String toString() {
        return "token: " + token + "\nnorma: " + norma + "\nstrin: " + strin;
    }

    @XmlElement
    @XmlSchemaType(name = "token")
    @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
    private String token;

    @XmlElement
    @XmlSchemaType(name = "normalizedString")
    @XmlJavaTypeAdapter(NormalizedStringAdapter.class)
    private String norma;

    @XmlElement
    //@XmlSchemaType(name = "string") // @@?
    //@XmlJavaTypeAdapter(StringAdapter.class) // there is no StringAdapter.class
    private String strin;
}

Note that NormalizedStringAdapter#marshal(String) and CollapsedStringAdapter#marshal(String) don’t do anything.

-------------------------------------- XML Schema
<?xml version="1.0" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="bind" type="stringBind"/>

  <xs:complexType name="stringBind">
    <xs:sequence>
      <xs:element name="token" type="xs:token" minOccurs="0"/>
      <xs:element name="norma" type="xs:normalizedString" minOccurs="0"/>
      <xs:element name="strin" type="xs:string" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>


-------------------------------------- marshalled
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<bind>
    <token>	 ab
   c	  
</token>
    <norma>	 ab
   c	  
</norma>
    <strin>	 ab
   c	  
</strin>
</bind>

------------------------------------ unmarshalled
token: ab c
norma:   ab    c    
strin: 	 ab
   c	  
Advertisements

‘class defined’ callback methods in JAXB


/**
 * Invoked by Marshaller after it has created an instance of this object.
 */
private void beforeMarshal(final Marshaller marshaller) {
    // this method marked as returning a 'boolean' value,
    // but 'void' seems work.
}

/**
 * Invoked by Marshaller after it has marshalled all properties of this
 * object.
 */
private void afterMmarshal(final Marshaller marshaller) {
}

/**
 * This method is called immediately after the object is created and before
 * the unmarshalling of this object begins. The callback provides an
 * opportunity to initialize JavaBean properties prior to unmarshalling.
 */
private void beforeUnmarshal(final Unmarshaller unmarshaller,
                             final Object parent) {
}

/**
 * This method is called after all the properties (except IDREF) are
 * unmarshalled for this object, but before this object is set to the parent
 * object.
 */
private void afterUnmarshal(final Unmarshaller unmarshaller,
                            final Object parent) {
}

As of writing this entry(java version “1.7.0_03”), becoreMarshal and afterMarshal seem being called twice each with any external listeners set.

import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Callback {

    public static void main(final String[] args) throws Exception {

        final JAXBContext context = JAXBContext.newInstance(Callback.class);

        final Callback before = new Callback();
        before.id = 0L;
        before.name = "name";

        final StringWriter writer = new StringWriter();
        context.createMarshaller().marshal(before, writer);
        writer.flush();

        final String xml = writer.toString();
        System.out.println(xml);

        final StringReader reader = new StringReader(xml);
        final Callback after =
            (Callback) context.createUnmarshaller().unmarshal(reader);
    }

    private void beforeMarshal(Marshaller marshaller) {
        System.out.println("beforeMarshal(" + marshaller + ")");
    }

    private void afterMarshal(final Marshaller marshaller) {
        System.out.println("afterMarshal(" + marshaller + ")");
    }

    private void beforeUnmarshal(final Unmarshaller unmarshaller,
                                 final Object parent) {
        System.out.println(
            "beforeUnmarshal(" + unmarshaller + ", " + parent + ")");
    }

    private void afterUnmarshal(final Unmarshaller unmarshaller,
                                final Object parent) {
        System.out.println(
            "afterUnmarshal(" + unmarshaller + ", " + parent + ")");
    }

    @XmlAttribute
    private Long id;

    @XmlElement
    private String name;
}
beforeMarshal(com.sun.xml.internal.bind.v2.runtime.MarshallerImpl@ccfcd20)
beforeMarshal(com.sun.xml.internal.bind.v2.runtime.MarshallerImpl@ccfcd20)
afterMarshal(com.sun.xml.internal.bind.v2.runtime.MarshallerImpl@ccfcd20)
afterMarshal(com.sun.xml.internal.bind.v2.runtime.MarshallerImpl@ccfcd20)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><callback id="0"><name>name</name></callback>
beforeUnmarshal(com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl@1ebe99f8, null)
afterUnmarshal(com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl@1ebe99f8, null)

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