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가 종종 헷갈리는 경우가 있다.

plural
.../resources/items
singular
.../resources/items/1234
GET
<R>
모든 Item들을 반환한다.


<items>
  <item id="1233">...</item>
  <item id="1234">...</item>
</items>


1234에 해당하는 Item 1개를 반환한다.


<item id="1234">
  ...
</item>


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개를 제거한다.