Tagged: xml

formatting XML in console


user@host:~$ cat xml.xml
<a><b>c</b></a>

user@host:~$ xmllint xml.xml
<?xml version="1.0"?>
<a><b>c</b></a>

user@host:~$ cat xml.xml | xmllint --format -
<?xml version="1.0"?>
<a>
  <b>c</b>
</a>

user@host:~$

Basic RESTful Webapp


Here comes a simple web application for demonstrating the RESTFul web APIs.

Source

svn

$ svn co http://jinahya.googlecode.com/svn/trunk\
> /com.googlecode.jinahya.test/basic-restful-webapp
...
Checked out revision n.

$

mvn

$ cd basic-restful-webapp
$ mvn clean package embedded-glassfish:run
...
Hit ENTER to redeploy, X to exit

Now you can access to resources under http://localhost:58080/basic-restful-webapp/.

Domain Objects

Item.java

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(namespace = "http://jinahya.googlecode.com/test")
@XmlType(propOrder = {"name", "stock"})
public class Item implements Serializable {


    private static final long serialVersionUID = -3104855615755133457L;


    private static final AtomicLong ATOMIC_ID = new AtomicLong();


    public static Item newInstance(final String name, final int stock) {

        final Item instance = new Item();

        instance.name = name;
        instance.stock = stock;

        return instance;
    }


    public static Item putCreatedAtAndId(final Item item) {

        item.createdAt = new Date();
        item.id = ATOMIC_ID.getAndIncrement();

        return item;
    }


    protected Item() {
        super();
    }


    @XmlAttribute
    public Date getCreatedAt() {
        return createdAt;
    }


    @XmlAttribute
    public Date getUpdatedAt() {
        return updatedAt;
    }


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


    private Date createdAt;


    protected Date updatedAt;


    private Long id;


    @XmlElement(required = true, nillable = true)
    protected String name;


    @XmlElement(required = true, nillable = true)
    protected int stock;


}

Items.java

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(namespace = "http://jinahya.googlecode.com/test")
public class Items implements Serializable {


    private static final long serialVersionUID = 5775071328874654134L;


    public Collection<Item> getItems() {

        if (items == null) {
            items = new ArrayList<>();
        }

        return items;
    }


    @XmlElement(name = "item")
    private Collection<Item> items;


}

Business Facade

ItemTable.java

public class ItemFacade {


    private static final long serialVersionUID = 5775071328874654134L;


    private static class InstanceHolder {


        private static final ItemFacade INSTANCE = new ItemFacade();


        private InstanceHolder() {
            super();
        }


    }


    public static ItemFacade getInstance() {
        return InstanceHolder.INSTANCE;
    }


    private ItemFacade() {
        super();
    }


    public void insert(final Item... items) {

        for (Item item : items) {
            Item.putCreatedAtAndId(item);
            tuples.put(item.getId(), item);
        }
    }


    public Item select(final long id) {

        return tuples.get(id);
    }


    public Collection<Item> selectAll() {
        return tuples.values();
    }


    public boolean update(final long id, final Item newItem) {

        if (!tuples.containsKey(id)) {
            return false;
        }

        final Item oldItem = tuples.get(id);
        if (oldItem == null) {
            return false;
        }

        oldItem.updatedAt = new Date();
        oldItem.name = newItem.name;
        oldItem.stock = newItem.stock;

        return true;
    }


    public Item delete(final long id) {
        return tuples.remove(id);
    }


    public void deleteAll() {
        tuples.clear();
    }


    private final Map<Long, Item> tuples = new HashMap<>();


}

Webservice Resource

ItemsResource.java

@Path("/items")
public class ItemsResource {


    @GET
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Items read() {

        final Items items = new Items();

        items.getItems().addAll(ItemFacade.getInstance().selectAll());

        return items;
    }


    @DELETE
    public void delete() {

        ItemFacade.getInstance().deleteAll();
    }


    @POST
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response createItem(@Context final UriInfo info,
                               final Item item) {

        ItemFacade.getInstance().insert(item);

        UriBuilder builder = info.getAbsolutePathBuilder();
        builder = builder.path(Long.toString(item.getId()));

        final URI uri = builder.build();

        return Response.created(uri).build();
    }


    @Path("/{id: \\d+}")
    @GET
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Item readItem(@PathParam("id") final long id) {

        return ItemFacade.getInstance().select(id);
    }


    @Path("/{id: \\d+}")
    @PUT
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response updateItem(@PathParam("id") final long id,
                               final Item newItem) {

        final boolean updated = ItemFacade.getInstance().update(id, newItem);

        if (!updated) {
            return Response.status(Status.NOT_FOUND).build();
        }

        return Response.status(Status.NO_CONTENT).build();
    }


    @Path("/{id: \\d+}")
    @DELETE
    public void deleteItem(@PathParam("id") final long id) {

        ItemFacade.getInstance().delete(id);
    }


}
resource POST GET PUT DELETE
/items CREATE
/items/{id: \\d+} READ UPDATE DELETE

Demonstration

CREATE

$ cat src/test/resources/item.xml
<item xmlns="http://jinahya.googlecode.com/test">
  <name>xml</name>
  <stock>30</stock>
</item>

$ curl -i \
> -X POST \
> http://localhost:58080/items \
> -H "Content-Type: application/xml" \
> --data "@src/test/resources/item.xml"
HTTP/1.1 201 Created
X-Powered-By: ...
Server: ...
Location: http://localhost:58080/items/0
Content-Length: 0
Date: ...

$

READ

$ curl -s \
> http://localhost:58080/items/0 \
> -H "Accept: application/xml" \
> | xmllint --format -
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:item xmlns:ns2="http://jinahya.googlecode.com/test"
          createdAt="2013-03-29T17:43:12.413+09:00"
          id="0">
  <name>xml</name>
  <stock>30</stock>
</ns2:item>

$

UPDATE

$ cat src/test/resources/item.json
{
    "name":"json",
    "stock":"40"
}

$ curl -i \
> -X PUT \
> http://localhost:58080/items/0 \
> -H "Content-Type: application/json" \
> --data "@src/test/resources/item.json"
HTTP/1.1 204 No Content
X-Powered-By: ...
Server: ...
Date: ...


$ curl -s \
> http://localhost:58080/items/0 \
> -H "Accept: application/json" \
> | python -m json.tool
{
    "@createdAt": "2013-03-29T17:43:12.413+09:00",
    "@id": "0",
    "@updatedAt": "2013-03-29T17:48:23.238+09:00",
    "name": "json",
    "stock": "40"
}

$

DELETE

$ curl -i \
> -X DELETE \
> http://localhost:58080/items/0
HTTP/1.1 204 No Content
X-Powered-By: ...
Server: ...
Date: ...

$

/schema.xsd

$ curl -s http://localhost:58080/schema.xsd | xmllint --format -
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema xmlns:tns="http://jinahya.googlecode.com/test"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           attributeFormDefault="unqualified"
           elementFormDefault="qualified"
           version="1.0"
           targetNamespace="http://jinahya.googlecode.com/test">
  <xs:element name="imageFormat" type="tns:imageFormat"/>
  <xs:element name="imageFormats" type="tns:imageFormats"/>
  <xs:element name="imageSuffix" type="tns:imageSuffix"/>
  <xs:element name="imageSuffixes" type="tns:imageSuffixes"/>
  <xs:element name="item" type="tns:item"/>
  <xs:element name="items" type="tns:items"/>
  <xs:complexType name="imageFormat">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="canRead" type="xs:boolean" use="required"/>
        <xs:attribute name="canWrite" type="xs:boolean" use="required"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
  <xs:complexType name="imageFormats">
    <xs:sequence>
      <xs:any processContents="lax" namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
    <xs:attribute name="empty" type="xs:boolean"/>
  </xs:complexType>
  <xs:complexType name="imageSuffix">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="canRead" type="xs:boolean" use="required"/>
        <xs:attribute name="canWrite" type="xs:boolean" use="required"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
  <xs:complexType name="imageSuffixes">
    <xs:sequence>
      <xs:any processContents="lax" namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
    <xs:attribute name="empty" type="xs:boolean"/>
  </xs:complexType>
  <xs:complexType name="item">
    <xs:sequence>
      <xs:element name="name" type="xs:string"/>
      <xs:element name="stock" type="xs:int"/>
    </xs:sequence>
    <xs:attribute name="createdAt" type="xs:dateTime"/>
    <xs:attribute name="id" type="xs:long"/>
    <xs:attribute name="updatedAt" type="xs:dateTime"/>
  </xs:complexType>
  <xs:complexType name="items">
    <xs:sequence>
      <xs:element ref="tns:item" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
    <xs:attribute name="empty" type="xs:boolean"/>
  </xs:complexType>
</xs:schema>

/imageFormats

POST GET PUT DELETE
/imageFormats Read All
/imageTypes/{name} Read Single
$ curl -s -H "Accept: application/xml" http://localhost:58080/imageFormats
<imageFormats xmlns="http://jinahya.googlecode.com/test">
  <imageFormat canRead="true" canWrite="true">jpg</imageFormat>
  <imageFormat canRead="true" canWrite="true">bmp</imageFormat>
  <imageFormat canRead="true" canWrite="true">BMP</imageFormat>
  <imageFormat canRead="true" canWrite="true">JPG</imageFormat>
  <imageFormat canRead="true" canWrite="true">wbmp</imageFormat>
  <imageFormat canRead="true" canWrite="true">jpeg</imageFormat>
  <imageFormat canRead="true" canWrite="true">png</imageFormat>
  <imageFormat canRead="true" canWrite="true">PNG</imageFormat>
  <imageFormat canRead="true" canWrite="true">JPEG</imageFormat>
  <imageFormat canRead="true" canWrite="true">WBMP</imageFormat>
  <imageFormat canRead="true" canWrite="true">GIF</imageFormat>
  <imageFormat canRead="true" canWrite="true">gif</imageFormat>
</imageFormats>
$ curl -s -H "Accept: application/json" http://localhost:58080/imageFormats
{
    "imageFormat": [
        {
            "$": "bmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "BMP",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "jpg",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "JPG",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "wbmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "jpeg",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "png",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "PNG",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "JPEG",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "WBMP",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "GIF",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "gif",
            "@canRead": "true",
            "@canWrite": "true"
        }
    ]
}
$ curl -s -H "Accept: application/xml" http://localhost:58080/imageFormats/png
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imageFormat xmlns="http://jinahya.googlecode.com/test"
             canRead="true" canWrite="true">png</imageFormat>
$ curl -s -H "Accept: application/json" http://localhost:58080/imageFormats/jpeg
{
    "$": "jpeg",
    "@canRead": "true",
    "@canWrite": "true"
}

/imageTypes

POST GET PUT DELETE
/imageTypes Read All
/imageTypes/{name} Read Single
$ curl -s -H "Accept: application/xml" http://localhost:58080/imageTypes
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imageTypes xmlns="http://jinahya.googlecode.com/test">
  <imageType canRead="true" canWrite="true">image/png</imageType>
  <imageType canRead="true" canWrite="true">image/jpeg</imageType>
  <imageType canRead="true" canWrite="true">image/x-png</imageType>
  <imageType canRead="true" canWrite="true">image/vnd.wap.wbmp</imageType>
  <imageType canRead="true" canWrite="true">image/bmp</imageType>
  <imageType canRead="true" canWrite="true">image/gif</imageType>
</imageTypes>
$ curl -s -H "Accept: application/json" http://localhost:58080/imageTypes
{
    "imageType": [
        {
            "$": "image/png",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "image/jpeg",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "image/x-png",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "image/vnd.wap.wbmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "image/bmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "image/gif",
            "@canRead": "true",
            "@canWrite": "true"
        }
    ]
}
$ curl -s -H "Accept: application/xml" http://localhost:58080/imageTypes/image/png
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imageType xmlns="http://jinahya.googlecode.com/test"
           canRead="true" canWrite="true">image/png</imageType>
$ curl -s -H "Accept: application/json" http://localhost:58080/imageTypes/image/jpeg
{
    "$": "image/jpeg",
    "@canRead": "true",
    "@canWrite": "true"
}

/imageSuffixes

POST GET PUT DELETE
/imageTypes Read All
/imageTypes/{name} Read Single
$ curl -H "Accept: application/xml" http://localhost:58080/imageSuffixes
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imageSuffixes xmlns="http://jinahya.googlecode.com/test">
  <imageSuffix canRead="true" canWrite="true">bmp</imageSuffix>
  <imageSuffix canRead="true" canWrite="true">jpg</imageSuffix>
  <imageSuffix canRead="true" canWrite="true">jpeg</imageSuffix>
  <imageSuffix canRead="true" canWrite="true">wbmp</imageSuffix>
  <imageSuffix canRead="true" canWrite="true">png</imageSuffix>
  <imageSuffix canRead="true" canWrite="true">gif</imageSuffix>
</imageSuffixes>
$ curl -H "Accept: application/json" http://localhost:58080/imageSuffixes
{
    "imageSuffix": [
        {
            "$": "bmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "jpg",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "wbmp",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "jpeg",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "png",
            "@canRead": "true",
            "@canWrite": "true"
        },
        {
            "$": "gif",
            "@canRead": "true",
            "@canWrite": "true"
        }
    ]
}
$ curl -H "Accept: application/xml" http://localhost:58080/imageSuffixes/png
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imageSuffix xmlns="http://jinahya.googlecode.com/test"
             canRead="true" canWrite="true">png</imageSuffix>
$ curl -s -H "Accept: application/json" http://localhost:58080/imageSuffixes/jpeg
{
    "$": "jpeg",
    "@canRead": "true",
    "@canWrite": "true"
}

Serving XML Schema with JAX-RS


update

@GET
@Path("/some.xsd")
@Produces({MediaType.APPLICATION_XML})
public Response readSomeXsd() throws JAXBException, IOException {

    final JAXBContext context; // get some

    return Response.ok((StreamingOutput) output -> {
        context.generateSchema(new SchemaOutputResolver() {
            @Override
            public Result createOutput(final String namespaceUri,
                                       final String suggestedFileName)
                throws IOException {
                final Result result = new StreamResult(output);
                result.setSystemId(uriInfo.getAbsolutePath().toString());
                return result;
            }
        });
    }).build();
}

@Context
private transient UriInfo uriInfo;

You can serve the XML Schema for your domain objects.

public class SchemaStreamingOutput implements StreamingOutput {

    public SchemaStreamingOutput(final JAXBContext context) {
        super();

        if (context == null) {
            throw new NullPointerException("null context");
        }

        this.context = context;
    }

    @Override
    public void write(final OutputStream output) throws IOException {

        context.generateSchema(new SchemaOutputResolver() {

            @Override
            public Result createOutput(final String namespaceUri,
                                       final String suggestedFileName)
                throws IOException {

                return new StreamResult(output) {

                    @Override
                    public String getSystemId() { // nasty
                        return suggestedFileName;
                    }
                };
            }
        });
    }

    private final JAXBContext context;
}

Now you can serve your XML Schema like this.

@GET
@Path("/items.xsd")
@Produces({MediaType.APPLICATION_XML})
public Response readXsd() throws JAXBException {

    final JAXBContext context = ...;

    return Response.ok(new SchemaStreamingOutput(context)).build();
}

인터넷 우체국 사업자 포털 오픈API


인터넷에서 우편번호 검색하는 서비스가 있나 하고 검색을 하던 중 반가운 페이지에 도착했다.

이름하여, “오픈API“!!!

아… 이제 우리나라도 이런거 되는구나! 역시!!!

기쁨은 아주 잠시 뿐, 실망스러운 부분이 너무도 많다.

References


다음 링크들을 참고하였다.

HTTP

  • GET POST 둘 다 된다. (이건 좋은 건가?)
  • query request parameter는 인코딩을 아예 안하거나(@@?) euc-kr로 하면 된다.
  • Accept-Language: ko를 꼭 써줘야 한다.

XML

  • 오랜만에 보는 encoding="euc-kr"이다.
  • 아주 멋찐 <![CDATA[]]>!!!
  • 역시나 XML Namespace를 기대하기는 어렵다.
  • 특정 element localname({}postcd)이 예시문서({}zipcode)와 다르다.
  • 서울 광진구 구의1동 248~252 이 따위로 주면 번지 구간을 어떻게 자르라는 건가?

How should be

EJB, JAX-RS

여기에 Stateless Session Bean과 JAX-RS Resource들을 구현해 놓았다. 참고하시길.
단 PostalCodesBean을 쓰려면 다음과 같이 ejb-jar.xml에 regkey값을 설정해줘야 한다.

<?xml version="1.0" encoding="UTF-8"?>

<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee"
         version = "3.1"
         xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd">
  <enterprise-beans>
    <session>
      <ejb-name>PostalCodesBean</ejb-name>
      <env-entry>
        <env-entry-name>com.googlecode.jinahya.epost.openapi.PostalCodesBean/regkey</env-entry-name>
        <!--env-entry-type>java.lang.String</env-entry-type-->
        <env-entry-value>{your_regkey}</env-entry-value>
      </env-entry>
    </session>
  </enterprise-beans>
</ejb-jar>

XML Schema

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" version="1.0"
           targetNamespace="http://jinahya.googlecode.com/epost/openapi"
           xmlns="http://jinahya.googlecode.com/epost/openapi"
           xmlns:tns="http://jinahya.googlecode.com/epost/openapi"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <xs:element name="postalCodes" type="postalCodes"/>

  <xs:complexType name="postalCode">
    <xs:sequence>
      <xs:element name="address" type="xs:string"/>
      <xs:element name="code" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="postalCodes">
    <xs:sequence>
      <xs:element name="postalCode" type="postalCode" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

XML

<?xml version="1.0" encoding="utf-8"?>
<postalCodes xmlns:xs="http://www.w3.org/2001/XMLSchema"
	     xmlns="http://jinahya.googlecode.com/epost/openapi"
	     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <postalCode>
    <address>서울 광진구 구의1동 632~640</address>
    <code>143835</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 257</address>
    <code>143828</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 253~254</address>
    <code>143827</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 248~252</address>
    <code>143826</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 232~241</address>
    <code>143824</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 221~222</address>
    <code>143823</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 서울광진경찰서</address>
    <code>143703</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동</address>
    <code>143201</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 새한아파트</address>
    <code>143722</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 242~243</address>
    <code>143825</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 246</address>
    <code>143825</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 641~655</address>
    <code>143836</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 크레신타워3차</address>
    <code>143716</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 225~226</address>
    <code>143960</code>
  </postalCode>
  <postalCode>
    <address>서울 광진구 구의1동 229~231</address>
    <code>143962</code>
  </postalCode>
</postalCodes>

Comments

<bluff>지금 내가 가는 길을 누군가 먼저 걷지 않았다면 내 발은 온통 자갈에 채여 멍이 들고 가시에 찔려 피를 흘리고 있을 것이다.</bluff>

— 나 —

아래는 그 ‘누군가’에 해당하는 분들의 말씀들이다.

어지간히 단가가 쌌던 모양이다.

— Dopany Soft —

여담이지만 우리나라 API제공은 전혀 친철하지 않다..
그냥 알아서 쓸려면 쓰고 말라면 말라는 식이다.

우체국 API페이지도 들어가보면 참 부실하기 짝이 없는 도움말들..
뭐 이유야 훤히 보이는 거지만..
외부에 맞겨서 API개발하고 소스는 돌아가지만
그 시스템을 아는 사람은 우체국 쪽에 별로 없다..
그래서 도움말도 외부업체에서 받은 기술서가 전부고 그마저도 대충 보여주고 마는 거겠지…

— 수유산장 —

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	  

import XML namespace in your XML Schema


<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:xml="http://www.w3.org/XML/1998/namespace" ...>
  <!--
  <xs:import namespace="http://www.w3.org/XML/1998/namespace"
             schemaLocation="http://www.w3.org/XML/1998/xml.xsd"/>
  -->
  <xs:import namespace="http://www.w3.org/XML/1998/namespace"
             schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
  ...
</xs:schema>

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

W3C Widget XML Configuration Schema


<?xml version="1.0" encoding="UTF-8"?>
<!-- Jin Kwon -->
<!-- This XML Schema MAY NOT fully equivalent to W3C's RELAX NG gramma. And it's for internal use only. -->
<xs:schema xmlns="http://www.w3.org/ns/widgets" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.w3.org/ns/widgets" xmlns:xml="http://www.w3.org/XML/1998/namespace" targetNamespace="http://www.w3.org/ns/widgets" elementFormDefault="qualified" attributeFormDefault="unqualified">
  <xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2007/08/xml.xsd"/>
  <!--xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="./xml.xsd"/-->
  <xs:annotation>
    <xs:documentation>Configuration Document</xs:documentation>
  </xs:annotation>
  <xs:element name="widget">
    <xs:annotation>
      <xs:documentation>The root element of a configuration document.</xs:documentation>
    </xs:annotation>
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:choice>
          <xs:element name="name">
            <xs:annotation>
              <xs:documentation>The name of the widget.</xs:documentation>
            </xs:annotation>
            <xs:complexType mixed="true">
              <xs:choice minOccurs="0" maxOccurs="unbounded">
                <xs:element ref="span"/>
                <!--xs:any namespace="##other" processContents="skip"/-->
              </xs:choice>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="short" type="xs:token"/>
              <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
            </xs:complexType>
          </xs:element>
          <xs:element name="description">
            <xs:annotation>
              <xs:documentation>Some text that describes the purpose of the widget.</xs:documentation>
            </xs:annotation>
            <xs:complexType mixed="true">
              <xs:choice minOccurs="0" maxOccurs="unbounded">
                <xs:element ref="span"/>
                <!--xs:any namespace="##other" processContents="skip"/-->
              </xs:choice>
              <xs:attributeGroup ref="global"/>
              <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
            </xs:complexType>
          </xs:element>
          <xs:element name="author">
            <xs:annotation>
              <xs:documentation>The person or person that created the widget.</xs:documentation>
            </xs:annotation>
            <xs:complexType mixed="true">
              <xs:choice minOccurs="0" maxOccurs="unbounded">
                <xs:element ref="span"/>
                <!--xs:any namespace="##other" processContents="skip"/-->
              </xs:choice>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="href" type="xs:anyURI"/>
              <xs:attribute name="email" type="xs:token"/>
              <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
            </xs:complexType>
          </xs:element>
          <xs:element name="license">
            <xs:annotation>
              <xs:documentation>The license under which the widget is distributed.</xs:documentation>
            </xs:annotation>
            <xs:complexType mixed="true">
              <xs:choice minOccurs="0" maxOccurs="unbounded">
                <xs:element ref="span"/>
                <!--xs:any namespace="##other" processContents="skip"/-->
              </xs:choice>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="href" type="xs:anyURI"/>
              <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
            </xs:complexType>
          </xs:element>
          <xs:element name="icon">
            <xs:annotation>
              <xs:documentation>An iconic representation of the widget.</xs:documentation>
            </xs:annotation>
            <xs:complexType>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="src" type="xs:token" use="required"/>
              <xs:attribute name="width" type="xs:unsignedInt"/>
              <xs:attribute name="height" type="xs:unsignedInt"/>
            </xs:complexType>
          </xs:element>
          <xs:element name="content">
            <xs:annotation>
              <xs:documentation>The means to point to the "main file" of a widget; serves as a boot-strapping mechanism.</xs:documentation>
            </xs:annotation>
            <xs:complexType>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="src" type="xs:token" use="required"/>
              <xs:attribute name="type" type="xs:token"/>
              <xs:attribute name="encoding" type="xs:token" default="UTF-8"/>
            </xs:complexType>
          </xs:element>
          <xs:element name="feature">
            <xs:annotation>
              <xs:documentation>A means to request the availability of a feature, such as an API, that would not normally be part of the default set of features provided by the user agent at runtime.</xs:documentation>
            </xs:annotation>
            <xs:complexType>
              <xs:sequence>
                <xs:element name="param" nillable="true" minOccurs="0" maxOccurs="unbounded">
                  <xs:complexType>
                    <xs:attributeGroup ref="global"/>
                    <xs:attribute name="name" type="xs:token" use="required"/>
                    <xs:attribute name="value" type="xs:token" use="required"/>
                  </xs:complexType>
                </xs:element>
              </xs:sequence>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="name" type="xs:token" use="required"/>
              <xs:attribute name="required" type="xs:boolean" default="true"/>
            </xs:complexType>
          </xs:element>
          <xs:element name="preference">
            <xs:annotation>
              <xs:documentation>A means to declare a name-value pair that is made available to the widget at runtime.</xs:documentation>
            </xs:annotation>
            <xs:complexType>
              <xs:attributeGroup ref="global"/>
              <xs:attribute name="name" type="xs:token" use="required"/>
              <xs:attribute name="value" type="xs:token"/>
              <xs:attribute name="readonly" type="xs:boolean" default="false"/>
            </xs:complexType>
          </xs:element>
          <xs:element name="update-description">
            <xs:complexType>
              <xs:attribute name="href" type="xs:anyURI"/>
            </xs:complexType>
          </xs:element>
          <xs:element name="access">
            <xs:complexType>
              <xs:attribute name="origin" type="xs:anyURI" use="required"/>
              <xs:attribute name="subdomains" type="xs:boolean" default="false"/>
              <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:choice>
      <xs:attributeGroup ref="global"/>
      <xs:attribute name="id" type="xs:anyURI"/>
      <xs:attribute name="version" type="xs:token"/>
      <xs:attribute name="height" type="xs:unsignedInt"/>
      <xs:attribute name="width" type="xs:unsignedInt"/>
      <xs:attribute name="viewmodes" type="viewmodes"/>
      <xs:attribute name="defaultlocale" type="xs:language"/>
    </xs:complexType>
    <xs:unique name="uniqueDescriptionByItsLang">
      <xs:selector xpath="tns:description"/>
      <xs:field xpath="@xml:lang"/>
    </xs:unique>
    <xs:unique name="uniqueLicenseByItsLang">
      <xs:selector xpath="tns:license"/>
      <xs:field xpath="@xml:lang"/>
    </xs:unique>
    <xs:unique name="uniqueNameByItsLang">
      <xs:selector xpath="tns:name"/>
      <xs:field xpath="@xml:lang"/>
    </xs:unique>
  </xs:element>
  <xs:simpleType name="dir">
    <xs:restriction base="xs:token">
      <xs:enumeration value="ltr">
        <xs:annotation>
          <xs:documentation>Left-to-right text.</xs:documentation>
        </xs:annotation>
      </xs:enumeration>
      <xs:enumeration value="rtl">
        <xs:annotation>
          <xs:documentation>Right-to-left text.</xs:documentation>
        </xs:annotation>
      </xs:enumeration>
      <xs:enumeration value="lro">
        <xs:annotation>
          <xs:documentation>Left-to-right override.</xs:documentation>
        </xs:annotation>
      </xs:enumeration>
      <xs:enumeration value="rlo">
        <xs:annotation>
          <xs:documentation>Right-to-left override.</xs:documentation>
        </xs:annotation>
      </xs:enumeration>
    </xs:restriction>
  </xs:simpleType>
  <xs:attributeGroup name="global">
    <xs:attribute ref="xml:lang"/>
    <xs:attribute name="dir" type="dir"/>
  </xs:attributeGroup>
  <xs:simpleType name="viewmode">
    <xs:restriction base="xs:token">
      <xs:enumeration value="windowed"/>
      <xs:enumeration value="floating"/>
      <xs:enumeration value="fullscreen"/>
      <xs:enumeration value="maximized"/>
      <xs:enumeration value="minimized"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="viewmodes">
    <xs:list itemType="viewmode"/>
  </xs:simpleType>
  <xs:element name="span">
    <xs:annotation>
      <xs:documentation>A generic text container which is mainly used for internationalization purposes.</xs:documentation>
    </xs:annotation>
    <xs:complexType mixed="true">
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element ref="span"/>
        <!--xs:any namespace="##other" processContents="skip"/-->
      </xs:choice>
      <xs:attributeGroup ref="global"/>
      <!--xs:anyAttribute namespace="##other" processContents="skip"/-->
    </xs:complexType>
  </xs:element>
</xs:schema>