Tagged: xml
Inserting an XML declaration with nXML mode
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(); }
Printing binary as ‘hexBinary’ not ‘base64Binary’ with JAXB
@XmlElement //@XmlSchemaType(name = "hexBinary") // seems not work @XmlJavaTypeAdapter(HexBinaryAdapter.class) private byte[] hex @XmlElement // :) private byte[] base64;
<hex>45374A18354C8415D580D32E7EB987A6890A4EA370B0EE4109D86B70159B23</hex> <base64>RTdKGDVMhBXVgNMufrmHpokKTqNwsO5BCdhrcBWbIw==</base64>
인터넷 우체국 사업자 포털 오픈API
인터넷에서 우편번호 검색하는 서비스가 있나 하고 검색을 하던 중 반가운 페이지에 도착했다.
이름하여, “오픈API“!!!
아… 이제 우리나라도 이런거 되는구나! 역시!!!
기쁨은 아주 잠시 뿐, 실망스러운 부분이 너무도 많다.
References
다음 링크들을 참고하였다.
- 우체국 Open API 인코딩 문제
- 우체국 오픈 api 사용하기
- Classic ASP에서 우체국 우편번호 검색 API 사용하기
- Classic ASP에서 우체국 우편번호 API 사용하기 2
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
- XML Schema Part 2: Datatypes Second Edition (W3C)
- Whitespace in xsd:string, xsd:normalizedString, xsd:token (xmlplease.com)
- XSD String Data Types (w3schools.com)
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>