Tagged: restful
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(); }
CRUD for RESTful Web Services
Jersey를 사용하지 않고 그냥 만들어야 할 경우 CRUD에 관계된 Resource URI와 HTTP Method가 종종 헷갈리는 경우가 있다.
- Representational state transfer/RESTful web services
- Build a RESTful Web service using Jersey and Apache Tomcat
plural.../resources/items |
singular.../resources/items/1234 |
|
---|---|---|
GET <R> |
모든 Item들을 반환한다.
|
1234에 해당하는 Item 1개를 반환한다.
|
PUT <U> |
모든 Item들을 교체한다. | 1234에 해당하는 Item 1개의 내용을 갱신한다. 없으면 새로 만드는 게 아니라 Not Found(404)이다. |
POST <C> |
새로운 Item을 생성한다. 이 때 사로이 생성된 Item의 uri를 CREATED(201)로 리턴한다. | 1234에 해당하는 리소스를 plural resource으로 보고 그 안에 새로운 singular resource를 생성한다. |
DELETE <D> |
다 날린다. | 1234에 해당하는 Item 1개를 제거한다. |