Web services are common in most applications today.
Web services provide synchroneous request-response style integration between applications in a convenient way:
Any developer need to have a basic understanding of web services.
RESTful web services been very popular since around 2010.
If asynchroneous integration is required then look at message queues. Read more here.
Web services exist in two types:
This article will cover RESTful web services.
For RPC style SOAP web services read here.
Roy Fielding introduced the term Representational State Transfer (REST) in 2000. And it became widely used when applied to web services - so called RESTful web services.
RESTful web services are characterized by:
Typical URL schmes look like:
Operations:
HTTP method | Operation (CRUD) | Type | Idempotent | Safe/cacheable | Request body | Response body | Note |
---|---|---|---|---|---|---|---|
GET | Read | Query | Yes | Yes | Data | ||
POST | Create | Update | No | No | Data | Data (copy of request data with information added server side) | |
PUT | Update | Update | Yes | No | Data | ||
DELETE | Delete | Update | Yes | No | |||
PATCH | Update | Update | Yes | No | Somewhat problematic. HTTP method is not defined in HTTP standard itself but in addendum and not supported by all servers and proxies. RESTful semantics vary between implementations. |
Data is typical sent as XML or JSON.
Examples will use the following wire protocol for XML:
Operation (CRUD) | Request | Response |
---|---|---|
Read - one | GET /t1/123 Accept: application/xml |
Status: 200 Content-Type: application/xml <t1><f1>123</f1><f2>getOne</f2></t1> |
Read - all | GET /t1 Accept: application/xml |
Status: 200 Content-Type: application/xml <t1array><t1><f1>1</f1><f2>getAll #1</f2></t1><t1><f1>2</f1><f2>getAll #2</f2></t1><t1><f1>3</f1><f2>getAll #3</f2></t1></t1array> |
Read - some | GET /t1?start=10&finish=12 Accept: application/xml |
Status: 200 Content-Type: application/xml <t1array><t1><f1>10</f1><f2>getSome #10</f2></t1><t1><f1>11</f1><f2>getSome #11</f2></t1><t1><f1>12</f1><f2>getSome #12</f2></t1></t1array> |
Create | POST /t1 Accept: application/xml Content-Type: application/xml <t1><f1>123</f1><f2>ABC</f2></t1> |
Status: 200 Content-Type: application/xml <t1><f1>123</f1><f2>ABC</f2></t1> |
Update | PUT /t1/123 Content-Type: application/xml <t1><f1>123</f1><f2>ABC</f2></t1> |
Status: 204 |
Delete | DELETE /t1/123 | Status: 204 |
and for JSON:
Operation (CRUD) | Request | Response |
---|---|---|
Read - one | GET /t1/123 Accept: application/json |
Status: 200 Content-Type: application/json {"f1":123,"f2":"getOne"} |
Read - all | GET /t1 Accept: application/json |
Status: 200 Content-Type: application/json [{"f1":1,"f2":"getAll #1"},{"f1":2,"f2":"getAll #2"},{"f1":3,"f2":"getAll #3"}] |
Read - some | GET /t1?start=10&finish=12 Accept: application/json |
Status: 200 Content-Type: application/json [{"f1":10,"f2":"getSome #10"},{"f1":11,"f2":"getSome #11"},{"f1":12,"f2":"getSome #12"}] |
Create | POST /t1 Accept: application/json Content-Type: application/json {"f1":123,"f2":"ABC"} |
Status: 200 Content-Type: application/json {"f1":123,"f2":"ABC"} |
Update | PUT /t1/123 Content-Type: application/json {"f1":123,"f2":"ABC"} |
Status: 204 |
Delete | DELETE /t1/123 | Status: 204 |
All examples support both XML and JSON and expect client to explicit specify which.
The examples are trivial but should be sufficient to illustrate many points.
For more information about XML and JSON see:
With hand coded solution the programmer is responsible for:
This result in much more code compared to using a framework.
Therefore hand coded only makes sense when one of:
Java servlet is the basis of all Java web technology and has existed since 1996.
This example use JAXB for XML serialization/deserialization and Google GSON for JSON serialization/deserialization.
T1.java:
package ws.rest;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
}
T1Array.java:
package ws.rest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getT1array(){
return t1array;
}
public void setT1array(T1[] array) {
this.t1array = array;
}
}
Test.java:
package ws.rest;
public class Test {
public T1 getOne(int f1) {
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] getAll() {
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] getSome(int start, int finish) {
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.length; i++) {
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public boolean save(T1 o) {
// dummy implementation
return o.getF1() > 0 && o.getF2().length() > 0;
}
public boolean delete(int f1) {
// dummy implementation
return f1 > 0;
}
}
Util.java:
package ws.rest.basic;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
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.Unmarshaller;
import com.google.gson.Gson;
public class Util {
public static String Object2XML(Object o) throws IOException {
StringWriter sw = new StringWriter();
try {
JAXBContext jxbctx = JAXBContext.newInstance(o.getClass());
Marshaller m = jxbctx.createMarshaller();
m.marshal(o, sw);
} catch (JAXBException e) {
throw new IOException("JAXB exception: " + e.getMessage(), e);
}
return sw.toString();
}
@SuppressWarnings("unchecked")
public static <T> T XML2Object(Class<T> clz, String xml) throws IOException {
try {
JAXBContext jxbctx = JAXBContext.newInstance(clz);
Unmarshaller um = jxbctx.createUnmarshaller();
return (T)um.unmarshal(new StringReader(xml));
} catch (JAXBException e) {
throw new IOException("JAXB exception: " + e.getMessage(), e);
}
}
public static String Object2JSON(Object o) throws IOException {
Gson g = new Gson();
return g.toJson(o);
}
public static <T> T JSON2Object(Class<T> clz, String json) {
Gson g = new Gson();
return g.fromJson(json, clz);
}
public static String Object2String(Object o, String typ) throws IOException {
if(typ.equals("application/xml")) {
return Object2XML(o);
} else if(typ.equals("application/json")) {
return Object2JSON(o);
} else {
throw new IOException("Unknown type: " + typ);
}
}
public static <T> T String2Object(Class<T> clz, String s, String typ) throws IOException {
if(typ.equals("application/xml")) {
return XML2Object(clz, s);
} else if(typ.equals("application/json")) {
return JSON2Object(clz, s);
} else {
throw new IOException("Unknown type: " + typ);
}
}
public static String getAllLines(Reader rdr) throws IOException {
StringBuffer sb = new StringBuffer();
BufferedReader br = new BufferedReader(rdr);
String line;
while((line = br.readLine()) != null) {
sb.append(line);
sb.append(System.getProperty("line.separator"));
}
return sb.toString();
}
}
TestServlet.java:
package ws.rest.basic;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;
@WebServlet(urlPatterns={"/testapi1/*"})
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static String getPathQuery(HttpServletRequest request) {
String reqinfo = request.getPathInfo();
if(request.getQueryString() != null) {
reqinfo += ("?" + request.getQueryString());
}
return reqinfo;
}
private static void writeResponse(HttpServletRequest request, HttpServletResponse response, Object result) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
String acctyp = request.getHeader("Accept");
response.setContentType(acctyp);
response.getWriter().print(Util.Object2String(result, acctyp));
}
private static <T> T readRequestBody(Class<T> clz, HttpServletRequest request) throws IOException, ServletException {
String contyp = request.getContentType().split(";")[0];
return Util.String2Object(clz, Util.getAllLines(request.getReader()), contyp);
}
private static final Pattern getOne = Pattern.compile("^/t1/(\\d+)$");
private static final Pattern getAll = Pattern.compile("^/t1$");
private static final Pattern getSome = Pattern.compile("^/t1\\?start=(\\d+)&finish=(\\d+)$");
private Test real = new Test();
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
////response.setHeader("Access-Control-Allow-Origin", "*");
String reqinfo = getPathQuery(request);
Matcher mone = getOne.matcher(reqinfo);
Matcher mall = getAll.matcher(reqinfo);
Matcher msome = getSome.matcher(reqinfo);
if(mone.find()) {
int f1 = Integer.parseInt(mone.group(1));
T1 result = real.getOne(f1);
writeResponse(request, response, result);
} else if(mall.find()) {
T1[] result = real.getAll();
if(request.getHeader("Accept").equals("application/xml"))
writeResponse(request, response, new T1Array(result));
else
writeResponse(request, response, result);
} else if(msome.find()) {
int start = Integer.parseInt(msome.group(1));
int finish = Integer.parseInt(msome.group(2));
T1[] result = real.getSome(start, finish);
if(request.getHeader("Accept").equals("application/xml"))
writeResponse(request, response, new T1Array(result));
else
writeResponse(request, response, result);
} else {
throw new ServletException("Invalid URL: " + reqinfo);
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
////response.setHeader("Access-Control-Allow-Origin", "*");
String reqinfo = getPathQuery(request);
Matcher mall = getAll.matcher(reqinfo);
if(mall.find()) {
T1 o = readRequestBody(T1.class, request);
if(real.save(o)) {
writeResponse(request, response, o);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND, o.getF1() + " not found");
}
} else {
throw new ServletException("Invalid URL: " + reqinfo);
}
}
@Override
public void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
////response.setHeader("Access-Control-Allow-Origin", "*");
String reqinfo = getPathQuery(request);
Matcher mone = getOne.matcher(reqinfo);
if(mone.find()) {
int f1 = Integer.parseInt(mone.group(1));
T1 o = readRequestBody(T1.class, request);
if(o.getF1() == f1 && real.save(o)) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND, f1 + " not found");
}
} else {
throw new ServletException("Invalid URL: " + reqinfo);
}
}
@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
////response.setHeader("Access-Control-Allow-Origin", "*");
String reqinfo = getPathQuery(request);
Matcher mone = getOne.matcher(reqinfo);
if(mone.find()) {
int f1 = Integer.parseInt(mone.group(1));
if(real.delete(f1)) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND, f1 + " not found");
}
} else {
throw new ServletException("Invalid URL: " + reqinfo);
}
}
////@Override
////public void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//// response.setHeader("Access-Control-Allow-Origin", "*");
//// response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
////}
}
Method is handled via servlet doXxxx methods. Regex is used to match URL.
The //// lines are needed to enable CORS.
ASP.NET web handler (.ashx) is a core part of ASP.NET and has existed since 2002. It is the equivalent of a Java servlet.
Surprisingly few ASP.NET developers know about it. Even though it can be very convenient for handling special cases - as ASP.NET web pages (.aspx) has way too much going on behind the scene.
This example use XmlSerializer for XML serialization/deserialization and JavaScriptSerializer for JSON serialization/deserialization.
TestHandler.cs:
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Script.Serialization;
using System.Xml.Serialization;
namespace Ws.Rest.ASHX
{
[XmlRoot(ElementName="t1")]
public class T1
{
public int f1 { get; set; }
public string f2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.f1 = f1;
this.f2 = f2;
}
}
[XmlRoot(ElementName="t1array")]
public class T1Array
{
[XmlElement(ElementName="t1")]
public T1[] t1array { get; set; }
public T1Array() : this(new T1[0])
{
}
public T1Array(T1[] array)
{
this.t1array = array;
}
}
public class Test
{
public T1 GetOne(int f1)
{
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] GetAll()
{
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] GetSome(int start, int finish)
{
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.Length; i++)
{
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public bool Save(T1 o)
{
// dummy implementation
return o.f1 > 0 && o.f2.Length > 0;
}
public bool Delete(int f1)
{
// dummy implementation
return f1 > 0;
}
}
public static class Util
{
public static string Object2XML(object o)
{
XmlSerializer ser = new XmlSerializer(o.GetType());
using(StringWriter sw = new StringWriter())
{
ser.Serialize(sw, o);
return sw.ToString().Replace("utf-16", "utf-8"); // StringWriter end up with "utf-16" which is not good;
}
}
public static T XML2Object<T>(string xml)
{
XmlSerializer ser = new XmlSerializer(typeof(T));
using(StringReader sr = new StringReader(xml))
{
return (T)ser.Deserialize(sr);
}
}
public static string Object2JSON(object o)
{
JavaScriptSerializer ser = new JavaScriptSerializer();
StringBuilder sb = new StringBuilder();
ser.Serialize(o, sb);
return sb.ToString();
}
public static T JSON2Object<T>(string xml)
{
JavaScriptSerializer ser = new JavaScriptSerializer();
return (T)ser.Deserialize(xml, typeof(T));
}
public static string Object2String(object o, string typ)
{
if(typ == "application/xml")
{
return Util.Object2XML(o);
}
else if(typ == "application/json")
{
return Util.Object2JSON(o);
}
else
{
throw new Exception("Unknown type: " + typ);
}
}
public static T String2Object<T>(string s, string typ)
{
if(typ == "application/xml")
{
return Util.XML2Object<T>(s);
}
else if(typ == "application/json")
{
return Util.JSON2Object<T>(s);
}
else
{
throw new Exception("Unknown type: " + typ);
}
}
public static string GetAllLines(Stream stm)
{
using(StreamReader sr = new StreamReader(stm))
{
return sr.ReadToEnd();
}
}
}
public class TestHandler : IHttpHandler
{
private static string GetPathQuery(HttpRequest request)
{
string reqinfo = request.RawUrl;
int ix = reqinfo.IndexOf(".ashx");
reqinfo = reqinfo.Substring(ix + ".ashx".Length);
return reqinfo;
}
private static void WriteResponse(HttpRequest request, HttpResponse response, object result)
{
response.StatusCode = (int)HttpStatusCode.OK;
string acctyp = request.AcceptTypes[0];
response.ContentType = acctyp;
if(acctyp == "application/xml")
{
response.Write(Util.Object2XML(result));
}
else if(acctyp == "application/json")
{
response.Write(Util.Object2JSON(result));
}
else
{
throw new Exception("Invalid accept type: " + acctyp);
}
}
private static T ReadRequestBody<T>(HttpRequest request)
{
string contyp = request.ContentType.Split(';')[0];
if(contyp == "application/xml")
{
return Util.XML2Object<T>(Util.GetAllLines(request.InputStream));
}
else if(contyp == "application/json")
{
return Util.JSON2Object<T>(Util.GetAllLines(request.InputStream));
}
else
{
throw new Exception("Invalid content type: " + contyp);
}
}
private readonly Regex getOne = new Regex("^/t1/(\\d+)$");
private readonly Regex getAll = new Regex("^/t1$");
private readonly Regex getSome = new Regex("^/t1\\?start=(\\d+)&finish=(\\d+)$");
private Test real = new Test();
public void ProcessRequest (HttpContext ctx)
{
HttpRequest request = ctx.Request;
HttpResponse response = ctx.Response;
////response.Headers.Add("Access-Control-Allow-Origin", "*");
string reqinfo = GetPathQuery(request);
Match mone = getOne.Match(reqinfo);
Match mall = getAll.Match(reqinfo);
Match msome = getSome.Match(reqinfo);
switch(request.HttpMethod)
{
case "GET":
if(mone.Success)
{
int f1 = int.Parse(mone.Groups[1].Value);
T1 result = real.GetOne(f1);
WriteResponse(request, response, result);
}
else if(mall.Success)
{
T1[] result = real.GetAll();
if(request.AcceptTypes[0] == "application/xml")
WriteResponse(request, response, new T1Array(result));
else
WriteResponse(request, response, result);
}
else if(msome.Success)
{
int start = int.Parse(msome.Groups[1].Value);
int finish = int.Parse(msome.Groups[2].Value);
T1[] result = real.GetSome(start, finish);
if(request.AcceptTypes[0] == "application/xml")
WriteResponse(request, response, new T1Array(result));
else
WriteResponse(request, response, result);
}
else
{
throw new Exception("Invalid URL: " + reqinfo);
}
break;
case "POST":
if(mall.Success)
{
T1 o = ReadRequestBody<T1>(request);
if(real.Save(o))
{
WriteResponse(request, response, o);
}
else
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.StatusDescription = o.f1 + " not found";
}
}
else
{
throw new Exception("Invalid URL: " + reqinfo);
}
break;
case "PUT":
if(mone.Success)
{
int f1 = int.Parse(mone.Groups[1].Value);
T1 o = ReadRequestBody<T1>(request);
if(o.f1 == f1 && real.Save(o))
{
response.StatusCode = (int)HttpStatusCode.NoContent;
}
else
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.StatusDescription = f1 + " not found";
}
}
else
{
throw new Exception("Invalid URL: " + reqinfo);
}
break;
case "DELETE":
if(mone.Success)
{
int f1 = int.Parse(mone.Groups[1].Value);
if(real.Delete(f1))
{
response.StatusCode = (int)HttpStatusCode.NoContent;
}
else
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.StatusDescription = f1 + " not found";
}
}
else
{
throw new Exception("Invalid URL: " + reqinfo);
}
break;
////case "OPTIONS":
//// response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
//// response.StatusCode = (int)HttpStatusCode.NoContent;
//// break;
}
}
public bool IsReusable
{
get { return true; }
}
}
}
Method is handled manually. Regex is used to match URL.
The //// lines are needed to enable CORS.
testapi.ashx:
<%@ WebHandler Language="C#" Class="Ws.Rest.ASHX.TestHandler" %>
It may also be necessary to add the following web.config fragment to get all HTTP methods forwarded properly:
<system.webServer>
<handlers>
<remove name="WebDAV" />
<remove name="ExtensionlessUrlHandler-Integrated-4.0"/>
<remove name="SimpleHandlerFactory-Integrated"/>
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,POST,PUT,DELETE,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="SimpleHandlerFactory-Integrated" path="*.ashx" verb="GET,POST,PUT,DELETE,OPTIONS" type="System.Web.UI.SimpleHandlerFactory" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode" />
</handlers>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule" />
</modules>
</system.webServer>
This example use custom serialization/deserialization.
There are a lot of code for the custom serialization/deserialization, but it is not really important for understanding the web service part, so I recommend not spending too much time on that custom serialization/deserialization code. If format is limited to JSON and whatever format json_encode/json_decode use is good enough, then it all goes away.
testapi.php:
<?php
/*
* Generic serializer/deserializer framework.
* It is not tested/robust enough for production usage.
*/
/*
interface Formatter {
public function fieldPrefix($nam);
public function fieldValue($nam, $val);
public function fieldSeparator();
public function fieldSuffix($nam);
public function arrayPrefix($nam);
public function arrayValue($val);
public function arraySeparator();
public function arraySuffix($nam);
}
class JSONFormatter implements Formatter {
public function fieldPrefix($nam) {
return '{';
}
public function fieldValue($nam, $val) {
if(substr($val, 0, 1) == '{' || substr($val, 0, 1) == '[' || is_int($val)) {
return sprintf('"%s":%s', $nam, $val);
} else {
return sprintf('"%s":"%s"', $nam, $val);
}
}
public function fieldSeparator() {
return ',';
}
public function fieldSuffix($nam) {
return '}';
}
public function arrayPrefix($nam) {
return '[';
}
public function arrayValue($val) {
return $val;
}
public function arraySeparator() {
return ',';
}
public function arraySuffix($nam) {
return ']';
}
}
class XMLFormatter implements Formatter {
public function fieldPrefix($nam) {
return sprintf('<%s>', $nam);
}
public function fieldValue($nam, $val) {
return sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
public function fieldSeparator() {
return '';
}
public function fieldSuffix($nam) {
return sprintf('</%s>', $nam);
}
public function arrayPrefix($nam) {
return sprintf('<%s>', $nam);
}
public function arrayValue($val) {
return $val;
}
public function arraySeparator() {
return '';
}
public function arraySuffix($nam) {
return sprintf('</%s>', $nam);
}
}
class Util {
private static function Object2Anything($o, $fmt) {
$clz = strtolower(get_class($o));
if(substr($clz, -5) == 'array' && isset($o->array)) {
$res = $fmt->arrayPrefix($clz);
foreach($o->array as $elm) {
if(strlen($res) > 1) $res .= $fmt->arraySeparator();
$res .= $fmt->arrayValue(Util::Object2Anything($elm, $fmt));
}
$res .= $fmt->arraySuffix($clz);
return $res;
} else {
$res = $fmt->fieldPrefix($clz);
foreach(get_object_vars($o) as $nam => $val) {
if(strlen($res) > 1) $res .= $fmt->fieldSeparator();
$res .= $fmt->fieldValue($nam, is_object($val) ? Util::Object2Anything($val, $fmt) : $val);
}
$res .= $fmt->fieldSuffix($clz);
return $res;
}
}
public static function Object2JSON($o) {
return Util::Object2Anything($o, new JSONFormatter());
}
public static function Object2XML($o) {
return Util::Object2Anything($o, new XMLFormatter());
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
private static function mapFields(&$fromobj, &$toobj) {
foreach(get_object_vars($toobj) as $nam => $val) {
if(is_object($toobj->$nam) || is_array($toobj->$nam)) {
Util::map($fromobj->$nam, $toobj->$nam);
} else {
if(is_int($toobj->$nam)) {
$toobj->$nam = (int)$fromobj->$nam;
} else {
$toobj->$nam = (string)$fromobj->$nam;
}
}
}
}
private static function mapArray(&$fromarr, &$toarr, $arrelmclz) {
foreach($fromarr as $elm) {
$temp = new $arrelmclz();
Util::map($elm, $temp);
$toarr[] = $temp;
}
}
private static function map(&$fromobj, &$toobj) {
$clz = strtolower(get_class($toobj));
if(substr($clz, -5) == 'array' && isset($toobj->array)) {
$arrelmclz = substr(get_class($toobj), 0 , -5);
$temp = $fromobj;
if(isset($fromobj->array)) $temp = $fromobj->array;
$f1 = strtolower($arrelmclz . 'array');
$f2 = strtolower($arrelmclz);
if(isset($fromobj->$f1)) $temp = $fromobj->$f1->$f2;
Util::mapArray($temp, $toobj->array, $arrelmclz);
} else {
Util::mapFields($fromobj, $toobj);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
Util::map($temp, $res);
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
Util::map($temp, $res);
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
*/
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
$clz = get_class($o);
switch($clz) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
class Test {
public function getOne($f1) {
// dummy implementation
return new T1($f1, 'getOne');
}
public function getAll() {
// dummy implementation
$res = array();
$res[] = new T1(1, 'getAll #1');
$res[] = new T1(2, 'getAll #2');
$res[] = new T1(3, 'getAll #3');
return $res;
}
public function getSome($start, $finish) {
// dummy implementation
$res = array();
for($i = 0; $i < $finish - $start + 1; $i++) {
$res[] = new T1($start + $i, 'getSome #' . ($start + $i));
}
return $res;
}
public function save($o) {
// dummy implementation
return $o->f1 > 0 && strlen($o->f2) > 0;
}
public function delete($f1) {
// dummy implementation
return $f1 > 0;
}
}
class TestService {
private static function writeResponse($result) {
$acctyp = $_SERVER['HTTP_ACCEPT'];
header('Content-Type: ' . $acctyp);
header('HTTP/1.1 200 OK');
echo Util::Object2String($result, $acctyp);
}
private static function readRequestBody($clz) {
$contyp = $_SERVER['CONTENT_TYPE'];
$contyp = explode(';', $contyp)[0];
$body = file_get_contents("php://input");
return Util::String2Object($clz, $body, $contyp);
}
public function process() {
$real = new Test();
////header("Access-Control-Allow-Origin: *");
$reqinfo = $_SERVER['PATH_INFO'];
if(isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0) {
$reqinfo .= ('?' . $_SERVER['QUERY_STRING']);
}
switch($_SERVER['REQUEST_METHOD']) {
case 'GET':
if(preg_match('#^/t1/(\\d+)$#', $reqinfo, $mone)) {
$f1 = (int)$mone[1];
$result = $real->getOne($f1);
$this->writeResponse($result);
} else if(preg_match('#^/t1$#', $reqinfo, $mall)) {
$result = $real->getAll();
$this->writeResponse(new T1Array($result));
} else if(preg_match('#^/t1\\?start=(\\d+)&finish=(\\d+)$#', $reqinfo, $msome)) {
$start = (int)$msome[1];
$finish = (int)$msome[2];
$result = $real->getSome($start, $finish);
$this->writeResponse(new T1Array($result));
} else {
throw new Exception('Invalid URL: ' . $reqinfo);
}
break;
case 'POST':
if(preg_match('#^/t1$#', $reqinfo, $mall)) {
$o = $this->readRequestBody('T1');
if($real->save($o)) {
$this->writeResponse($o);
} else {
header('HTTP/1.1 404 Not found');
}
} else {
throw new Exception('Invalid URL: ' . $reqinfo);
}
break;
case 'PUT':
if(preg_match('#^/t1/(\\d+)$#', $reqinfo, $mone)) {
$f1 = (int)$mone[1];
$o = $this->readRequestBody('T1');
if($o->f1 == $f1 && $real->save($o)) {
header('HTTP/1.1 204 No content');
} else {
header('HTTP/1.1 404 Not found');
}
} else {
throw new Exception('Invalid URL: ' . $reqinfo);
}
break;
case 'DELETE':
if(preg_match('#^/t1/(\\d+)$#', $reqinfo, $mone)) {
$f1 = (int)$mone[1];
if($real->delete($f1)) {
header('HTTP/1.1 204 No content');
} else {
header('HTTP/1.1 404 Not found');
}
} else {
throw new Exception('Invalid URL: ' + $reqinfo);
}
break;
////case 'OPTIONS':
//// header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
//// header("Access-Control-Allow-Headers: Content-Type, Accept");
//// break;
}
}
}
$srv = new TestService();
$srv->process();
?>
Method is handled manually. Regex is used to match URL.
The //// lines are needed to enable CORS.
With framework solution the framework is handling:
based on one of:
Especially configuration by annotations/attributes has become popular.
This result in much less code compared to hand coded.
JAX-RS is the Java standard for RESTful web services. JAX-RS was introduced 2008.
There are multiple JAX-RS implementations. Two of the most well known are:
If you are using a full Java EE application server then there will be a JAX-RS implementation available by default.
T1.java:
package ws.rest;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
}
T1Array.java:
package ws.rest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getT1array(){
return t1array;
}
public void setT1array(T1[] array) {
this.t1array = array;
}
}
Test.java:
package ws.rest;
public class Test {
public T1 getOne(int f1) {
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] getAll() {
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] getSome(int start, int finish) {
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.length; i++) {
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public boolean save(T1 o) {
// dummy implementation
return o.getF1() > 0 && o.getF2().length() > 0;
}
public boolean delete(int f1) {
// dummy implementation
return f1 > 0;
}
}
LoadTestService.java:
package ws.rest.jaxrs;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/testapi2")
public class LoadTestService extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> res = new HashSet<Class<?>>();
res.add(TestService.class);
return res;
}
}
The above code defines which resource handling classes are available on the path specified with @ApplicationPath.
TestService.java:
package ws.rest.jaxrs;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
////import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;
@Path("/t1")
public class TestService {
@Context
private HttpServletResponse response;
Test real = new Test();
@GET
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Path("/{f1}")
public T1 getOne(@PathParam("f1") int f1) {
////response.setHeader("Access-Control-Allow-Origin", "*");
return real.getOne(f1);
}
// there is no getAll as it is processed as getSome with default query parameters
@GET
@Produces({MediaType.APPLICATION_XML})
@Path("")
public T1Array getSomeXml(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
////response.setHeader("Access-Control-Allow-Origin", "*");
if(start == 0 && finish == 2147483647) {
return new T1Array(real.getAll());
} else {
return new T1Array(real.getSome(start, finish));
}
}
@GET
@Produces({MediaType.APPLICATION_JSON})
@Path("")
public T1[] getSomeJson(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
////response.setHeader("Access-Control-Allow-Origin", "*");
if(start == 0 && finish == 2147483647) {
return real.getAll();
} else {
return real.getSome(start, finish);
}
}
@POST
@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Path("")
public T1 save(T1 o) {
////response.setHeader("Access-Control-Allow-Origin", "*");
if(real.save(o)) {
return o;
} else {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
@PUT
@Path("/{f1}")
@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
public void update(@PathParam("f1") int f1, T1 o) {
response.setHeader("Access-Control-Allow-Origin", "*");
if(o.getF1() == f1 && real.save(o)) {
} else {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
@DELETE
@Path("/{f1}")
public void delete(@PathParam("f1") int f1) {
////response.setHeader("Access-Control-Allow-Origin", "*");
if(real.delete(f1)) {
} else {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
////@OPTIONS
////@Path("")
////public void options1() {
//// response.setHeader("Access-Control-Allow-Origin", "*");
//// response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
////}
////@OPTIONS
////@Path("/{f1}")
////public void options2(@PathParam("f1") int f1) {
//// response.setHeader("Access-Control-Allow-Origin", "*");
//// response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
////}
}
@Path defines the path for the resource and additional path for the methods.
@GET, @POST, @PUT and @DELETE defines what methods to call for what HTTP method.
@Produces defines what format to use in response.
@Consumes defines what format to use in request.
The //// lines are needed to enable CORS.
This alternate version handle response and HTTP error codes differently.
T1.java:
package ws.rest;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
}
T1Array.java:
package ws.rest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getT1array(){
return t1array;
}
public void setT1array(T1[] array) {
this.t1array = array;
}
}
Test.java:
package ws.rest;
public class Test {
public T1 getOne(int f1) {
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] getAll() {
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] getSome(int start, int finish) {
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.length; i++) {
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public boolean save(T1 o) {
// dummy implementation
return o.getF1() > 0 && o.getF2().length() > 0;
}
public boolean delete(int f1) {
// dummy implementation
return f1 > 0;
}
}
LoadTestServiceAlt.java:
package ws.rest.jaxrs;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/testapi3")
public class LoadTestServiceAlt extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> res = new HashSet<Class<?>>();
res.add(TestServiceAlt.class);
return res;
}
}
TestServiceAlt.java:
package ws.rest.jaxrs;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
////import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;
@Path("/t1")
public class TestServiceAlt {
Test real = new Test();
@GET
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Path("/{f1}")
public Response getOne(@PathParam("f1") int f1) {
return Response.ok(real.getOne(f1))
////.header("Access-Control-Allow-Origin", "*")
.build();
}
// there is no getAll as it is processed as getSome with default query parameters
@GET
@Produces({MediaType.APPLICATION_XML})
@Path("")
public Response getSomeXml(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
if(start == 0 && finish == 2147483647) {
return Response.ok(new T1Array(real.getAll()))
////.header("Access-Control-Allow-Origin", "*")
.build();
} else {
return Response.ok(new T1Array(real.getSome(start, finish)))
////.header("Access-Control-Allow-Origin", "*")
.build();
}
}
@GET
@Produces({MediaType.APPLICATION_JSON})
@Path("")
public Response getSomeJson(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
if(start == 0 && finish == 2147483647) {
return Response.ok(real.getAll())
////.header("Access-Control-Allow-Origin", "*")
.build();
} else {
return Response.ok(real.getSome(start, finish))
////.header("Access-Control-Allow-Origin", "*")
.build();
}
}
@POST
@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Path("")
public Response save(T1 o) {
if(real.save(o)) {
return Response.ok(o)
////.header("Access-Control-Allow-Origin", "*")
.build();
} else {
return Response.status(Response.Status.NOT_FOUND)
////.header("Access-Control-Allow-Origin", "*")
.build();
}
}
@PUT
@Path("/{f1}")
@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
public Response update(@PathParam("f1") int f1, T1 o) {
if(o.getF1() == f1 && real.save(o)) {
return Response.noContent()
////.header("Access-Control-Allow-Origin", "*")
.build();
} else {
return Response.status(Response.Status.NOT_FOUND)
////.header("Access-Control-Allow-Origin", "*")
.build();
}
}
@DELETE
@Path("/{f1}")
public Response delete(@PathParam("f1") int f1) {
if(real.delete(f1)) {
return Response.noContent()
////.header("Access-Control-Allow-Origin", "*")
.build();
} else {
return Response.status(Response.Status.NOT_FOUND)
////.header("Access-Control-Allow-Origin", "*")
.build();
}
}
////@OPTIONS
////@Path("")
////public Response options1() {
//// return Response.noContent().header("Access-Control-Allow-Origin", "*")
//// .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
//// .header("Access-Control-Allow-Headers", "Content-Type, Accept").build();
////}
////@OPTIONS
////@Path("/{f1}")
////public Response options2(@PathParam("f1") int f1) {
//// return Response.noContent().header("Access-Control-Allow-Origin", "*")
//// .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
//// .header("Access-Control-Allow-Headers", "Content-Type, Accept").build();
////}
}
Spring is a suite of very widely used Java frameworks. Spring MVC is Spring's web framework. And Spring MVC also supports RESTful web services.
T1.java:
package ws.rest;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
}
T1Array.java:
package ws.rest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getT1array(){
return t1array;
}
public void setT1array(T1[] array) {
this.t1array = array;
}
}
Test.java:
package ws.rest;
public class Test {
public T1 getOne(int f1) {
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] getAll() {
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] getSome(int start, int finish) {
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.length; i++) {
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public boolean save(T1 o) {
// dummy implementation
return o.getF1() > 0 && o.getF2().length() > 0;
}
public boolean delete(int f1) {
// dummy implementation
return f1 > 0;
}
}
TestInitializer.java:
package ws.rest.spring;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class TestInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return new Class[] { TestConfiguration.class };
}
@Override
protected Class>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/testapi4/*" };
}
}
The above code specifies configuartion for the path.
TestConfiguration.java:
package ws.rest.spring;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "ws.rest")
public class TestConfiguration {
}
The above code specified what packages to scan for annotations for configuration.
TestController.java:
package ws.rest.spring;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
////import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;
////@CrossOrigin(origins="*",methods={RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE,RequestMethod.OPTIONS},allowedHeaders={"Content-Type","Accept"})
@RestController
@RequestMapping(value="/t1")
public class TestController {
private Test real = new Test();
@RequestMapping(value="/{f1}",method=RequestMethod.GET,produces={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<T1> getOne(@PathVariable("f1") int f1) {
return new ResponseEntity<T1>(real.getOne(f1), HttpStatus.OK);
}
// there is no getAll as it is processed as getSome with default query parameters
@RequestMapping(value="",method=RequestMethod.GET,produces={MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<T1Array> getSomeXml(@RequestParam(value="start",defaultValue="0") int start, @RequestParam(value="finish",defaultValue="2147483647") int finish) {
if(start == 0 && finish == 2147483647) {
return new ResponseEntity<T1Array>(new T1Array(real.getAll()), HttpStatus.OK);
} else {
return new ResponseEntity<T1Array>(new T1Array(real.getSome(start, finish)), HttpStatus.OK);
}
}
@RequestMapping(value="",method=RequestMethod.GET,produces={MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<T1[]> getSomeJson(@RequestParam(value="start",defaultValue="0") int start, @RequestParam(value="finish",defaultValue="2147483647") int finish) {
if(start == 0 && finish == 2147483647) {
return new ResponseEntity<T1[]>(real.getAll(), HttpStatus.OK);
} else {
return new ResponseEntity<T1[]>(real.getSome(start, finish), HttpStatus.OK);
}
}
@RequestMapping(value="",method=RequestMethod.POST,produces={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE},consumes={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<T1> save(@RequestBody T1 o) {
if(real.save(o)) {
return new ResponseEntity<T1>(o, HttpStatus.OK);
} else {
return new ResponseEntity<T1>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value="/{f1}",method=RequestMethod.PUT,consumes={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<Void> update(@PathVariable("f1") int f1, @RequestBody T1 o) {
if(o.getF1() == f1 && real.save(o)) {
return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value="/{f1}",method=RequestMethod.DELETE)
public ResponseEntity<Void> delete(@PathVariable("f1") int f1) {
if(real.delete(f1)) {
return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
}
}
}
@RestController defines it as a RESTful web service.
@RequestMapping defines path, HTTP method and format for methods.
The //// lines are needed to enable CORS.
Note that enabling CORS is extremely easy.
WCF (Windows Communication Foundation) is a very general web service framework and was introduced in 2006. In 2007 support for RESTful web services was added.
TestService.cs:
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
namespace Ws.Rest.WCF
{
[DataContract(Name="t1",Namespace="")]
public class T1
{
[DataMember(Name="f1")]
public int F1 { get; set; }
[DataMember(Name="f2")]
public string F2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.F1 = f1;
this.F2 = f2;
}
}
[CollectionDataContract(Name="t1array",Namespace="")]
public class T1Array : List<T1>
{
public T1Array() : this(new T1[0])
{
}
public T1Array(T1[] array) : base(array)
{
}
}
public class Test
{
public T1 GetOne(int f1)
{
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] GetAll()
{
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] GetSome(int start, int finish)
{
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.Length; i++)
{
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public bool Save(T1 o)
{
// dummy implementation
return o.F1 > 0 && o.F2.Length > 0;
}
public bool Delete(int f1)
{
// dummy implementation
return f1 > 0;
}
}
[ServiceContract]
public interface ITestService
{
[OperationContract]
[WebInvoke(UriTemplate="/t1/{f1}",Method="GET",BodyStyle=WebMessageBodyStyle.Bare)]
T1 GetOne(string f1);
// there is no GetAll as it is processed as GetSome with null query parameters
[OperationContract]
[WebInvoke(UriTemplate="/t1?start={start}&finish={finish}",Method="GET",RequestFormat=WebMessageFormat.Xml,BodyStyle=WebMessageBodyStyle.Bare)]
T1Array GetSome(string start, string finish);
[OperationContract]
[WebInvoke(UriTemplate="/t1",Method="POST",BodyStyle=WebMessageBodyStyle.Bare)]
T1 Save(T1 o);
[OperationContract]
[WebInvoke(UriTemplate="/t1/{f1}",Method="PUT")]
void Update(string f1, T1 o);
[OperationContract]
[WebInvoke(UriTemplate="/t1/{f1}",Method="DELETE")]
void Delete(string f1);
////[WebInvoke(UriTemplate="/t1",Method="OPTIONS")]
////void Option1();
////[WebInvoke(UriTemplate="/t1/{f1}",Method="OPTIONS")]
////void Option2(string f1);
}
[ServiceBehavior]
public class TestService : ITestService
{
private Test real = new Test();
public T1 GetOne(string f1)
{
////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
return real.GetOne(int.Parse(f1));
}
public T1Array GetSome(string start, string finish)
{
////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
if(start == null && finish == null)
{
return new T1Array(real.GetAll());
}
else
{
return new T1Array(real.GetSome(int.Parse(start), int.Parse(finish)));
}
}
public T1 Save(T1 o)
{
////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
if(real.Save(o))
{
return o;
}
else
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
return null;
}
}
public void Update(string f1, T1 o)
{
////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
if(int.Parse(f1) == o.F1 && real.Save(o))
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
}
else
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
}
}
public void Delete(string f1)
{
////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
if(real.Delete(int.Parse(f1)))
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
}
else
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
}
}
////public void Option1()
////{
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
//// WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
////}
////public void Option2(string f1)
////{
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//// WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
//// WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
////}
}
}
[DataContract] and [DataMember] defines the data protocol.
[ServiceContract] and [OperationContract] defines the service.
[WebInvoke] defines methods for HTTP method and URL.
The //// lines are needed to enable CORS.
testapi.svc:
<%@ ServiceHost Factory="System.ServiceModel.Activation.WebServiceHostFactory" Service="Ws.Rest.WCF.TestService" %>
web.config fragment:
<system.serviceModel>
<services>
<service name="Ws.Rest.WCF.TestService">
<endpoint binding="webHttpBinding" contract="Ws.Rest.WCF.ITestService" behaviorConfiguration="ws"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="ws">
<webHttp automaticFormatSelectionEnabled="true"/>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
As with all other WCF services it need to be defined in config.
ASP.NET Web API is a much simpler and easier to use RESTful web service framework and was introduced in 2012. It is somewhat build on ASP.NET MVC.
Web API can be used in different ways, but I will show attribute based routing as I believe that is by far the best way.
TestApi.cs:
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.Serialization;
using System.Web.Http;
using System.Web.Http.Cors;
namespace Ws.Rest.WebApi
{
[DataContract(Name="t1",Namespace="")]
public class T1
{
[DataMember(Name="f1")]
public int F1 { get; set; }
[DataMember(Name="f2")]
public string F2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.F1 = f1;
this.F2 = f2;
}
}
[CollectionDataContract(Name="t1array",Namespace="")]
public class T1Array : List<T1>
{
public T1Array() : this(new T1[0])
{
}
public T1Array(T1[] array) : base(array)
{
}
}
public class Test
{
public T1 GetOne(int f1)
{
// dummy implementation
return new T1(f1, "getOne");
}
public T1[] GetAll()
{
// dummy implementation
T1[] res = new T1[3];
res[0] = new T1(1, "getAll #1");
res[1] = new T1(2, "getAll #2");
res[2] = new T1(3, "getAll #3");
return res;
}
public T1[] GetSome(int start, int finish)
{
// dummy implementation
T1[] res = new T1[finish - start + 1];
for(int i = 0; i < res.Length; i++)
{
res[i] = new T1(start + i, "getSome #" + (start + i));
}
return res;
}
public bool Save(T1 o)
{
// dummy implementation
return o.F1 > 0 && o.F2.Length > 0;
}
public bool Delete(int f1)
{
// dummy implementation
return f1 > 0;
}
}
[RoutePrefix("testapi/t1")]
////[EnableCors(origins: "*", headers: "Content-Type", methods: "GET,POST,PUT,DELETE,OPTIONS")]
public class TestApiController : ApiController
{
private Test real = new Test();
[HttpGet]
[Route("{f1:int}")]
public IHttpActionResult GetOne(int f1)
{
return Ok(real.GetOne(f1));
}
[HttpGet]
[Route("")]
public IHttpActionResult GetSome(int start = 0, int finish = 2147483647)
{
if(start == 0 && finish == 2147483647)
{
return Ok(new T1Array(real.GetAll()));
}
else
{
return Ok(new T1Array(real.GetSome(start, finish)));
}
}
[HttpPost]
[Route("")]
public IHttpActionResult Save(T1 o)
{
if(real.Save(o))
{
return Ok(o);
}
else
{
return NotFound();
}
}
[HttpPut]
[Route("{f1:int}")]
public IHttpActionResult Update(int f1, T1 o)
{
if(f1 == o.F1 && real.Save(o))
{
return StatusCode(HttpStatusCode.NoContent);
}
else
{
return NotFound();
}
}
[HttpDelete]
[Route("{f1:int}")]
public IHttpActionResult Delete(int f1)
{
if(real.Delete(f1))
{
return StatusCode(HttpStatusCode.NoContent);
}
else
{
return NotFound();
}
}
}
}
[DataContract] and [DataMember] defines the data protocol.
[RoutePrefix] and [Route] defines the path for the resource and additional path for the methods.
[HttpGet], [HttpPost], [HttpPut] and [HttpDelete] defines what methods to call for what HTTP method.
The //// lines are needed to enable CORS.
Note that enabling CORS is extremely easy.
global.asax fragment:
protected void Application_Start(object sender, EventArgs e)
{
GlobalConfiguration.Configure(config => {
////config.EnableCors();
config.MapHttpAttributeRoutes();
config.Formatters.Clear();
config.Formatters.Add(new JsonMediaTypeFormatter());
config.Formatters.Add(new XmlMediaTypeFormatter());
});
}
This enable attribute based routing and enable both JSON and XML format.
There are many frameworks available for PHP that supports writing RESTful web services. Including:
Limonade is an older framework from 2009. It is it procedural not object oriented and lacks a few features, but it is very simple to use.
There are a lot of code for the custom serialization/deserialization, but it is not really important for understanding the web service part, so I recommend not spending too much time on that custom serialization/deserialization code. If format is limited to JSON and whatever format json_encode/json_decode use is good enough, then it all goes away.
testapi_limonade.php:
<?php
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
switch(get_class($o)) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
class Test {
public function getOne($f1) {
// dummy implementation
return new T1($f1, 'getOne');
}
public function getAll() {
// dummy implementation
$res = array();
$res[] = new T1(1, 'getAll #1');
$res[] = new T1(2, 'getAll #2');
$res[] = new T1(3, 'getAll #3');
return $res;
}
public function getSome($start, $finish) {
// dummy implementation
$res = array();
for($i = 0; $i < $finish - $start + 1; $i++) {
$res[] = new T1($start + $i, 'getSome #' . ($start + $i));
}
return $res;
}
public function save($o) {
// dummy implementation
return $o->f1 > 0 && strlen($o->f2) > 0;
}
public function delete($f1) {
// dummy implementation
return $f1 > 0;
}
}
function read_request_body($clz) {
$contyp = $_SERVER['CONTENT_TYPE'];
$contyp = explode(';', $contyp)[0];
$body = file_get_contents("php://input");
return Util::String2Object($clz, $body, $contyp);
}
require_once 'lib/limonade.php';
////function before($route) {
//// header("Access-Control-Allow-Origin: *");
////}
dispatch_get('/t1', 'get_all_or_some');
function get_all_or_some() {
$acctyp = $_SERVER['HTTP_ACCEPT'];
$real = new Test();
if(isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0) {
parse_str($_SERVER['QUERY_STRING'], $q);
$a = $real->getSome($q['start'], $q['finish']);
} else {
$a = $real->getAll();
}
header('Content-Type: ' . $acctyp);
return Util::Object2String(new T1Array($a), $acctyp);
}
dispatch_get('/t1/:f1', 'get_one');
function get_one($f1) {
$acctyp = $_SERVER['HTTP_ACCEPT'];
$real = new Test();
$o = $real->getOne((int)$f1);
header('Content-Type: ' . $acctyp);
return Util::Object2String($o, $acctyp);
}
dispatch_post('/t1', 'save');
function save() {
$acctyp = $_SERVER['HTTP_ACCEPT'];
$o = read_request_body('T1');
$real = new Test();
if($real->save($o)) {
header('Content-Type: ' . $acctyp);
return Util::Object2String($o, $acctyp);
} else {
halt(404, 'Not found');
}
}
dispatch_put('/t1/:f1', 'update');
function update($f1) {
$o = read_request_body('T1');
$real = new Test();
if($o->f1 == $f1 && $real->save($o)) {
status(204);
} else {
halt(404, 'Not found');
}
}
dispatch_delete('/t1/:f1', 'delete');
function delete($f1) {
$real = new Test();
if($real->delete($f1)) {
status(204);
} else {
halt(404, 'Not found');
}
}
/////* NOTE: hack limonnade.php function request_methods by adding "OPTIONS" to array of valid methods. */
////route('OPTIONS', '**', 'options', array());
////function options() {
//// header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
//// header("Access-Control-Allow-Headers: Content-Type, Accept");
//// status(200);
////}
run();
?>
dispatch_xxxx call maps methos and path to a PHP function.
The //// lines are needed to enable CORS.
There are many frameworks available for PHP that supports writing RESTful web services. Including:
Slim is a framework from 2013. It is object oriented and has relative rich features, but it is still simple to use.
There are a lot of code for the custom serialization/deserialization, but it is not really important for understanding the web service part, so I recommend not spending too much time on that custom serialization/deserialization code. If format is limited to JSON and whatever format json_encode/json_decode use is good enough, then it all goes away.
testapi_slim.php:
<?php
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
switch(get_class($o)) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
class Test {
public function getOne($f1) {
// dummy implementation
return new T1($f1, 'getOne');
}
public function getAll() {
// dummy implementation
$res = array();
$res[] = new T1(1, 'getAll #1');
$res[] = new T1(2, 'getAll #2');
$res[] = new T1(3, 'getAll #3');
return $res;
}
public function getSome($start, $finish) {
// dummy implementation
$res = array();
for($i = 0; $i < $finish - $start + 1; $i++) {
$res[] = new T1($start + $i, 'getSome #' . ($start + $i));
}
return $res;
}
public function save($o) {
// dummy implementation
return $o->f1 > 0 && strlen($o->f2) > 0;
}
public function delete($f1) {
// dummy implementation
return $f1 > 0;
}
}
ini_set('include_path', '.;C:\DivNative\32bit\php-5.6.23-Win32-VC11-x86\vendor');
require 'autoload.php';
use \Slim\App;
$app = new App();
header("Access-Control-Allow-Origin: *");
////$app->add(function ($request, $response, $next) {
//// return $next($request, $response)->withHeader("Access-Control-Allow-Origin", "*");
////});
$app->get('/t1/{f1}', function($request, $response, $args) {
$acctyp = $request->getHeader('Accept')[0];
$f1 = (int)$args['f1'];
$real = new Test();
$o = $real->getOne($f1);
$response->getBody()->write(Util::Object2String($o, $acctyp));
return $response->withHeader('Content-Type', $acctyp);
});
$app->get('/t1', function($request, $response, $args) {
$acctyp = $request->getHeader('Accept')[0];
$real = new Test();
if(isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0) {
parse_str($_SERVER['QUERY_STRING'], $q);
$a = $real->getSome($q['start'], $q['finish']);
} else {
$a = $real->getAll();
}
$response->getBody()->write(Util::Object2String(new T1Array($a), $acctyp));
return $response->withHeader('Content-Type', $acctyp);
});
$app->post('/t1', function($request, $response, $args) {
$acctyp = $request->getHeader('Accept')[0];
$contyp = $request->getHeader('Content-Type')[0];
$o = Util::String2Object('T1', $request->getBody(), $contyp);
$real = new Test();
if($real->save($o)) {
$response->getBody()->write(Util::Object2String($o, $acctyp));
return $response->withHeader('Content-Type', $acctyp);
} else {
return $response->withStatus(404, 'Not found');
}
});
$app->put('/t1/{f1}', function($request, $response, $args) {
$f1 = (int)$args['f1'];
$contyp = $request->getHeader('Content-Type')[0];
$o = Util::String2Object('T1', $request->getBody(), $contyp);
$real = new Test();
if($o->f1 == $f1 && $real->save($o)) {
return $response->withStatus(204);
} else {
return $response->withStatus(404, 'Not found');
}
});
$app->delete('/t1/{f1}', function($request, $response, $args) {
$f1 = (int)$args['f1'];
$real = new Test();
if($real->delete($f1)) {
return $response->withStatus(204);
} else {
return $response->withStatus(404, 'Not found');
}
});
////$app->options('/{routes:.*}', function ($request, $response, $args) {
//// return $response->withHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")->withHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
////});
$app->run();
?>
app->xxxx method maps methods and path to a PHP anonymous function.
Request and response objects provides convenient access to input and output.
The //// lines are needed to enable CORS.
With hand coded solution the programmer is responsible for:
This result in much more code compared to using a framework.
Therefore hand coded only makes sense when one of:
This example use:
(all well known technologies that has been around for many years and are widely used)
T1.java:
package test;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
@Override
public String toString() {
return String.format("{%d,%s}", f1, f2);
}
}
T1Array.java:
package test;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getArray(){
return t1array;
}
public void setArray(T1[] array) {
this.t1array = array;
}
}
Util.java:
package test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
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.Unmarshaller;
import com.google.gson.Gson;
public class Util {
public static String Object2XML(Object o) throws IOException {
StringWriter sw = new StringWriter();
try {
JAXBContext jxbctx = JAXBContext.newInstance(o.getClass());
Marshaller m = jxbctx.createMarshaller();
m.marshal(o, sw);
} catch (JAXBException e) {
throw new IOException("JAXB exception: " + e.getMessage(), e);
}
return sw.toString();
}
@SuppressWarnings("unchecked")
public static <T> T XML2Object(Class<T> clz, String xml) throws IOException {
try {
JAXBContext jxbctx = JAXBContext.newInstance(clz);
Unmarshaller um = jxbctx.createUnmarshaller();
return (T)um.unmarshal(new StringReader(xml));
} catch (JAXBException e) {
throw new IOException("JAXB exception: " + e.getMessage(), e);
}
}
public static String Object2JSON(Object o) throws IOException {
Gson g = new Gson();
return g.toJson(o);
}
public static <T> T JSON2Object(Class<T> clz, String json) {
Gson g = new Gson();
return g.fromJson(json, clz);
}
public static String Object2String(Object o, String typ) throws IOException {
if(typ.equals("application/xml")) {
return Object2XML(o);
} else if(typ.equals("application/json")) {
return Object2JSON(o);
} else {
throw new IOException("Unknown type: " + typ);
}
}
public static <T> T String2Object(Class<T> clz, String s, String typ) throws IOException {
if(typ.equals("application/xml")) {
return XML2Object(clz, s);
} else if(typ.equals("application/json")) {
return JSON2Object(clz, s);
} else {
throw new IOException("Unknown type: " + typ);
}
}
public static String getAllLines(Reader rdr) throws IOException {
StringBuffer sb = new StringBuffer();
BufferedReader br = new BufferedReader(rdr);
String line;
while((line = br.readLine()) != null) {
sb.append(line);
sb.append(System.getProperty("line.separator"));
}
return sb.toString();
}
}
ClientManual.java:
package test;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.HttpResponse;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
public class ClientManual {
private static void testGetOne(String urlprefix, int f1, String acctyp) throws IOException {
HttpGet request = new HttpGet(urlprefix + "/t1/" + f1);
request.setHeader("Accept", acctyp);
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
String contyp = response.getFirstHeader("Content-Type").getValue().split(";")[0];
String respbody = Util.getAllLines(new InputStreamReader(response.getEntity().getContent()));
System.out.println(Util.String2Object(T1.class, respbody, contyp));
}
private static void testGetAll(String urlprefix, String acctyp) throws IOException {
HttpGet request = new HttpGet(urlprefix + "/t1");
request.setHeader("Accept", acctyp);
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
String contyp = response.getFirstHeader("Content-Type").getValue().split(";")[0];
String respbody = Util.getAllLines(new InputStreamReader(response.getEntity().getContent()));
T1[] a;
if(contyp.equals("application/xml")) {
a = Util.String2Object(T1Array.class, respbody, contyp).getArray();
} else {
a = Util.String2Object(T1[].class, respbody, contyp);
}
for(T1 o : a) System.out.print(o);
System.out.println();
}
private static void testGetSome(String urlprefix, int start, int finish, String acctyp) throws IOException {
HttpGet request = new HttpGet(urlprefix + "/t1?start=" + start + "&finish=" + finish);
request.setHeader("Accept", acctyp);
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
String contyp = response.getFirstHeader("Content-Type").getValue().split(";")[0];
String respbody = Util.getAllLines(new InputStreamReader(response.getEntity().getContent()));
T1[] a;
if(contyp.equals("application/xml")) {
a = Util.String2Object(T1Array.class, respbody, contyp).getArray();
} else {
a = Util.String2Object(T1[].class, respbody, contyp);
}
for(T1 o : a) System.out.print(o);
System.out.println();
}
private static void testPost(String urlprefix, String contyp, T1 o, String acctyp) throws IOException {
String reqbody = Util.Object2String(o, contyp);
HttpPost request = new HttpPost(urlprefix + "/t1");
request.setHeader("Content-Type", contyp);
request.setEntity(new StringEntity(reqbody));
request.setHeader("Accept", acctyp);
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
String contyp2 = response.getFirstHeader("Content-Type").getValue().split(";")[0];
String respbody = Util.getAllLines(new InputStreamReader(response.getEntity().getContent()));
System.out.println(Util.String2Object(T1.class, respbody, contyp2));
}
private static void testPut(String urlprefix, String contyp, T1 o) throws IOException {
String reqbody = Util.Object2String(o, contyp);
HttpPut request = new HttpPut(urlprefix + "/t1/" + o.getF1());
request.setHeader("Content-Type", contyp);
request.setEntity(new StringEntity(reqbody));
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
}
private static void testDelete(String urlprefix, int f1) throws IOException {
HttpDelete request = new HttpDelete(urlprefix + "/t1/" + f1);
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(request);
System.out.println(response.getStatusLine().getStatusCode() / 100 == 2);
}
private static void test(String urlprefix, String typ) throws IOException {
System.out.println("**** " + urlprefix + " (" + typ + ") ****");
testGetOne(urlprefix, 123, typ);
testGetAll(urlprefix, typ);
testGetSome(urlprefix, 10, 12, typ);
testPost(urlprefix, typ, new T1(123, "ABC"), typ);
testPut(urlprefix, typ, new T1(123, "ABC"));
testDelete(urlprefix, 123);
}
private static void test(String urlprefix) throws IOException {
test(urlprefix, "application/xml");
test(urlprefix, "application/json");
}
public static void main(String[] args) throws IOException {
Logger.getRootLogger().setLevel(Level.OFF);
test("http://localhost:8080/wsrest/testapi1");
test("http://localhost:8080/wsrest/testapi2");
test("http://localhost:8080/wsrest/testapi3");
test("http://localhost/testapi.ashx");
test("http://localhost/testapi.svc");
test("http://localhost/testapi");
test("http://localhost:81/testapi.php");
}
}
This example use:
(all part of .NET)
ClientManual.cs:
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Web.Script.Serialization;
using System.Xml.Serialization;
namespace Test1
{
[XmlRoot(ElementName="t1")]
public class T1
{
public int f1 { get; set; }
public string f2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.f1 = f1;
this.f2 = f2;
}
public override string ToString()
{
return string.Format("{{{0},{1}}}", f1, f2);
}
}
[XmlRoot(ElementName="t1array")]
public class T1Array
{
[XmlElement(ElementName="t1")]
public T1[] t1array { get; set; }
public T1Array() : this(new T1[0])
{
}
public T1Array(T1[] array)
{
this.t1array = array;
}
}
public static class Util
{
public static string Object2XML(object o)
{
XmlSerializer ser = new XmlSerializer(o.GetType());
using(StringWriter sw = new StringWriter())
{
ser.Serialize(sw, o);
return sw.ToString().Replace("utf-16", "utf-8"); // StringWriter end up with "utf-16" which is not good
}
}
public static T XML2Object<T>(string xml)
{
XmlSerializer ser = new XmlSerializer(typeof(T));
using(StringReader sr = new StringReader(xml))
{
return (T)ser.Deserialize(sr);
}
}
public static string Object2JSON(object o)
{
JavaScriptSerializer ser = new JavaScriptSerializer();
StringBuilder sb = new StringBuilder();
ser.Serialize(o, sb);
return sb.ToString();
}
public static T JSON2Object<T>(string xml)
{
JavaScriptSerializer ser = new JavaScriptSerializer();
return (T)ser.Deserialize(xml, typeof(T));
}
public static string Object2String(object o, string typ)
{
if(typ == "application/xml")
{
return Util.Object2XML(o);
}
else if(typ == "application/json")
{
return Util.Object2JSON(o);
}
else
{
throw new Exception("Unknown type: " + typ);
}
}
public static T String2Object<T>(string s, string typ)
{
if(typ == "application/xml")
{
return Util.XML2Object<T>(s);
}
else if(typ == "application/json")
{
return Util.JSON2Object<T>(s);
}
else
{
throw new Exception("Unknown type: " + typ);
}
}
public static string GetAllLines(Stream stm)
{
using(StreamReader sr = new StreamReader(stm))
{
return sr.ReadToEnd();
}
}
}
public class ClientManual
{
private static void TestGetOne(string urlprefix, int f1, string acctyp)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1/" + f1);
request.Method = WebRequestMethods.Http.Get;
request.Accept = acctyp;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
string contyp = response.ContentType.Split(';')[0];
string respbody = Util.GetAllLines(response.GetResponseStream());
Console.WriteLine(Util.String2Object<T1>(respbody, contyp));
response.Close();
}
private static void TestGetAll(string urlprefix, string acctyp)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1");
request.Method = WebRequestMethods.Http.Get;
request.Accept = acctyp;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
string contyp = response.ContentType.Split(';')[0];
string respbody = Util.GetAllLines(response.GetResponseStream());
T1[] a;
if(acctyp == "application/xml")
{
a = Util.String2Object<T1Array>(respbody, contyp).t1array;
}
else
{
a = Util.String2Object<T1[]>(respbody, contyp);
}
foreach(T1 o in a) Console.Write(o);
Console.WriteLine();
response.Close();
}
private static void TestGetSome(string urlprefix, int start, int finish, string acctyp)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1?start=" + start + "&finish=" + finish);
request.Method = WebRequestMethods.Http.Get;
request.Accept = acctyp;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
string contyp = response.ContentType.Split(';')[0];
string respbody = Util.GetAllLines(response.GetResponseStream());
T1[] a;
if(acctyp == "application/xml")
{
a = Util.String2Object<T1Array>(respbody, contyp).t1array;
}
else
{
a = Util.String2Object<T1[]>(respbody, contyp);
}
foreach(T1 o in a) Console.Write(o);
Console.WriteLine();
response.Close();
}
private static void TestPost(string urlprefix, string contyp, T1 o, string acctyp)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1");
request.Method = WebRequestMethods.Http.Post;
request.Accept = acctyp;
request.ContentType = contyp;
string reqbody = Util.Object2String(o, contyp);
request.GetRequestStream().Write(Encoding.UTF8.GetBytes(reqbody), 0, Encoding.UTF8.GetByteCount(reqbody));
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
string contyp2 = response.ContentType.Split(';')[0];
string respbody = Util.GetAllLines(response.GetResponseStream());
Console.WriteLine(Util.String2Object<T1>(respbody, contyp2));
response.Close();
}
private static void TestPut(string urlprefix, string contyp, T1 o)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1/" + o.f1);
request.Method = WebRequestMethods.Http.Put;
request.ContentType = contyp;
string reqbody = Util.Object2String(o, contyp);
request.GetRequestStream().Write(Encoding.UTF8.GetBytes(reqbody), 0, Encoding.UTF8.GetByteCount(reqbody));
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
response.Close();
}
private static void TestDelete(string urlprefix, int f1)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlprefix + "/t1/" + f1);
request.Method = "DELETE";
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine((int)response.StatusCode / 100 == 2);
response.Close();
}
private static void Test(string urlprefix, string typ)
{
Console.WriteLine("**** " + urlprefix + " (" + typ + ") ****");
TestGetOne(urlprefix, 123, typ);
TestGetAll(urlprefix, typ);
TestGetSome(urlprefix, 10, 12, typ);
TestPost(urlprefix, typ, new T1(123, "ABC"), typ);
TestPut(urlprefix, typ, new T1(123, "ABC"));
TestDelete(urlprefix, 123);
}
private static void Test(string urlprefix)
{
Test(urlprefix, "application/xml");
Test(urlprefix, "application/json");
}
public static void Main(string[] args)
{
Test("http://localhost:8080/wsrest/testapi1");
Test("http://localhost:8080/wsrest/testapi2");
Test("http://localhost:8080/wsrest/testapi3");
Test("http://localhost/testapi.ashx");
Test("http://localhost/testapi.svc");
Test("http://localhost/testapi");
Test("http://localhost:81/testapi.php");
}
}
}
This example use:
(all part of .NET)
ClientManual.vb:
Imports System
Imports System.IO
Imports System.Net
Imports System.Text
Imports System.Web.Script.Serialization
Imports System.Xml.Serialization
Namespace Test1
<XmlRoot(ElementName := "t1")> _
Public Class T1
Public Property f1() As Integer
Public Property f2() As String
Public Sub New()
Me.New(0, "")
End Sub
Public Sub New(f1 As Integer, f2 As String)
Me.f1 = f1
Me.f2 = f2
End Sub
Public Overrides Function ToString() As String
Return String.Format("{{{0},{1}}}", f1, f2)
End Function
End Class
<XmlRoot(ElementName := "t1array")> _
Public Class T1Array
<XmlElement(ElementName := "t1")> _
Public Property t1array() As T1()
Public Sub New()
Me.New(New T1(-1) {})
End Sub
Public Sub New(array As T1())
Me.t1array = array
End Sub
End Class
Public NotInheritable Class Util
Private Sub New()
End Sub
Public Shared Function Object2XML(o As Object) As String
Dim ser As New XmlSerializer(o.[GetType]())
Using sw As New StringWriter()
ser.Serialize(sw, o)
Return sw.ToString().Replace("utf-16", "utf-8") ' StringWriter end up with "utf-16" which is not good
End Using
End Function
Public Shared Function XML2Object(Of T)(xml As String) As T
Dim ser As New XmlSerializer(GetType(T))
Using sr As New StringReader(xml)
Return DirectCast(ser.Deserialize(sr), T)
End Using
End Function
Public Shared Function Object2JSON(o As Object) As String
Dim ser As New JavaScriptSerializer()
Dim sb As New StringBuilder()
ser.Serialize(o, sb)
Return sb.ToString()
End Function
Public Shared Function JSON2Object(Of T)(xml As String) As T
Dim ser As New JavaScriptSerializer()
Return DirectCast(ser.Deserialize(xml, GetType(T)), T)
End Function
Public Shared Function Object2String(o As Object, typ As String) As String
If typ = "application/xml" Then
Return Util.Object2XML(o)
ElseIf typ = "application/json" Then
Return Util.Object2JSON(o)
Else
Throw New Exception("Unknown type: " & typ)
End If
End Function
Public Shared Function String2Object(Of T)(s As String, typ As String) As T
If typ = "application/xml" Then
Return Util.XML2Object(Of T)(s)
ElseIf typ = "application/json" Then
Return Util.JSON2Object(Of T)(s)
Else
Throw New Exception("Unknown type: " & typ)
End If
End Function
Public Shared Function GetAllLines(stm As Stream) As String
Using sr As New StreamReader(stm)
Return sr.ReadToEnd()
End Using
End Function
End Class
Public Class ClientManual
Private Shared Sub TestGetOne(urlprefix As String, f1 As Integer, acctyp As String)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1/" & f1), HttpWebRequest)
request.Method = WebRequestMethods.Http.[Get]
request.Accept = acctyp
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
Dim contyp As String = response.ContentType.Split(";"C)(0)
Dim respbody As String = Util.GetAllLines(response.GetResponseStream())
Console.WriteLine(Util.String2Object(Of T1)(respbody, contyp))
response.Close()
End Sub
Private Shared Sub TestGetAll(urlprefix As String, acctyp As String)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1"), HttpWebRequest)
request.Method = WebRequestMethods.Http.[Get]
request.Accept = acctyp
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
Dim contyp As String = response.ContentType.Split(";"C)(0)
Dim respbody As String = Util.GetAllLines(response.GetResponseStream())
Dim a As T1()
If acctyp = "application/xml" Then
a = Util.String2Object(Of T1Array)(respbody, contyp).t1array
Else
a = Util.String2Object(Of T1())(respbody, contyp)
End If
For Each o As T1 In a
Console.Write(o)
Next
Console.WriteLine()
response.Close()
End Sub
Private Shared Sub TestGetSome(urlprefix As String, start As Integer, finish As Integer, acctyp As String)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1?start=" & start & "&finish=" & finish), HttpWebRequest)
request.Method = WebRequestMethods.Http.[Get]
request.Accept = acctyp
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
Dim contyp As String = response.ContentType.Split(";"C)(0)
Dim respbody As String = Util.GetAllLines(response.GetResponseStream())
Dim a As T1()
If acctyp = "application/xml" Then
a = Util.String2Object(Of T1Array)(respbody, contyp).t1array
Else
a = Util.String2Object(Of T1())(respbody, contyp)
End If
For Each o As T1 In a
Console.Write(o)
Next
Console.WriteLine()
response.Close()
End Sub
Private Shared Sub TestPost(urlprefix As String, contyp As String, o As T1, acctyp As String)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1"), HttpWebRequest)
request.Method = WebRequestMethods.Http.Post
request.Accept = acctyp
request.ContentType = contyp
Dim reqbody As String = Util.Object2String(o, contyp)
request.GetRequestStream().Write(Encoding.UTF8.GetBytes(reqbody), 0, Encoding.UTF8.GetByteCount(reqbody))
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
Dim contyp2 As String = response.ContentType.Split(";"C)(0)
Dim respbody As String = Util.GetAllLines(response.GetResponseStream())
Console.WriteLine(Util.String2Object(Of T1)(respbody, contyp2))
response.Close()
End Sub
Private Shared Sub TestPut(urlprefix As String, contyp As String, o As T1)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1/" & o.f1), HttpWebRequest)
request.Method = WebRequestMethods.Http.Put
request.ContentType = contyp
Dim reqbody As String = Util.Object2String(o, contyp)
request.GetRequestStream().Write(Encoding.UTF8.GetBytes(reqbody), 0, Encoding.UTF8.GetByteCount(reqbody))
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
response.Close()
End Sub
Private Shared Sub TestDelete(urlprefix As String, f1 As Integer)
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(urlprefix & "/t1/" & f1), HttpWebRequest)
request.Method = "DELETE"
Dim response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWebResponse)
Console.WriteLine(CInt(response.StatusCode) \ 100 = 2)
response.Close()
End Sub
Private Shared Sub Test(urlprefix As String, typ As String)
Console.WriteLine("**** " & urlprefix & " (" & typ & ") ****")
TestGetOne(urlprefix, 123, typ)
TestGetAll(urlprefix, typ)
TestGetSome(urlprefix, 10, 12, typ)
TestPost(urlprefix, typ, New T1(123, "ABC"), typ)
TestPut(urlprefix, typ, New T1(123, "ABC"))
TestDelete(urlprefix, 123)
End Sub
Private Shared Sub Test(urlprefix As String)
Test(urlprefix, "application/xml")
Test(urlprefix, "application/json")
End Sub
Public Shared Sub Main(args As String())
Test("http://localhost:8080/wsrest/testapi1")
Test("http://localhost:8080/wsrest/testapi2")
Test("http://localhost:8080/wsrest/testapi3")
Test("http://localhost/testapi.ashx")
Test("http://localhost/testapi.svc")
Test("http://localhost/testapi")
Test("http://localhost:81/testapi.php")
End Sub
End Class
End Namespace
The curl extension is used for HTTP support.
This example use custom serialization/deserialization.
There are a lot of code for the custom serialization/deserialization, but it is not really important for understanding the web service part, so I recommend not spending too much time on that custom serialization/deserialization code. If format is limited to JSON and whatever format json_encode/json_decode use is good enough, then it all goes away.
clientmanual.php:
<?php
/*
* Generic serializer/deserializer framework.
* It is not tested/robust enough for production usage.
*/
/*
interface Formatter {
public function fieldPrefix($nam);
public function fieldValue($nam, $val);
public function fieldSeparator();
public function fieldSuffix($nam);
public function arrayPrefix($nam);
public function arrayValue($val);
public function arraySeparator();
public function arraySuffix($nam);
}
class JSONFormatter implements Formatter {
public function fieldPrefix($nam) {
return '{';
}
public function fieldValue($nam, $val) {
if(substr($val, 0, 1) == '{' || substr($val, 0, 1) == '[' || is_int($val)) {
return sprintf('"%s":%s', $nam, $val);
} else {
return sprintf('"%s":"%s"', $nam, $val);
}
}
public function fieldSeparator() {
return ',';
}
public function fieldSuffix($nam) {
return '}';
}
public function arrayPrefix($nam) {
return '[';
}
public function arrayValue($val) {
return $val;
}
public function arraySeparator() {
return ',';
}
public function arraySuffix($nam) {
return ']';
}
}
class XMLFormatter implements Formatter {
public function fieldPrefix($nam) {
return sprintf('<%s>', $nam);
}
public function fieldValue($nam, $val) {
return sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
public function fieldSeparator() {
return '';
}
public function fieldSuffix($nam) {
return sprintf('</%s>', $nam);
}
public function arrayPrefix($nam) {
return sprintf('<%s>', $nam);
}
public function arrayValue($val) {
return $val;
}
public function arraySeparator() {
return '';
}
public function arraySuffix($nam) {
return sprintf('</%s>', $nam);
}
}
class Util {
private static function Object2Anything($o, $fmt) {
$clz = strtolower(get_class($o));
if(substr($clz, -5) == 'array' && isset($o->array)) {
$res = $fmt->arrayPrefix($clz);
foreach($o->array as $elm) {
if(strlen($res) > 1) $res .= $fmt->arraySeparator();
$res .= $fmt->arrayValue(Util::Object2Anything($elm, $fmt));
}
$res .= $fmt->arraySuffix($clz);
return $res;
} else {
$res = $fmt->fieldPrefix($clz);
foreach(get_object_vars($o) as $nam => $val) {
if(strlen($res) > 1) $res .= $fmt->fieldSeparator();
$res .= $fmt->fieldValue($nam, is_object($val) ? Util::Object2Anything($val, $fmt) : $val);
}
$res .= $fmt->fieldSuffix($clz);
return $res;
}
}
public static function Object2JSON($o) {
return Util::Object2Anything($o, new JSONFormatter());
}
public static function Object2XML($o) {
return Util::Object2Anything($o, new XMLFormatter());
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
private static function mapFields(&$fromobj, &$toobj) {
foreach(get_object_vars($toobj) as $nam => $val) {
if(is_object($toobj->$nam) || is_array($toobj->$nam)) {
Util::map($fromobj->$nam, $toobj->$nam);
} else {
if(is_int($toobj->$nam)) {
$toobj->$nam = (int)$fromobj->$nam;
} else {
$toobj->$nam = (string)$fromobj->$nam;
}
}
}
}
private static function mapArray(&$fromarr, &$toarr, $arrelmclz) {
foreach($fromarr as $elm) {
$temp = new $arrelmclz();
Util::map($elm, $temp);
$toarr[] = $temp;
}
}
private static function map(&$fromobj, &$toobj) {
$clz = strtolower(get_class($toobj));
if(substr($clz, -5) == 'array' && isset($toobj->array)) {
$arrelmclz = substr(get_class($toobj), 0 , -5);
$temp = $fromobj;
if(isset($fromobj->array)) $temp = $fromobj->array;
$f1 = strtolower($arrelmclz . 'array');
$f2 = strtolower($arrelmclz);
if(isset($fromobj->$f1)) $temp = $fromobj->$f1->$f2;
Util::mapArray($temp, $toobj->array, $arrelmclz);
} else {
Util::mapFields($fromobj, $toobj);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
Util::map($temp, $res);
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
Util::map($temp, $res);
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
*/
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
switch(get_class($o)) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
public function __toString() {
return sprintf('{%s,%s}', $this->f1, $this->f2);
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
function testGetOne($urlprefix, $f1, $acctyp) {
$curl = curl_init($urlprefix . '/t1/' . $f1);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: ' . $acctyp));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response, $acctyp) . "\r\n";
}
function testGetAll($urlprefix, $acctyp) {
$curl = curl_init($urlprefix . '/t1');
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: ' . $acctyp));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response, $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testGetSome($urlprefix, $start, $finish, $acctyp) {
$curl = curl_init($urlprefix . '/t1?start=' . $start . '&finish=' . $finish);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: ' . $acctyp));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response, $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testPost($urlprefix, $contyp, $o, $acctyp) {
$curl = curl_init($urlprefix . '/t1');
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: ' . $acctyp, 'Content-Type: ' . $contyp));
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, Util::Object2String($o, $contyp));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response, $acctyp) . "\r\n";
}
function testPut($urlprefix, $contyp, $o) {
$curl = curl_init($urlprefix . '/t1/' . $o->f1);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: ' . $contyp));
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($curl, CURLOPT_POSTFIELDS, Util::Object2String($o, $contyp));
curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
}
function testDelete($urlprefix, $f1) {
$curl = curl_init($urlprefix . '/t1/' . $f1);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
echo (200 <= $info['http_code'] && $info['http_code'] < 300 ? 'true' : 'false') . "\r\n";
}
function test2($urlprefix, $typ) {
echo '**** ' . $urlprefix . ' (' . $typ . ') ****' . "\r\n";
testGetOne($urlprefix, 123, $typ);
testGetAll($urlprefix, $typ);
testGetSome($urlprefix, 10, 12, $typ);
testPost($urlprefix, $typ, new T1(123, 'ABC'), $typ);
testPut($urlprefix, $typ, new T1(123, 'ABC'));
testDelete($urlprefix, 123);
}
function test($urlprefix) {
test2($urlprefix, "application/xml");
test2($urlprefix, "application/json");
}
test("http://localhost:8080/wsrest/testapi1");
test("http://localhost:8080/wsrest/testapi2");
test("http://localhost:8080/wsrest/testapi3");
test("http://localhost/testapi.ashx");
test("http://localhost/testapi.svc");
test("http://localhost/testapi");
test("http://localhost:81/testapi.php");
?>
I don't like the Curl API.
Indy is used for HTTP support.
This example use custom serialization/deserialization.
There are a lot of code for the custom serialization/deserialization, but it is not really important for understanding the web service part, so I recommend not spending too much time on that custom serialization/deserialization code.
The example is tested with Lazarus but should work with Delphi with minimal changes.
ClientManual.pas:
program ClientManual;
uses
Classes, SysUtils,
IdHTTP,
DOM, XMLRead, XMLWrite,
fpjson, jsonparser;
type
T1 = class(TObject)
private
m_f1 : integer;
m_f2 : string;
public
constructor Create;
constructor Create(f1 : integer; f2 : string);
property F1 : integer read m_f1;
property F2 : string read m_f2;
function ToString() : string; override;
procedure Free;
end;
ListT1 = class(TObject)
private
m_list : TList;
public
constructor Create;
procedure Add(o : T1);
procedure Add(f1 : integer; f2 : string);
function Count : integer;
function GetElement(ix : integer) : T1;
procedure Free;
end;
constructor T1.Create;
begin
m_f1 := 0;
m_f2 := '';
end;
constructor T1.Create(f1 : integer; f2 : string);
begin
m_f1 := f1;
m_f2 := f2;
end;
function T1.ToString() : string;
begin
ToString := '{' + IntToStr(m_f1) + ',' + m_f2 + '}';
end;
procedure T1.Free;
begin
end;
constructor ListT1.Create;
begin
m_list := TList.Create;
end;
procedure ListT1.Add(o : T1);
begin
m_list.Add(o);
end;
procedure ListT1.Add(f1 : integer; f2 : string);
begin
m_list.Add(T1.Create(f1, f2));
end;
function ListT1.Count : integer;
begin
Count := m_list.Count;
end;
function ListT1.GetElement(ix : integer) : T1;
begin
GetElement := T1(m_list[ix]);
end;
procedure ListT1.Free;
var
i : integer;
begin
for i := 0 to m_list.Count - 1 do begin
T1(m_list[i]).Free;
end;
m_list.Free;
end;
(* start primitive serialization framework *)
function Object2XML(clz : string; obj : TObject) : string;
var
o : T1;
a : ListT1;
res : string;
i : integer;
begin
if clz = 'T1' then begin
o := T1(obj);
Object2XML := '<t1><f1>' + IntToStr(o.F1) + '</f1><f2>' + o.F2 + '</f2></t1>';
end else if clz = 'ListT1' then begin
a := ListT1(obj);
res := '<t1array>';
for i := 0 to a.Count - 1 do begin
res := res + Object2XML('T1', a.GetElement(i));
end;
res := res + '</t1array>';
Object2XML := res;
end else begin
raise Exception.Create('Unknown class: ' + clz);
end;
end;
function XML2Object(clz, s : string) : TObject;
function CreateT1(node : TDOMNode) : T1;
var
childs : TDOMNodeList;
i, f1 : integer;
f2 : string;
begin
childs := node.GetChildNodes;
for i := 0 to childs.Count - 1 do begin
if childs.Item[i].NodeName = 'f1' then begin
f1 := StrToInt(string(childs.Item[i].FirstChild.NodeValue));
end;
if childs.Item[i].NodeName = 'f2' then begin
f2 := string(childs.Item[i].FirstChild.NodeValue);
end;
end;
CreateT1 := T1.Create(f1, f2);
end;
var
parser : TDOMParser;
doc : TXMLDocument;
childs : TDOMNodeList;
res : ListT1;
i : integer;
begin
parser := TDOMparser.Create;
parser.Parse(TXMLInputSource.Create(s), doc);
parser.Free;
if clz = 'T1' then begin
XML2Object := CreateT1(doc.DocumentElement);
end else if clz = 'ListT1' then begin
res := ListT1.Create;
childs := doc.DocumentElement.GetChildNodes;
for i := 0 to childs.Count - 1 do begin
res.Add(CreateT1(childs.Item[i]));
end;
XML2Object := res;
end else begin
raise Exception.Create('Unknown class: ' + clz);
end;
end;
function Object2JSON(clz : string; obj : TObject) : string;
var
o : T1;
a : ListT1;
res : string;
i : integer;
begin
if clz = 'T1' then begin
o := T1(obj);
Object2JSON := '{"f1":' + IntToStr(o.F1) + ',"f2":"' + o.F2 + '"}';
end else if clz = 'ListT1' then begin
a := ListT1(obj);
res := '[';
for i := 0 to a.Count - 1 do begin
res := res + Object2JSON('T1', a.GetElement(i));
end;
res := res + ']';
Object2JSON := res;
end else begin
raise Exception.Create('Unknown class: ' + clz);
end;
end;
function JSON2Object(clz, s : string) : TObject;
function CreateT1(obj : TJSONObject) : T1;
begin
CreateT1 := T1.Create(obj.Integers['f1'], obj.Strings['f2']);
end;
var
o : TJSONObject;
a : TJSONArray;
res : ListT1;
i : integer;
begin
if clz = 'T1' then begin
o := TJSONObject(GetJSON(s));
JSON2Object := CreateT1(o);
end else if clz = 'ListT1' then begin
a := TJSONArray(GetJSON(s));
res := ListT1.Create;
for i := 0 to a.Count - 1 do begin
res.Add(CreateT1(TJSONObject(a.Items[i])));;
end;
JSON2Object := res;
end else begin
raise Exception.Create('Unknown class: ' + clz);
end;
end;
function Object2String(clz : string; obj : TObject; typ : string) : string;
begin
if typ = 'application/xml' then begin
Object2String := Object2XML(clz, obj);
end else if typ = 'application/json' then begin
Object2String := Object2JSON(clz, obj);
end else begin
raise Exception.Create('Unknown type: ' + typ);
end;
end;
function String2Object(clz, s, typ : string) : TObject;
begin
if typ = 'application/xml' then begin
String2Object := XML2Object(clz, s);
end else if typ = 'application/json' then begin
String2Object := JSON2Object(clz, s);
end else begin
raise Exception.Create('Unknown type: ' + typ);
end;
end;
(* end primitive serialization framework *)
procedure TestGetOne(urlprefix : string; f1 : integer; acctyp : string);
var
con : TIdHTTP;
respbody, contyp : string;
begin
con := TIdHTTP.Create;
con.Request.Accept := acctyp;
respbody := con.Get(urlprefix + '/t1/' + IntToStr(f1));
writeln((con.Response.ResponseCode div 100) = 2);
contyp := con.Response.ContentType;
writeln(String2Object('T1', respbody, contyp).ToString());
con.Free;
end;
procedure TestGetAll(urlprefix : string; acctyp : string);
var
con : TIdHTTP;
respbody, contyp : string;
a : ListT1;
i : integer;
begin
con := TIdHTTP.Create;
con.Request.Accept := acctyp;
respbody := con.Get(urlprefix + '/t1');
writeln((con.Response.ResponseCode div 100) = 2);
contyp := con.Response.ContentType;
a := ListT1(String2Object('ListT1', respbody, contyp));
for i := 0 to a.Count - 1 do begin
writeln(a.GetElement(i).ToString());
end;
con.Free;
end;
procedure TestGetSome(urlprefix : string; start, finish : integer; acctyp : string);
var
con : TIdHTTP;
respbody, contyp : string;
a : ListT1;
i : integer;
begin
con := TIdHTTP.Create;
con.Request.Accept := acctyp;
respbody := con.Get(urlprefix + '/t1?start=' + IntToStr(start) + '&finish=' + IntToStr(finish));
writeln((con.Response.ResponseCode div 100) = 2);
contyp := con.Response.ContentType;
a := ListT1(String2Object('ListT1', respbody, contyp));
for i := 0 to a.Count - 1 do begin
writeln(a.GetElement(i).ToString());
end;
con.Free;
end;
procedure TestPost(urlprefix, contyp : string; o : T1; acctyp : string);
var
con : TIdHTTP;
respbody, contyp2 : string;
begin
con := TIdHTTP.Create;
con.Request.Accept := acctyp;
con.Request.ContentType := contyp;
respbody := con.Post(urlprefix + '/t1', TStringStream.Create(Object2String('T1', o, contyp)));
writeln((con.Response.ResponseCode div 100) = 2);
contyp2 := con.Response.ContentType;
writeln(String2Object('T1', respbody, contyp2).ToString());
con.Free;
end;
procedure TestPut(urlprefix, contyp : string; o : T1);
var
con : TIdHTTP;
begin
con := TIdHTTP.Create;
con.Request.ContentType := contyp;
con.Put(urlprefix + '/t1/' + IntToStr(o.F1), TStringStream.Create(Object2String('T1', o, contyp)));
writeln((con.Response.ResponseCode div 100) = 2);
con.Free;
end;
procedure TestDelete(urlprefix : string; f1 : integer);
var
con : TIdHTTP;
begin
con := TIdHTTP.Create;
con.Delete(urlprefix + '/t1/' + IntToStr(f1));
writeln((con.Response.ResponseCode div 100) = 2);
con.Free;
end;
procedure TestAll(urlprefix, typ : string);
begin
writeln('**** ' + urlprefix + ' (' + typ + ') ****');
TestGetOne(urlprefix, 123, typ);
TestGetAll(urlprefix, typ);
TestGetSome(urlprefix, 10, 12, typ);
TestPost(urlprefix, typ, T1.Create(123, 'ABC'), typ);
TestPut(urlprefix, typ, T1.Create(123, 'ABC'));
TestDelete(urlprefix, 123);
end;
procedure Test(urlprefix : string);
begin
TestAll(urlprefix, 'application/xml');
TestAll(urlprefix, 'application/json');
end;
begin
Test('http://localhost:8080/wsrest/testapi1');
Test('http://localhost:8080/wsrest/testapi2');
Test('http://localhost:8080/wsrest/testapi3');
Test('http://localhost/testapi.ashx');
Test('http://localhost/testapi.svc');
Test('http://localhost/testapi');
Test('http://localhost:81/testapi.php');
end.
With framework solution the framework is handling:
This result in much less code compared to hand coded.
JAX-RS also has a client framework.
My experience is that RestEasy is very difficult to get working for client usage (no problem for server usage) while Jersey is easy to get working for client usage, so I will recommend Jersey over RestEasy for client usage.
T1.java:
package test;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class T1 {
private int f1;
private String f2;
public T1() {
this(0, "");
}
public T1(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public int getF1() {
return f1;
}
public void setF1(int f1) {
this.f1 = f1;
}
public String getF2() {
return f2;
}
public void setF2(String f2) {
this.f2 = f2;
}
@Override
public String toString() {
return String.format("{%d,%s}", f1, f2);
}
}
T1Array.java:
package test;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="t1array")
public class T1Array {
private T1[] t1array;
public T1Array() {
this(new T1[0]);
}
public T1Array(T1[] array) {
this.t1array = array;
}
@XmlElements(@XmlElement(name="t1",type=T1.class))
public T1[] getArray(){
return t1array;
}
public void setArray(T1[] array) {
this.t1array = array;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for(T1 o : t1array) {
if(sb.length() > 1) sb.append(",");
sb.append(o);
}
sb.append("]");
return sb.toString();
}
}
ClientJAXRS.java:
package test;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
public class ClientJAXRS {
private static void testGetOne(String urlprefix, int f1, String acctyp) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1/" + f1).request(acctyp).get();
System.out.println(response.getStatus() / 100 == 2);
System.out.println(response.readEntity(T1.class));
}
private static void testGetAll(String urlprefix, String acctyp) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1").request(acctyp).get();
System.out.println(response.getStatus() / 100 == 2);
T1[] a;
if(acctyp.equals(MediaType.APPLICATION_XML)) {
a = response.readEntity(T1Array.class).getArray();
} else {
a = response.readEntity(T1[].class);
}
for(T1 o : a) System.out.print(o);
System.out.println();
}
private static void testGetSome(String urlprefix, int start, int finish, String acctyp) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1").queryParam("start", start).queryParam("finish", finish).request(acctyp).get();
System.out.println(response.getStatus() / 100 == 2);
T1[] a;
if(acctyp.equals(MediaType.APPLICATION_XML)) {
a = response.readEntity(T1Array.class).getArray();
} else {
a = response.readEntity(T1[].class);
}
for(T1 o : a) System.out.print(o);
System.out.println();
}
private static void testPost(String urlprefix, String contyp, T1 o, String acctyp) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1").request(acctyp).post(Entity.entity(o, contyp));
System.out.println(response.getStatus() / 100 == 2);
System.out.println(response.readEntity(T1.class));
}
private static void testPut(String urlprefix, String contyp, T1 o) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1/" + o.getF1()).request().put(Entity.entity(o, contyp));
System.out.println(response.getStatus() / 100 == 2);
}
private static void testDelete(String urlprefix, int f1) {
Client client = ClientBuilder.newClient();
Response response = client.target(urlprefix).path("/t1/" + f1).request().delete();
System.out.println(response.getStatus() / 100 == 2);
}
private static void test(String urlprefix, String typ) {
System.out.println("**** " + urlprefix + " (" + typ + ") ****");
testGetOne(urlprefix, 123, typ);
testGetAll(urlprefix, typ);
testGetSome(urlprefix, 10, 12, typ);
testPost(urlprefix, typ, new T1(123, "ABC"), typ);
testPut(urlprefix, typ, new T1(123, "ABC"));
testDelete(urlprefix, 123);
}
private static void test(String urlprefix) {
test(urlprefix, MediaType.APPLICATION_XML);
test(urlprefix, MediaType.APPLICATION_JSON);
}
public static void main(String[] args) {
test("http://localhost:8080/wsrest/testapi1");
test("http://localhost:8080/wsrest/testapi2");
test("http://localhost:8080/wsrest/testapi3");
test("http://localhost/testapi.ashx");
test("http://localhost/testapi.svc");
test("http://localhost/testapi");
test("http://localhost:81/testapi.php");
}
}
Web API also has a client framework.
Note that Web API client is geared towards asynchroneus usage, but this example is synchroneous.
ClientWebApi.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
namespace Test2
{
[DataContract(Name="t1",Namespace="")]
public class T1
{
[DataMember(Name="f1")]
public int F1 { get; set; }
[DataMember(Name="f2")]
public string F2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.F1 = f1;
this.F2 = f2;
}
public override string ToString()
{
return string.Format("{{{0},{1}}}", F1, F2);
}
}
[CollectionDataContract(Name="t1array",Namespace="")]
public class T1Array : List<T1>
{
public T1Array() : this(new T1[0])
{
}
public T1Array(T1[] array) : base(array)
{
}
}
public class ClientWebApi
{
private static void TestGetOne(string urlprefix, int f1, string acctyp)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(acctyp));
HttpResponseMessage response = client.GetAsync(urlprefix + "/t1/" + f1).Result;
Console.WriteLine(response.IsSuccessStatusCode);
Console.WriteLine(response.Content.ReadAsAsync<T1>().Result);
}
private static void TestGetAll(string urlprefix, string acctyp)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(acctyp));
HttpResponseMessage response = client.GetAsync(urlprefix + "/t1").Result;
Console.WriteLine(response.IsSuccessStatusCode);
T1[] a;
if(acctyp == "application/xml")
{
a = response.Content.ReadAsAsync<T1Array>().Result.ToArray();
}
else
{
a = response.Content.ReadAsAsync<T1[]>().Result;
}
foreach(T1 o in a) Console.Write(o);
Console.WriteLine();
}
private static void TestGetSome(string urlprefix, int start, int finish, string acctyp)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(acctyp));
HttpResponseMessage response = client.GetAsync(urlprefix + "/t1?start=" + start + "&finish=" + finish).Result;
Console.WriteLine(response.IsSuccessStatusCode);
T1[] a;
if(acctyp == "application/xml")
{
a = response.Content.ReadAsAsync<T1Array>().Result.ToArray();
}
else
{
a = response.Content.ReadAsAsync<T1[]>().Result;
}
foreach(T1 o in a) Console.Write(o);
Console.WriteLine();
}
private static void TestPost(string urlprefix, string contyp, T1 o, string acctyp)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(acctyp));
HttpResponseMessage response;
if(contyp == "application/xml")
{
response = client.PostAsXmlAsync(urlprefix + "/t1", o).Result;
}
else
{
response = client.PostAsJsonAsync(urlprefix + "/t1", o).Result;
}
Console.WriteLine(response.IsSuccessStatusCode);
Console.WriteLine(response.Content.ReadAsAsync<T1>().Result);
}
private static void TestPut(string urlprefix, string contyp, T1 o)
{
HttpClient client = new HttpClient();
HttpResponseMessage response;
if(contyp == "application/xml")
{
response = client.PutAsXmlAsync(urlprefix + "/t1/" + o.F1, o).Result;
}
else
{
response = client.PutAsJsonAsync(urlprefix + "/t1/" + o.F1, o).Result;
}
Console.WriteLine(response.IsSuccessStatusCode);
}
private static void TestDelete(string urlprefix, int f1)
{
HttpClient client = new HttpClient();
HttpResponseMessage response = client.DeleteAsync(urlprefix + "/t1/" + f1).Result;
Console.WriteLine(response.IsSuccessStatusCode);
}
private static void Test(string urlprefix, string typ)
{
Console.WriteLine("**** " + urlprefix + " (" + typ + ") ****");
TestGetOne(urlprefix, 123, typ);
TestGetAll(urlprefix, typ);
TestGetSome(urlprefix, 10, 12, typ);
TestPost(urlprefix, typ, new T1(123, "ABC"), typ);
TestPut(urlprefix, typ, new T1(123, "ABC"));
TestDelete(urlprefix, 123);
}
private static void Test(string urlprefix)
{
Test(urlprefix, "application/xml");
Test(urlprefix, "application/json");
}
public static void Main(string[] args)
{
Test("http://localhost:8080/wsrest/testapi1");
Test("http://localhost:8080/wsrest/testapi2");
Test("http://localhost:8080/wsrest/testapi3");
Test("http://localhost/testapi.ashx");
Test("http://localhost/testapi.svc");
Test("http://localhost/testapi");
Test("http://localhost:81/testapi.php");
}
}
}
Refit is an open source library for type safe REST clients.
The example will only show JSON as that is what Refit is geared towards. There is some XML support though, but I could not get it to work.
I could not get it to work with old .NET 4.x either, but it did work fine with .NET 6.0.
When it works then Refit saves a lot of code. Basically one just put attributes on an interface and then Refit takes care of all the plumbing.
Very cool!
ClientRefit.cs:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Refit;
namespace TestNew
{
public class T1
{
public int F1 { get; set; }
public string F2 { get; set; }
public T1() : this(0, "")
{
}
public T1(int f1, string f2)
{
this.F1 = f1;
this.F2 = f2;
}
public override string ToString()
{
return string.Format("{{{0},{1}}}", F1, F2);
}
}
[Headers("Content-Type: application/json", "Accept: application/json")]
public interface IT1Service
{
[Get("/t1/{f1}")]
Task<T1> GetOne(int f1);
[Get("/t1")]
Task<List<T1>> GetAll();
[Get("/t1")]
Task<List<T1>> GetSome(int start, int finish);
[Post("/t1")]
Task<T1> Post([Body] T1 o);
[Put("/t1")]
Task Put([Body] T1 o);
[Delete("/t1/{f1}")]
Task Delete(int f1);
}
public class ClientRefit
{
private static void TestGetOne(string urlprefix, int f1)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
Console.WriteLine(api.GetOne(f1).Result);
}
private static void TestGetAll(string urlprefix)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
foreach(T1 o in api.GetAll().Result) Console.Write(o);
Console.WriteLine();
}
private static void TestGetSome(string urlprefix, int start, int finish)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
foreach (T1 o in api.GetSome(start, finish).Result) Console.Write(o);
Console.WriteLine();
}
private static void TestPost(string urlprefix, T1 o)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
Console.WriteLine(api.Post(o).Result);
}
private static void TestPut(string urlprefix, T1 o)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
api.Put(o).Wait();
}
private static void TestDelete(string urlprefix, int f1)
{
IT1Service api = RestService.For<IT1Service>(urlprefix);
api.Delete(f1).Wait();
}
private static void Test(string urlprefix)
{
Console.WriteLine("**** " + urlprefix + " (application/json) ****");
TestGetOne(urlprefix, 123);
TestGetAll(urlprefix);
TestGetSome(urlprefix, 10, 12);
TestPost(urlprefix, new T1(123, "ABC"));
TestPut(urlprefix, new T1(123, "ABC"));
TestDelete(urlprefix, 123);
}
public static void Main(string[] args)
{
Test("http://localhost:81/testapi_slim.php");
}
}
}
Web API also has a client framework.
Note that Web API client is geared towards asynchroneus usage, but this example is synchroneous.
ClientWebApi.vb:
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Runtime.Serialization
Namespace Test2
<DataContract(Name := "t1", [Namespace] := "")> _
Public Class T1
<DataMember(Name := "f1")> _
Public Property F1() As Integer
<DataMember(Name := "f2")> _
Public Property F2() As String
Public Sub New()
Me.New(0, "")
End Sub
Public Sub New(f1 As Integer, f2 As String)
Me.F1 = f1
Me.F2 = f2
End Sub
Public Overrides Function ToString() As String
Return String.Format("{{{0},{1}}}", F1, F2)
End Function
End Class
<CollectionDataContract(Name := "t1array", [Namespace] := "")> _
Public Class T1Array
Inherits List(Of T1)
Public Sub New()
Me.New(New T1(-1) {})
End Sub
Public Sub New(array As T1())
MyBase.New(array)
End Sub
End Class
Public Class ClientWebApi
Private Shared Sub TestGetOne(urlprefix As String, f1 As Integer, acctyp As String)
Dim client As New HttpClient()
client.DefaultRequestHeaders.Accept.Clear()
client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue(acctyp))
Dim response As HttpResponseMessage = client.GetAsync(urlprefix & "/t1/" & f1).Result
Console.WriteLine(response.IsSuccessStatusCode)
Console.WriteLine(response.Content.ReadAsAsync(Of T1)().Result)
End Sub
Private Shared Sub TestGetAll(urlprefix As String, acctyp As String)
Dim client As New HttpClient()
client.DefaultRequestHeaders.Accept.Clear()
client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue(acctyp))
Dim response As HttpResponseMessage = client.GetAsync(urlprefix & "/t1").Result
Console.WriteLine(response.IsSuccessStatusCode)
Dim a As T1()
If acctyp = "application/xml" Then
a = response.Content.ReadAsAsync(Of T1Array)().Result.ToArray()
Else
a = response.Content.ReadAsAsync(Of T1())().Result
End If
For Each o As T1 In a
Console.Write(o)
Next
Console.WriteLine()
End Sub
Private Shared Sub TestGetSome(urlprefix As String, start As Integer, finish As Integer, acctyp As String)
Dim client As New HttpClient()
client.DefaultRequestHeaders.Accept.Clear()
client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue(acctyp))
Dim response As HttpResponseMessage = client.GetAsync(urlprefix & "/t1?start=" & start & "&finish=" & finish).Result
Console.WriteLine(response.IsSuccessStatusCode)
Dim a As T1()
If acctyp = "application/xml" Then
a = response.Content.ReadAsAsync(Of T1Array)().Result.ToArray()
Else
a = response.Content.ReadAsAsync(Of T1())().Result
End If
For Each o As T1 In a
Console.Write(o)
Next
Console.WriteLine()
End Sub
Private Shared Sub TestPost(urlprefix As String, contyp As String, o As T1, acctyp As String)
Dim client As New HttpClient()
client.DefaultRequestHeaders.Accept.Clear()
client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue(acctyp))
Dim response As HttpResponseMessage
If contyp = "application/xml" Then
response = client.PostAsXmlAsync(urlprefix & "/t1", o).Result
Else
response = client.PostAsJsonAsync(urlprefix & "/t1", o).Result
End If
Console.WriteLine(response.IsSuccessStatusCode)
Console.WriteLine(response.Content.ReadAsAsync(Of T1)().Result)
End Sub
Private Shared Sub TestPut(urlprefix As String, contyp As String, o As T1)
Dim client As New HttpClient()
Dim response As HttpResponseMessage
If contyp = "application/xml" Then
response = client.PutAsXmlAsync(urlprefix & "/t1/" & o.F1, o).Result
Else
response = client.PutAsJsonAsync(urlprefix & "/t1/" & o.F1, o).Result
End If
Console.WriteLine(response.IsSuccessStatusCode)
End Sub
Private Shared Sub TestDelete(urlprefix As String, f1 As Integer)
Dim client As New HttpClient()
Dim response As HttpResponseMessage = client.DeleteAsync(urlprefix & "/t1/" & f1).Result
Console.WriteLine(response.IsSuccessStatusCode)
End Sub
Private Shared Sub Test(urlprefix As String, typ As String)
Console.WriteLine("**** " & urlprefix & " (" & typ & ") ****")
TestGetOne(urlprefix, 123, typ)
TestGetAll(urlprefix, typ)
TestGetSome(urlprefix, 10, 12, typ)
TestPost(urlprefix, typ, New T1(123, "ABC"), typ)
TestPut(urlprefix, typ, New T1(123, "ABC"))
TestDelete(urlprefix, 123)
End Sub
Private Shared Sub Test(urlprefix As String)
Test(urlprefix, "application/xml")
Test(urlprefix, "application/json")
End Sub
Public Shared Sub Main(args As String())
Test("http://localhost:8080/wsrest/testapi1")
Test("http://localhost:8080/wsrest/testapi2")
Test("http://localhost:8080/wsrest/testapi3")
Test("http://localhost/testapi.ashx")
Test("http://localhost/testapi.svc")
Test("http://localhost/testapi")
Test("http://localhost:81/testapi.php")
End Sub
End Class
End Namespace
Several REST client libraries exists for PHP including:
Requests is a simple library inspired by the Python library of the same name. It was started in 2012 by Ryan McCue. It's homepage is http://requests.ryanmccue.info/.
The serializer/deserializer framework is just a hack to demo the actual web service calls.
clientrequest.php:
<?php
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
$clz = get_class($o);
switch($clz) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
public function __toString() {
return sprintf('{%s,%s}', $this->f1, $this->f2);
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
include('vendor/rmccue/requests/library/Requests.php');
Requests::register_autoloader();
function testGetOne($urlprefix, $f1, $acctyp) {
$response = Requests::get($urlprefix . '/t1/' . $f1, array('Accept' => $acctyp));
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response->body, $acctyp) . "\r\n";
}
function testGetAll($urlprefix, $acctyp) {
$response = Requests::get($urlprefix . '/t1', array('Accept' => $acctyp));
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response->body, $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testGetSome($urlprefix, $start, $finish, $acctyp) {
$response = Requests::get($urlprefix . '/t1?start=' . $start . '&finish=' . $finish, array('Accept' => $acctyp));
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response->body, $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testPost($urlprefix, $contyp, $o, $acctyp) {
$response = Requests::post($urlprefix . '/t1', array('Accept' => $acctyp, 'Content-Type' => $contyp), Util::Object2String($o, $contyp));
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response->body, $acctyp) . "\r\n";
}
function testPut($urlprefix, $contyp, $o) {
$response = Requests::put($urlprefix . '/t1/' . $o->f1, array('Content-Type' => $contyp), Util::Object2String($o, $contyp));
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
}
function testDelete($urlprefix, $f1) {
$response = Requests::delete($urlprefix . '/t1/' . $f1, array());
echo (200 <= $response->status_code && $response->status_code > 300 ? 'true' : 'false') . "\r\n";
}
function test2($urlprefix, $typ) {
echo '**** ' . $urlprefix . ' (' . $typ . ') ****' . "\r\n";
testGetOne($urlprefix, 123, $typ);
testGetAll($urlprefix, $typ);
testGetSome($urlprefix, 10, 12, $typ);
testPost($urlprefix, $typ, new T1(123, 'ABC'), $typ);
testPut($urlprefix, $typ, new T1(123, 'ABC'));
testDelete($urlprefix, 123);
}
function test($urlprefix) {
test2($urlprefix, "application/xml");
test2($urlprefix, "application/json");
}
test("http://localhost:8080/wsrest/testapi1");
test("http://localhost:8080/wsrest/testapi2");
test("http://localhost:8080/wsrest/testapi3");
test("http://localhost/testapi.ashx");
test("http://localhost/testapi.svc");
test("http://localhost/testapi");
test("http://localhost:81/testapi.php");
?>
Several REST client libraries exists for PHP including:
Guzzle is a powerful library that is PSR-7 compliant. It was started by Michael Dowling in 2011. It's homepage is http://guzzlephp.org/.
The serializer/deserializer framework is just a hack to demo the actual web service calls.
clientguzzle.php:
<?php
/*
* Minimalistic/hardcoded serializer/deserializer framework.
*/
class Util {
public static function Object2JSON($o) {
$clz = get_class($o);
switch($clz) {
// simple data classes
case 'T1':
return json_encode($o);
// array wrapper classes
case 'T1Array':
return json_encode($o->array);
default:
throw new Exception('Unknown type: ' . $clz);
}
}
public static function Object2XML($o) {
$clz = get_class($o);
$res = sprintf('<%s>', strtolower($clz));
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($o) as $nam => $val) {
$res .= sprintf('<%s>%s</%s>', $nam, $val, $nam);
}
break;
// array wrapper classes
case 'T1Array':
foreach($o->array as $elm) {
$res .= Util::Object2XML($elm);
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
$res .= sprintf('</%s>', strtolower($clz));
return $res;
}
public static function Object2String($o, $typ) {
if($typ == 'application/xml') {
return Util::Object2XML($o);
} else if($typ == 'application/json') {
return Util::Object2JSON($o);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
public static function XML2Object($clz, $xml) {
$temp = simplexml_load_string($xml);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
if(is_int($res->$nam)) {
$res->$nam = (int)$temp->$nam;
} else {
$res->$nam = (string)$temp->$nam;
}
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
if(is_int($o->$nam)) {
$o->$nam = (int)$elm->$nam;
} else {
$o->$nam = (string)$elm->$nam;
}
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function JSON2Object($clz, $json) {
$temp = json_decode($json);
$res = new $clz();
switch($clz) {
// simple data classes
case 'T1':
foreach(get_object_vars($res) as $nam => $val) {
$res->$nam = $temp->$nam;
}
break;
// array wrapper classes
case 'T1Array':
$elmclz = substr($clz, 0, -5);
foreach($temp as $elm) {
$o = new $elmclz();
foreach(get_object_vars($o) as $nam => $val) {
$o->$nam = $elm->$nam;
}
$res->array[] = $o;
}
break;
default:
throw new Exception('Unknown type: ' . $clz);
}
return $res;
}
public static function String2Object($clz, $s, $typ) {
if($typ == 'application/xml') {
return Util::XML2Object($clz, $s);
} else if($typ == 'application/json') {
return Util::JSON2Object($clz, $s);
} else {
throw new Exception('Unknown type: ' . $typ);
}
}
}
class T1 {
public $f1;
public $f2;
public function __construct($f1 = 0, $f2 = '') {
$this->f1 = $f1;
$this->f2 = $f2;
}
public function __toString() {
return sprintf('{%s,%s}', $this->f1, $this->f2);
}
}
class T1Array {
public $array;
public function __construct($array = array()) {
$this->array = $array;
}
}
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
function testGetOne($urlprefix, $f1, $acctyp) {
$client = new Client();
$request = new Request('GET', $urlprefix . '/t1/' . $f1, array('Accept' => $acctyp));
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response->getBody(), $acctyp) . "\r\n";
}
function testGetAll($urlprefix, $acctyp) {
$client = new Client();
$request = new Request('GET', $urlprefix . '/t1', array('Accept' => $acctyp));
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response->getBody(), $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testGetSome($urlprefix, $start, $finish, $acctyp) {
$client = new Client();
$request = new Request('GET', $urlprefix . '/t1?start=' . $start . '&finish=' . $finish, array('Accept' => $acctyp));
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
$a = Util::String2Object('T1Array', $response->getBody(), $acctyp)->array;
foreach($a as $o) echo $o;
echo "\r\n";
}
function testPost($urlprefix, $contyp, $o, $acctyp) {
$client = new Client();
$request = new Request('POST', $urlprefix . '/t1', array('Content-Type' => $contyp, 'Accept' => $acctyp), Util::Object2String($o, $contyp));
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
echo Util::String2Object('T1', $response->getBody(), $acctyp) . "\r\n";
}
function testPut($urlprefix, $contyp, $o) {
$client = new Client();
$request = new Request('PUT', $urlprefix . '/t1/' . $o->f1, array('Content-Type' => $contyp), Util::Object2String($o, $contyp));
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
}
function testDelete($urlprefix, $f1) {
$client = new Client();
$request = new Request('DELETE', $urlprefix . '/t1/' . $f1, array());
$response = $client->send($request);
echo (200 <= $response->getStatusCode() && $response->getStatusCode() > 300 ? 'true' : 'false') . "\r\n";
}
function test2($urlprefix, $typ) {
echo '**** ' . $urlprefix . ' (' . $typ . ') ****' . "\r\n";
testGetOne($urlprefix, 123, $typ);
testGetAll($urlprefix, $typ);
testGetSome($urlprefix, 10, 12, $typ);
testPost($urlprefix, $typ, new T1(123, 'ABC'), $typ);
testPut($urlprefix, $typ, new T1(123, 'ABC'));
testDelete($urlprefix, 123);
}
function test($urlprefix) {
test2($urlprefix, "application/xml");
test2($urlprefix, "application/json");
}
test("http://localhost:8080/wsrest/testapi1");
test("http://localhost:8080/wsrest/testapi2");
test("http://localhost:8080/wsrest/testapi3");
test("http://localhost/testapi.ashx");
test("http://localhost/testapi.svc");
test("http://localhost/testapi");
test("http://localhost:81/testapi.php");
?>
Requests is a HTTP library for Python by Kenneth Reitz and can be installed with "pip install requests".
Note that my experience with Python is very limited, so the code below may not be optimal.
The serializer/deserializer framework is just a hack to demo the actual web service calls.
clientrequests.py:
import requests
import json
import xml.dom.minidom
#
# Minimalistic/hardcoded serializer/deserializer framework.
#
def Object2Xml(o):
clz = o.__class__.__name__
if clz == 'T1':
return '<t1><f1>%d</f1><f2>%s</f2></t1>' % (o.f1, o.f2)
if clz == 'T1Array':
return '<t1array>' + ''.join(map(lambda o1 : Object2Xml(o1), o.array)) + '</t1array>'
raise Exception('Unknown class: ' + clz)
def Object2Json(o):
clz = o.__class__.__name__
if clz == 'T1':
return json.dumps(o.__dict__)
if clz == 'T1Array':
return '[' + ','.join(map(lambda o1 : Object2Json(o1), o.array)) + ']'
raise Exception('Unknown class: ' + clz)
def Object2String(o, typ):
if typ == 'application/xml':
return Object2Xml(o)
if typ == 'application/json':
return Object2Json(o)
else:
raise Exception('Unknown type: ' + typ)
def Xml2Object(clz, s):
doc = xml.dom.minidom.parseString(s)
if clz == 'T1':
return T1(doc.getElementsByTagName('f1')[0].firstChild.data, doc.getElementsByTagName('f2')[0].firstChild.data)
if clz == 'T1Array':
res = T1Array()
for elm in doc.getElementsByTagName('t1'):
res.array.append(T1(elm.getElementsByTagName('f1')[0].firstChild.data, elm.getElementsByTagName('f2')[0].firstChild.data))
return res
raise Exception('Unknown class: ' + clz)
def Json2Object(clz, s):
o = json.loads(s)
if clz == 'T1':
return T1(o['f1'], o['f2'])
if clz == 'T1Array':
res = T1Array()
for o1 in o:
res.array.append(T1(o1['f1'], o1['f2']))
return res
raise Exception('Unknown class: ' + clz)
def String2Object(clz, s, typ):
if typ == 'application/xml':
return Xml2Object(clz, s)
if typ == 'application/json':
return Json2Object(clz, s)
else:
raise Exception('Unknown type: ' + typ)
#
# Data classes.
#
class T1(object):
def __init__(self, _f1 = 0, _f2 = ''):
self.f1 = _f1
self.f2 = _f2
def __str__(self):
return '{%s,%s}' % (self.f1, self.f2)
class T1Array(object):
def __init__(self):
self.array = []
#
# Web service calls.
#
def testGetOne(url, f1, acctyp):
resp = requests.get(url + '/t1/' + str(f1), headers = { 'Accept': acctyp})
print(int(resp.status_code / 100) == 2)
contyp = resp.headers['Content-Type'].split(';')[0]
o = String2Object('T1', resp.text, contyp)
print(str(o))
def testGetAll(url, acctyp):
resp = requests.get(url + '/t1', headers = { 'Accept': acctyp})
print(int(resp.status_code / 100) == 2)
contyp = resp.headers['Content-Type'].split(';')[0]
a = String2Object('T1Array', resp.text, contyp)
for o in a.array:
print(str(o))
def testGetSome(url, start, finish, acctyp):
resp = requests.get(url + '/t1?start=' + str(start) + '&finish=' + str(finish), headers = { 'Accept': acctyp})
print(int(resp.status_code / 100) == 2)
contyp = resp.headers['Content-Type'].split(';')[0]
a = String2Object('T1Array', resp.text, contyp)
for o in a.array:
print(str(o))
def testPost(url, contyp, o, acctyp):
resp = requests.post(url + '/t1', headers = { 'Content-Type': contyp, 'Accept': acctyp}, data = Object2String(o, contyp))
print(int(resp.status_code / 100) == 2)
contyp2 = resp.headers['Content-Type'].split(';')[0]
o = String2Object('T1', resp.text, contyp2)
print(str(o))
def testPut(url, contyp, o):
resp = requests.put(url + '/t1/' + str(o.f1), headers = { 'Content-Type': contyp}, data = Object2String(o, contyp))
print(int(resp.status_code / 100) == 2)
def testDelete(url, f1):
resp = requests.delete(url + '/t1/' + str(f1))
print(int(resp.status_code / 100) == 2)
def test2(url, acctyp):
print('**** %s (%s) ****' % (url, acctyp))
testGetOne(url, 123, acctyp)
testGetAll(url, acctyp)
testGetSome(url, 10, 12, acctyp)
testPost(url, acctyp, T1(123, 'ABC'), acctyp)
testPut(url, acctyp, T1(123, 'ABC'))
testDelete(url, 123)
def test(url):
test2(url, 'application/xml')
test2(url, 'application/json')
test('http://localhost:8080/wsrest/testapi1');
test('http://localhost:8080/wsrest/testapi2');
test('http://localhost:8080/wsrest/testapi3');
test('http://localhost/testapi.ashx');
test('http://localhost/testapi.svc');
test('http://localhost/testapi');
test('http://localhost:81/testapi.php')
jQuery is a very widely used JavaScript library. First version was released in 2006.
Note that I know practically nothing about JavaScript and jQuery, so the example should not be considered good code just illustrate the ability to work with RESTful web services.
clientjquery.js:
<html>
<head>
<script type="text/javascript" src="/jquery/1.12.4/jquery.min.js"></script>
<script>
function T1tostring(o) {
return '{' + o.f1 + ',' + o.f2 + '}';
}
function displayStatus(div, status) {
$(div).append(status + '<br>');
}
function displayT1(div, o) {
var d = $(div);
d.append('true<br>');
d.append(T1tostring(o) + '<br>');
}
function displayT1Array(div, a) {
var d = $(div);
d.append('true<br>');
for(var i = 0; i < a.length; i++) {
d.append(T1tostring(a[i]));
}
d.append('<br>');
}
function convert_XML_T1(xml) {
var doc = $($.parseXML(xml));
return { "f1":doc.find('f1').text(), "f2":doc.find('f2').text() };
}
function convert_XML_T1Array(xml) {
var res = [];
var doc = $($.parseXML(xml));
doc.find('t1').each(function() { res.push({ "f1":$(this).find('f1').text(), "f2":$(this).find('f2').text() }); });
return res;
}
function convert_T1_XML(o) {
return '<t1><f1>' + o.f1 + '</f1><f2>' + o.f2 + '</f2></t1>';
}
function testGetOne(urlprefix, div, f1, acctyp) {
$.ajax({
method: 'GET',
url: urlprefix + '/t1/' + f1,
headers: { accept: 'application/' + acctyp },
dataType: acctyp,
converters: { 'text xml': function(xml) { return convert_XML_T1(xml); } }
}).done(function(data) {
displayT1(div, data);
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function testGetAll(urlprefix, div, acctyp) {
$.ajax({
method: 'GET',
url: urlprefix + '/t1',
headers: { accept: 'application/' + acctyp },
dataType: acctyp,
converters: { 'text xml': function(xml) { return convert_XML_T1Array(xml); } }
}).done(function(data) {
displayT1Array(div, data);
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function testGetSome(urlprefix, div, start, finish, acctyp) {
$.ajax({
method: 'GET',
url: urlprefix + '/t1?start=' + start + '∓finish=' + finish,
headers: { accept: 'application/' + acctyp },
dataType: acctyp,
converters: { 'text xml': function(xml) { return convert_XML_T1Array(xml); } }
}).done(function(data) {
displayT1Array(div, data);
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function testPost(urlprefix, div, contyp, o, acctyp) {
$.ajax({
method: 'POST',
url: urlprefix + '/t1',
headers: { accept: 'application/' + acctyp, "content-type": 'application/' + contyp },
dataType: acctyp,
data: contyp == 'xml' ? convert_T1_XML(o) : JSON.stringify(o),
converters: { 'text xml': function(xml) { return convert_XML_T1(xml); } }
}).done(function(data) {
displayT1(div, data);
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function testPut(urlprefix, div, f1, contyp, o) {
$.ajax({
method: 'PUT',
url: urlprefix + '/t1/' + f1,
headers: { "content-type": 'application/' + contyp },
data: contyp == 'xml' ? convert_T1_XML(o) : JSON.stringify(o),
}).done(function(data) {
displayStatus(div, 'true');
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function testDelete(urlprefix, div, f1) {
$.ajax({
method: 'DELETE',
url: urlprefix + '/t1/' + f1,
}).done(function(data) {
displayStatus(div, 'true');
}).fail(function(header, status, error) {
displayStatus(div, 'false');
});
}
function test3(urlprefix, div, typ) {
setTimeout(function() { $(div + '_' + typ).append('**** ' + urlprefix + ' (application/' + typ + ') ****<br>'); }, 100);
testGetOne(urlprefix, div + '_' + typ + '_getone', 123, typ);
testGetAll(urlprefix, div + '_' + typ + '_getall', typ);
testGetSome(urlprefix, div + '_' + typ + '_getsome', 10, 12, typ);
testPost(urlprefix, div + '_' + typ + '_post', typ, {"f1":123,"f2":"ABC"}, typ);
testPut(urlprefix, div + '_' + typ + '_put', 123, typ, {"f1":123,"f2":"ABC"});
testDelete(urlprefix, div + '_' + typ + '_delete', 123);
}
function test2(urlprefix, div) {
test3(urlprefix, div, 'xml');
test3(urlprefix, div, 'json');
}
function test() {
test2('http://localhost:8080/wsrest/testapi1', '#java1');
test2('http://localhost:8080/wsrest/testapi2', '#java2');
test2('http://localhost:8080/wsrest/testapi3', '#java3');
test2('http://localhost/testapi.ashx', '#dn1');
test2('http://localhost/testapi.svc', '#dn2');
test2('http://localhost/testapi', '#dn3');
test2('http://localhost:81/testapi.php', '#php');
}
$(document).ready(test());
</script>
</head>
<body>
<div id="java1_xml"></div>
<div id="java1_xml_getone"></div>
<div id="java1_xml_getall"></div>
<div id="java1_xml_getsome"></div>
<div id="java1_xml_post"></div>
<div id="java1_xml_put"></div>
<div id="java1_xml_delete"></div>
<div id="java1_json"></div>
<div id="java1_json_getone"></div>
<div id="java1_json_getall"></div>
<div id="java1_json_getsome"></div>
<div id="java1_json_post"></div>
<div id="java1_json_put"></div>
<div id="java1_json_delete"></div>
<div id="java2_xml"></div>
<div id="java2_xml_getone"></div>
<div id="java2_xml_getall"></div>
<div id="java2_xml_getsome"></div>
<div id="java2_xml_post"></div>
<div id="java2_xml_put"></div>
<div id="java2_xml_delete"></div>
<div id="java2_json"></div>
<div id="java2_json_getone"></div>
<div id="java2_json_getall"></div>
<div id="java2_json_getsome"></div>
<div id="java2_json_post"></div>
<div id="java2_json_put"></div>
<div id="java2_json_delete"></div>
<div id="java3_xml"></div>
<div id="java3_xml_getone"></div>
<div id="java3_xml_getall"></div>
<div id="java3_xml_getsome"></div>
<div id="java3_xml_post"></div>
<div id="java3_xml_put"></div>
<div id="java3_xml_delete"></div>
<div id="java3_json"></div>
<div id="java3_json_getone"></div>
<div id="java3_json_getall"></div>
<div id="java3_json_getsome"></div>
<div id="java3_json_post"></div>
<div id="java3_json_put"></div>
<div id="java3_json_delete"></div>
<div id="dn1_xml"></div>
<div id="dn1_xml_getone"></div>
<div id="dn1_xml_getall"></div>
<div id="dn1_xml_getsome"></div>
<div id="dn1_xml_post"></div>
<div id="dn1_xml_put"></div>
<div id="dn1_xml_delete"></div>
<div id="dn1_json"></div>
<div id="dn1_json_getone"></div>
<div id="dn1_json_getall"></div>
<div id="dn1_json_getsome"></div>
<div id="dn1_json_post"></div>
<div id="dn1_json_put"></div>
<div id="dn1_json_delete"></div>
<div id="dn2_xml"></div>
<div id="dn2_xml_getone"></div>
<div id="dn2_xml_getall"></div>
<div id="dn2_xml_getsome"></div>
<div id="dn2_xml_post"></div>
<div id="dn2_xml_put"></div>
<div id="dn2_xml_delete"></div>
<div id="dn2_json"></div>
<div id="dn2_json_getone"></div>
<div id="dn2_json_getall"></div>
<div id="dn2_json_getsome"></div>
<div id="dn2_json_post"></div>
<div id="dn2_json_put"></div>
<div id="dn2_json_delete"></div>
<div id="dn3_xml"></div>
<div id="dn3_xml_getone"></div>
<div id="dn3_xml_getall"></div>
<div id="dn3_xml_getsome"></div>
<div id="dn3_xml_post"></div>
<div id="dn3_xml_put"></div>
<div id="dn3_xml_delete"></div>
<div id="dn3_json"></div>
<div id="dn3_json_getone"></div>
<div id="dn3_json_getall"></div>
<div id="dn3_json_getsome"></div>
<div id="dn3_json_post"></div>
<div id="dn3_json_put"></div>
<div id="dn3_json_delete"></div>
<div id="php_xml"></div>
<div id="php_xml_getone"></div>
<div id="php_xml_getall"></div>
<div id="php_xml_getsome"></div>
<div id="php_xml_post"></div>
<div id="php_xml_put"></div>
<div id="php_xml_delete"></div>
<div id="php_json"></div>
<div id="php_json_getone"></div>
<div id="php_json_getall"></div>
<div id="php_json_getsome"></div>
<div id="php_json_post"></div>
<div id="php_json_put"></div>
<div id="php_json_delete"></div>
</body>
</html>
For details on how to write a web service as a standalone program outside of web-server/application-server see Web Service - Standalone.
Version | Date | Description |
---|---|---|
1.0 | October 7th 2018 | Initial version |
1.1 | October 29th 2018 | Add PHP Limonade and PHP Slim |
1.2 | November 3rd 2018 | Add Python requests |
1.3 | November 18th 2018 | Add PHP Requests and PHP Guzzle. Add Java Spring MVC. |
1.4 | May 26th 2019 | Add Delphi/Lazarus client. |
1.5 | July 13th 2024 | Add C# Refit. |
See list of all articles here
Please send comments to Arne Vajhøj