Merge changes from Javier
- update on RESTbuck examples - new example - fixed bad typo in WSF_REQUEST Reverted some changes such as - http_client_response: keep the headers as a list to handle multiple message-value with same message-name Fixed simple and simple_file example Improved HTTP_HEADER Changed libcurl implementation for http client - now the header from the context really overwrite any of the session headers - better design which is more strict, and remove any doubt about context's header usage
This commit is contained in:
11
examples/restbucksCRUD/client/README.txt
Normal file
11
examples/restbucksCRUD/client/README.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Make sure to have the Clib generated in the related cURL library
|
||||
|
||||
- if you use EiffelStudio >= 7.0
|
||||
check %ISE_LIBRARY%\library\cURL\spec\%ISE_C_COMPILER%\$ISE_PLATFORM
|
||||
or $ISE_LIBRARY/library/cURL/spec/$ISE_PLATFORM
|
||||
|
||||
- otherwise if you use earlier version
|
||||
check under ext/ise_library/curl/spec/...
|
||||
|
||||
And on Windows, be sure to get the libcurl.dll from %ISE_LIBRARY%\studio\spec\%ISE_PLATFORM%\bin\libcurl.dll
|
||||
|
||||
19
examples/restbucksCRUD/client/client-safe.ecf
Normal file
19
examples/restbucksCRUD/client/client-safe.ecf
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-9-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-9-0 http://www.eiffel.com/developers/xml/configuration-1-9-0.xsd" name="client" uuid="D0059CEB-5F5C-4D21-8C71-842BD0F88468">
|
||||
<target name="client">
|
||||
<root class="RESTBUCK_CLIENT" feature="make"/>
|
||||
<file_rule>
|
||||
<exclude>/.git$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/.svn$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="standard">
|
||||
</option>
|
||||
<setting name="concurrency" value="thread"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<library name="http_client" location="..\..\..\library\client\http_client\http_client-safe.ecf" readonly="false"/>
|
||||
<library name="json" location="..\..\..\contrib\library\text\parser\json\library\json-safe.ecf" readonly="false"/>
|
||||
<library name="thread" location="$ISE_LIBRARY\library\thread\thread-safe.ecf"/>
|
||||
<cluster name="src" location=".\src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
19
examples/restbucksCRUD/client/client.ecf
Normal file
19
examples/restbucksCRUD/client/client.ecf
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-8-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-8-0 http://www.eiffel.com/developers/xml/configuration-1-8-0.xsd" name="client" uuid="D0059CEB-5F5C-4D21-8C71-842BD0F88468">
|
||||
<target name="client">
|
||||
<root class="RESTBUCK_CLIENT" feature="make"/>
|
||||
<file_rule>
|
||||
<exclude>/.git$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/.svn$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true" full_class_checking="true" void_safety="none" syntax="standard">
|
||||
</option>
|
||||
<setting name="concurrency" value="thread"/>
|
||||
<library name="http_client" location="..\..\..\library\client\http_client\http_client.ecf"/>
|
||||
<library name="json" location="..\..\..\contrib\library\text\parser\json\library\json.ecf" readonly="false"/>
|
||||
<library name="base" location="$ISE_LIBRARY/library/base/base.ecf"/>
|
||||
<library name="thread" location="$ISE_LIBRARY/library/thread/thread.ecf"/>
|
||||
<cluster name="src" location="./src" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
170
examples/restbucksCRUD/client/src/restbuck_client.e
Normal file
170
examples/restbucksCRUD/client/src/restbuck_client.e
Normal file
@@ -0,0 +1,170 @@
|
||||
note
|
||||
description : "Objects that ..."
|
||||
author : "$Author$"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class
|
||||
RESTBUCK_CLIENT
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
-- Initialize `Current'.
|
||||
local
|
||||
h: LIBCURL_HTTP_CLIENT
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
resp : HTTP_CLIENT_RESPONSE
|
||||
l_location : detachable READABLE_STRING_8
|
||||
body : STRING
|
||||
do
|
||||
create h.make
|
||||
sess := h.new_session ("http://127.0.0.1:8080")
|
||||
-- Create Order
|
||||
print ("%N Create Order %N")
|
||||
resp := create_order (sess)
|
||||
|
||||
-- Read the Order
|
||||
print ("%N Read Order %N")
|
||||
l_location := resp.headers.at ("Location")
|
||||
resp := read_order (sess, l_location)
|
||||
|
||||
|
||||
-- Update the Order
|
||||
|
||||
if attached resp.body as l_body then
|
||||
body := l_body.as_string_8
|
||||
body.replace_substring_all ("takeAway", "in Shop")
|
||||
print ("%N Update Order %N")
|
||||
resp := update_order (sess, l_location, body)
|
||||
end
|
||||
end
|
||||
|
||||
update_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8; a_body : STRING) : HTTP_CLIENT_RESPONSE
|
||||
local
|
||||
l_headers: HASH_TABLE [READABLE_STRING_8,READABLE_STRING_8]
|
||||
do
|
||||
create Result.make
|
||||
if attached uri as l_uri then
|
||||
sess.set_base_url (l_uri)
|
||||
Result := sess.put ("", Void, a_body )
|
||||
if attached Result as r then
|
||||
-- Show headers
|
||||
l_headers := r.headers
|
||||
from
|
||||
l_headers.start
|
||||
until
|
||||
l_headers.after
|
||||
loop
|
||||
print (l_headers.key_for_iteration)
|
||||
print (":")
|
||||
print (l_headers.item_for_iteration)
|
||||
l_headers.forth
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
-- Show body
|
||||
print (r.body)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
read_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8) : HTTP_CLIENT_RESPONSE
|
||||
local
|
||||
l_headers: HASH_TABLE [READABLE_STRING_8,READABLE_STRING_8]
|
||||
do
|
||||
create Result.make
|
||||
if attached uri as l_uri then
|
||||
sess.set_base_url (l_uri)
|
||||
Result := sess.get ("", Void)
|
||||
if attached Result as r then
|
||||
-- Show headers
|
||||
l_headers := r.headers
|
||||
from
|
||||
l_headers.start
|
||||
until
|
||||
l_headers.after
|
||||
loop
|
||||
print (l_headers.key_for_iteration)
|
||||
print (":")
|
||||
print (l_headers.item_for_iteration)
|
||||
l_headers.forth
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
-- Show body
|
||||
print (r.body)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
create_order (sess: HTTP_CLIENT_SESSION) : HTTP_CLIENT_RESPONSE
|
||||
local
|
||||
s: READABLE_STRING_8
|
||||
j: JSON_PARSER
|
||||
id: detachable STRING
|
||||
context : HTTP_CLIENT_REQUEST_CONTEXT
|
||||
l_headers: HASH_TABLE [READABLE_STRING_8,READABLE_STRING_8]
|
||||
do
|
||||
s := "[
|
||||
{
|
||||
"location":"takeAway",
|
||||
"items":[
|
||||
{
|
||||
"name":"Late",
|
||||
"option":"skim",
|
||||
"size":"Small",
|
||||
"quantity":1
|
||||
}
|
||||
]
|
||||
}
|
||||
]"
|
||||
|
||||
create context.make
|
||||
context.headers.put ("application/json", "Content-Type")
|
||||
Result := sess.post ("/order", context, s)
|
||||
if attached Result as r then
|
||||
-- Show the Headers
|
||||
l_headers := r.headers
|
||||
from
|
||||
l_headers.start
|
||||
until
|
||||
l_headers.after
|
||||
loop
|
||||
print (l_headers.key_for_iteration)
|
||||
print (":")
|
||||
print (l_headers.item_for_iteration)
|
||||
l_headers.forth
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
|
||||
-- Show the Response body
|
||||
if attached r.body as m then
|
||||
create j.make_parser (m)
|
||||
if j.is_parsed and attached j.parse_object as j_o then
|
||||
if attached {JSON_STRING} j_o.item ("id") as l_id then
|
||||
id := l_id.item
|
||||
end
|
||||
print (m)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
invariant
|
||||
-- invariant_clause: True
|
||||
|
||||
end
|
||||
4
examples/restbucksCRUD/license.lic
Normal file
4
examples/restbucksCRUD/license.lic
Normal file
@@ -0,0 +1,4 @@
|
||||
${NOTE_KEYWORD}
|
||||
copyright: "2011-${YEAR}, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
|
||||
293
examples/restbucksCRUD/readme.md
Normal file
293
examples/restbucksCRUD/readme.md
Normal file
@@ -0,0 +1,293 @@
|
||||
Restbuck Eiffel Implementation based on the book of REST in Practice
|
||||
====================================================================
|
||||
This is an ihmplementation of CRUD pattern for manipulate resources, this is the first step to use
|
||||
the HTTP protocol as an application protocol instead of a transport protocol.
|
||||
|
||||
Restbuck Protocol
|
||||
-----------------
|
||||
|
||||
<table>
|
||||
<TR><TH>Verb</TH> <TH>URI or template</TH> <TH>Use</TH></TR>
|
||||
<TR><TD>POST</TD> <TD>/order</TD> <TD>Create a new order, and upon success, receive a Locationheader specifying the new order's URI.</TD></TR>
|
||||
<TR><TD>GET</TD> <TD>/order/{orderId}</TD> <TD>Request the current state of the order specified by the URI.</TD></TR>
|
||||
<TR><TD>PUT</TD> <TD>/order/{orderId}</TD> <TD>Update an order at the given URI with new information, providing the full representation.</TD></TR>
|
||||
<TR><TD>DELETE</TD> <TD>/order/{orderId}</TD> <TD>Logically remove the order identified by the given URI.</TD></TR>
|
||||
</table>
|
||||
|
||||
Resource Represenation
|
||||
----------------------
|
||||
The previous tables shows a contrat, the URI or URI template, allows us to indentify resources, now we will chose a
|
||||
representacion, for this particular case we will use JSON.
|
||||
|
||||
Note: <br/>
|
||||
1. *A resource can have multiple URIs*.<br/>
|
||||
2. *A resource can have multiple Representations*.<br/>
|
||||
|
||||
RESTBUCKS_SERVER
|
||||
----------------
|
||||
This class implement the main entry of our REST CRUD service, we are using a default connector (Nino Connector,
|
||||
using a WebServer written in Eiffel).
|
||||
We are inheriting from URI_TEMPLATE_ROUTED_SERVICE, this allows us to map our service contrat, as is shown in the previous
|
||||
table, the mapping is defined in the feature setup_router, this also show that the class ORDER_HANDLER will be encharge
|
||||
of to handle different type of request to the ORDER resource.
|
||||
|
||||
|
||||
class
|
||||
RESTBUCKS_SERVER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
|
||||
URI_TEMPLATE_ROUTED_SERVICE
|
||||
|
||||
DEFAULT_SERVICE
|
||||
-- Here we are using a default connector using the default Nino Connector,
|
||||
-- but it's possible to use other connector (CGI or FCGI).
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
-- Initialize the router (this will have the request handler and
|
||||
-- their context).
|
||||
do
|
||||
initialize_router
|
||||
make_and_launch
|
||||
end
|
||||
|
||||
create_router
|
||||
do
|
||||
create router.make (2)
|
||||
end
|
||||
|
||||
setup_router
|
||||
local
|
||||
order_handler: ORDER_HANDLER [REQUEST_URI_TEMPLATE_HANDLER_CONTEXT]
|
||||
do
|
||||
create order_handler
|
||||
router.map_with_request_methods ("/order", order_handler, <<"POST">>)
|
||||
router.map_with_request_methods ("/order/{orderid}", order_handler, <<"GET", "DELETE", "PUT">>)
|
||||
end
|
||||
|
||||
feature -- Execution
|
||||
|
||||
execute_default (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- I'm using this method to handle the method not allowed response
|
||||
-- in the case that the given uri does not have a corresponding http method
|
||||
-- to handle it.
|
||||
local
|
||||
h : HTTP_HEADER
|
||||
l_description : STRING
|
||||
l_api_doc : STRING
|
||||
do
|
||||
if req.content_length_value > 0 then
|
||||
req.input.read_string (req.content_length_value.as_integer_32)
|
||||
end
|
||||
create h.make
|
||||
h.put_status ({HTTP_STATUS_CODE}.method_not_allowed)
|
||||
h.put_content_type_text_plain
|
||||
l_api_doc := "%NPlease check the API%NURI:/order METHOD: POST%NURI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
|
||||
l_description := req.request_method + req.request_uri + " is not allowed" + "%N" + l_api_doc
|
||||
h.put_content_length (l_description.count)
|
||||
h.put_current_date
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.method_not_allowed)
|
||||
res.write_header_text (h.string)
|
||||
res.write_string (l_description)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
How to Create an order with POST
|
||||
--------------------------------
|
||||
|
||||
Here is the convention that we are using:
|
||||
POST is used for creation and the server determines the URI of the created resource.
|
||||
If the request POST is SUCCESS, the server will create the order and will response with
|
||||
201 CREATED, the Location header will contains the newly created order's URI,
|
||||
if the request POST is not SUCCESS, the server will response with
|
||||
400 BAD REQUEST, the client send a bad request or
|
||||
500 INTERNAL_SERVER_ERROR, when the server can deliver the request.
|
||||
|
||||
POST /order HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
Connection: keep-alive
|
||||
Content-Length: 196
|
||||
Origin: chrome-extension://fhjcajmcbmldlhcimfajhfbgofnpcjmb
|
||||
Content-Type: application/json
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: es-419,es;q=0.8,en;q=0.6
|
||||
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
|
||||
|
||||
{
|
||||
"location":"takeAway",
|
||||
"items":[
|
||||
{
|
||||
"name":"Late",
|
||||
"option":"skim",
|
||||
"size":"Small",
|
||||
"quantity":1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Status 201 Created
|
||||
Content-Type application/json
|
||||
Content-Length 123
|
||||
Location http://localhost:8080/order/1
|
||||
Date FRI,09 DEC 2011 20:34:20.00 GMT
|
||||
|
||||
{
|
||||
"location" : "takeAway",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
|
||||
How to Read an order with GET
|
||||
-----------------------------
|
||||
Using GET to retrieve resource information.
|
||||
If the GET request is SUCCESS, we response with 200 OK, and a representation of the order
|
||||
If the GET request is not SUCCESS, we response with 404 Resource not found
|
||||
If is a Conditional GET and the resource does not change we send a 304, Resource not modifed
|
||||
|
||||
GET /order/1 HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
Connection: keep-alive
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: es-419,es;q=0.8,en;q=0.6
|
||||
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
|
||||
If-None-Match: 6542EF270D91D3EAF39CFB382E4CEBA7
|
||||
|
||||
Response
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Status 200 OK
|
||||
Content-Type application/json
|
||||
Content-Length 123
|
||||
Date FRI,09 DEC 2011 20:53:46.00 GMT
|
||||
etag 2ED3A40954A95D766FC155682DC8BB52
|
||||
|
||||
{
|
||||
"location" : "takeAway",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
|
||||
|
||||
How to Update an order with PUT
|
||||
-------------------------------
|
||||
A successful PUT request will not create a new resource, instead it will change the state of the resource identified by the current uri.
|
||||
If success we response with 200 and the updated order.
|
||||
404 if the order is not found
|
||||
400 in case of a bad request
|
||||
500 internal server error
|
||||
If the request is a Conditional PUT, and it does not mat we response 415, precondition failed.
|
||||
|
||||
Suposse that we had created an Order with the values shown in the _How to create an order with POST_
|
||||
But we change our decision and we want to stay in the shop.
|
||||
|
||||
|
||||
|
||||
PUT /order/1 HTTP/1.1
|
||||
Content-Length: 122
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
Expect: 100-Continue
|
||||
|
||||
{
|
||||
"location" : "in shop",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Status 200 OK
|
||||
Content-Type application/json
|
||||
Date FRI,09 DEC 2011 21:06:26.00 GMT
|
||||
etag 8767F900674B843E1F3F70BCF3E62403
|
||||
Content-Length 122
|
||||
|
||||
{
|
||||
"location" : "in shop",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
How to Delete an order with DELETE
|
||||
----------------------------------
|
||||
Here we use DELETE to cancel an order, if that order is in state where it can still be canceled.
|
||||
204 if is ok
|
||||
404 Resource not found
|
||||
405 if consumer and service's view of the resouce state is inconsisent
|
||||
500 if we have an internal server error
|
||||
|
||||
|
||||
DELETE /order/1 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
Status 204 No Content
|
||||
Content-Type application/json
|
||||
Date FRI,09 DEC 2011 21:10:51.00 GMT
|
||||
|
||||
If we want to check that the resource does not exist anymore we can try to retrieve a GET /order/1 and we will receive a
|
||||
404 No Found
|
||||
|
||||
GET /order/1 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
|
||||
Response
|
||||
|
||||
HTTP/1.1 404 Not Found
|
||||
|
||||
Status 404 Not Found
|
||||
Content-Type application/json
|
||||
Content-Length 44
|
||||
Date FRI,09 DEC 2011 21:14:17.79 GMT
|
||||
|
||||
The following resource/order/1 is not found
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
1. [How to get a cup of coffe](http://www.infoq.com/articles/webber-rest-workflow)
|
||||
2. [Rest in Practice] (http://restinpractice.com/default.aspx)
|
||||
0
examples/restbucksCRUD/readme.txt
Normal file
0
examples/restbucksCRUD/readme.txt
Normal file
27
examples/restbucksCRUD/restbucks-safe.ecf
Normal file
27
examples/restbucksCRUD/restbucks-safe.ecf
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-9-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-9-0 http://www.eiffel.com/developers/xml/configuration-1-9-0.xsd" name="restbucks" uuid="7C9887BD-4AE4-47F2-A0AA-4BBB6736D433">
|
||||
<target name="restbucks">
|
||||
<root class="RESTBUCKS_SERVER" feature="make"/>
|
||||
<file_rule>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/\.git$</exclude>
|
||||
<exclude>/\.svn$</exclude>
|
||||
</file_rule>
|
||||
<option debug="true" warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="provisional">
|
||||
<debug name="nino" enabled="true"/>
|
||||
<assertions precondition="true" postcondition="true" invariant="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<setting name="concurrency" value="thread"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<library name="connector_nino" location="..\..\library\server\ewsgi\connectors\nino\nino-safe.ecf" readonly="false"/>
|
||||
<library name="default_nino" location="..\..\library\server\wsf\default\nino-safe.ecf" readonly="false"/>
|
||||
<library name="eel" location="..\..\library\crypto\eel\eel-safe.ecf" readonly="false"/>
|
||||
<library name="encoder" location="..\..\library\text\encoder\encoder-safe.ecf" readonly="false"/>
|
||||
<library name="http" location="..\..\library\protocol\http\http-safe.ecf" readonly="false"/>
|
||||
<library name="json" location="..\..\contrib\library\text\parser\json\library\json-safe.ecf" readonly="false"/>
|
||||
<library name="router" location="..\..\library\server\request\router\router-safe.ecf" readonly="false"/>
|
||||
<library name="uri_template" location="..\..\library\protocol\uri_template\uri_template-safe.ecf" readonly="false"/>
|
||||
<library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/>
|
||||
<cluster name="src" location="src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
1
examples/restbucksCRUD/restbucks.rc
Normal file
1
examples/restbucksCRUD/restbucks.rc
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
26
examples/restbucksCRUD/src/database/database_api.e
Normal file
26
examples/restbucksCRUD/src/database/database_api.e
Normal file
@@ -0,0 +1,26 @@
|
||||
note
|
||||
description: "Summary description for {DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
DATABASE_API
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Initialization
|
||||
|
||||
make
|
||||
do
|
||||
create orders.make (10)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
orders: HASH_TABLE [ORDER, STRING]
|
||||
|
||||
;note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
19
examples/restbucksCRUD/src/database/shared_database_api.e
Normal file
19
examples/restbucksCRUD/src/database/shared_database_api.e
Normal file
@@ -0,0 +1,19 @@
|
||||
note
|
||||
description: "Summary description for {SHARED_DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SHARED_DATABASE_API
|
||||
|
||||
feature -- Access
|
||||
|
||||
db_access: DATABASE_API
|
||||
once
|
||||
create Result.make
|
||||
end
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
90
examples/restbucksCRUD/src/domain/item.e
Normal file
90
examples/restbucksCRUD/src/domain/item.e
Normal file
@@ -0,0 +1,90 @@
|
||||
note
|
||||
description: "Summary description for {ITEM}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ITEM
|
||||
inherit
|
||||
ITEM_CONSTANTS
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
make ( a_name : STRING_32 ; a_size:STRING_32; a_option: STRING_32; a_quantity:INTEGER_8)
|
||||
do
|
||||
set_name (a_name)
|
||||
set_size (a_size)
|
||||
set_option (a_option)
|
||||
set_quantity (a_quantity)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
name : STRING
|
||||
-- product name type of Coffee(Late, Cappuccino, Expresso)
|
||||
|
||||
option : STRING
|
||||
-- customization option Milk (skim, semi, whole)
|
||||
|
||||
size : STRING
|
||||
-- small, mediumm large
|
||||
|
||||
quantity :INTEGER
|
||||
|
||||
|
||||
|
||||
|
||||
feature -- Element Change
|
||||
set_name (a_name: STRING)
|
||||
require
|
||||
valid_name: is_valid_coffee_type (a_name)
|
||||
do
|
||||
name := a_name
|
||||
ensure
|
||||
name_assigned : name.same_string(a_name)
|
||||
end
|
||||
|
||||
set_size (a_size: STRING)
|
||||
require
|
||||
valid_size : is_valid_size_option (a_size)
|
||||
do
|
||||
size := a_size
|
||||
ensure
|
||||
size_assigned : size.same_string(a_size)
|
||||
end
|
||||
|
||||
set_option (an_option: STRING)
|
||||
require
|
||||
valid_option : is_valid_milk_type (an_option)
|
||||
do
|
||||
option := an_option
|
||||
ensure
|
||||
option_assigned : option.same_string (an_option)
|
||||
end
|
||||
|
||||
set_quantity (a_quantity: INTEGER)
|
||||
require
|
||||
valid_quantity : a_quantity > 0
|
||||
do
|
||||
quantity := a_quantity
|
||||
ensure
|
||||
quantity_assigned : quantity = a_quantity
|
||||
end
|
||||
|
||||
feature -- Report
|
||||
hash_code: INTEGER
|
||||
--Hash code value
|
||||
do
|
||||
Result := option.hash_code + name.hash_code + size.hash_code + quantity.hash_code
|
||||
end
|
||||
|
||||
|
||||
invariant
|
||||
valid_size : is_valid_size_option (size)
|
||||
valid_coffe : is_valid_coffee_type (name)
|
||||
valid_customization : is_valid_milk_type (option)
|
||||
valid_quantity : quantity > 0
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
54
examples/restbucksCRUD/src/domain/item_constants.e
Normal file
54
examples/restbucksCRUD/src/domain/item_constants.e
Normal file
@@ -0,0 +1,54 @@
|
||||
note
|
||||
description: "Summary description for {ITEM_CONSTANTS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ITEM_CONSTANTS
|
||||
feature -- Access
|
||||
is_valid_coffee_type (a_type: STRING) : BOOLEAN
|
||||
--is `a_type' a valid coffee type
|
||||
do
|
||||
a_type.to_lower
|
||||
coffe_types.compare_objects
|
||||
Result := coffe_types.has (a_type)
|
||||
end
|
||||
|
||||
Coffe_types : ARRAY[STRING]
|
||||
-- List of valid Coffee types
|
||||
once
|
||||
Result := <<"late","cappuccino", "expresso">>
|
||||
end
|
||||
|
||||
is_valid_milk_type (a_type: STRING) : BOOLEAN
|
||||
--is `a_type' a valid milk type
|
||||
do
|
||||
a_type.to_lower
|
||||
milk_types.compare_objects
|
||||
Result := milk_types.has (a_type)
|
||||
end
|
||||
|
||||
Milk_types : ARRAY[STRING]
|
||||
-- List of valid Milk types
|
||||
once
|
||||
Result := <<"skim","semi", "whole">>
|
||||
end
|
||||
|
||||
is_valid_size_option (an_option: STRING) : BOOLEAN
|
||||
--is `an_option' a valid size option
|
||||
do
|
||||
an_option.to_lower
|
||||
size_options.compare_objects
|
||||
Result := size_options.has (an_option)
|
||||
end
|
||||
|
||||
Size_options : ARRAY[STRING]
|
||||
-- List of valid Size_options
|
||||
once
|
||||
Result := <<"small","mediumn", "large">>
|
||||
end
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
170
examples/restbucksCRUD/src/domain/json_order_converter.e
Normal file
170
examples/restbucksCRUD/src/domain/json_order_converter.e
Normal file
@@ -0,0 +1,170 @@
|
||||
note
|
||||
description: "Summary description for {JSON_ORDER_CONVERTER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JSON_ORDER_CONVERTER
|
||||
inherit
|
||||
JSON_CONVERTER
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
make
|
||||
do
|
||||
create object.make ("","","")
|
||||
end
|
||||
feature -- Access
|
||||
object : ORDER
|
||||
|
||||
|
||||
value : detachable JSON_OBJECT
|
||||
feature -- Conversion
|
||||
|
||||
from_json (j: attached like value): detachable like object
|
||||
-- Convert from JSON value. Returns Void if unable to convert
|
||||
local
|
||||
s_id, s_location, s_status: detachable STRING_32
|
||||
q: INTEGER_8
|
||||
o: ORDER
|
||||
i : ITEM
|
||||
l_array : detachable ARRAYED_LIST [JSON_VALUE]
|
||||
is_valid_from_json : BOOLEAN
|
||||
do
|
||||
is_valid_from_json := True
|
||||
|
||||
s_id ?= json.object (j.item (id_key), Void)
|
||||
s_location ?= json.object (j.item (location_key), Void)
|
||||
s_status ?= json.object (j.item (status_key), Void)
|
||||
|
||||
create o.make ("", s_location, s_status)
|
||||
|
||||
if attached {JSON_ARRAY} j.item (items_key) as l_val then
|
||||
l_array := l_val.array_representation
|
||||
from
|
||||
l_array.start
|
||||
until
|
||||
l_array.after
|
||||
loop
|
||||
if attached {JSON_OBJECT} l_array.item_for_iteration as jv then
|
||||
if attached {INTEGER_8} json.object (jv.item (quantity_key), Void) as l_integer then
|
||||
q := l_integer
|
||||
else
|
||||
q := 0
|
||||
end
|
||||
if
|
||||
attached {STRING_32} json.object (jv.item (name_key), Void) as s_name and then
|
||||
attached {STRING_32} json.object (jv.item (size_key), Void) as s_key and then
|
||||
attached {STRING_32} json.object (jv.item (option_key), Void) as s_option
|
||||
then
|
||||
if is_valid_item_customization (s_name, s_key, s_option,q) then
|
||||
create i.make (s_name, s_key, s_option, q)
|
||||
o.add_item (i)
|
||||
else
|
||||
is_valid_from_json := False
|
||||
end
|
||||
else
|
||||
is_valid_from_json := False
|
||||
end
|
||||
end
|
||||
|
||||
l_array.forth
|
||||
end
|
||||
end
|
||||
if not is_valid_from_json or o.items.is_empty then
|
||||
Result := Void
|
||||
else
|
||||
Result := o
|
||||
end
|
||||
end
|
||||
|
||||
to_json (o: like object): like value
|
||||
-- Convert to JSON value
|
||||
local
|
||||
ja : JSON_ARRAY
|
||||
i : ITEM
|
||||
jv: JSON_OBJECT
|
||||
do
|
||||
create Result.make
|
||||
-- Result.put (json.value (o.id), id_key)
|
||||
Result.put (json.value (o.location), location_key)
|
||||
Result.put (json.value (o.status), status_key)
|
||||
from
|
||||
create ja.make_array
|
||||
o.items.start
|
||||
until
|
||||
o.items.after
|
||||
loop
|
||||
i := o.items.item_for_iteration
|
||||
create jv.make
|
||||
jv.put (json.value (i.name), name_key)
|
||||
jv.put (json.value (i.size),size_key)
|
||||
jv.put (json.value (i.quantity), quantity_key)
|
||||
jv.put (json.value (i.option), option_key)
|
||||
ja.add (jv)
|
||||
o.items.forth
|
||||
end
|
||||
Result.put (ja, items_key)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
id_key: JSON_STRING
|
||||
once
|
||||
create Result.make_json ("id")
|
||||
end
|
||||
|
||||
location_key: JSON_STRING
|
||||
once
|
||||
create Result.make_json ("location")
|
||||
end
|
||||
|
||||
status_key: JSON_STRING
|
||||
once
|
||||
create Result.make_json ("status")
|
||||
end
|
||||
|
||||
items_key : JSON_STRING
|
||||
once
|
||||
create Result.make_json ("items")
|
||||
end
|
||||
|
||||
|
||||
name_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_json ("name")
|
||||
end
|
||||
|
||||
size_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_json ("size")
|
||||
end
|
||||
|
||||
quantity_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_json ("quantity")
|
||||
end
|
||||
|
||||
|
||||
option_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_json ("option")
|
||||
end
|
||||
feature -- Validation
|
||||
|
||||
is_valid_item_customization ( name : STRING_32; size: STRING_32; option : STRING_32; quantity : INTEGER_8 ) : BOOLEAN
|
||||
local
|
||||
ic : ITEM_CONSTANTS
|
||||
do
|
||||
create ic
|
||||
Result := ic.is_valid_coffee_type (name) and ic.is_valid_milk_type (option) and ic.is_valid_size_option (size) and quantity > 0
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
114
examples/restbucksCRUD/src/domain/order.e
Normal file
114
examples/restbucksCRUD/src/domain/order.e
Normal file
@@ -0,0 +1,114 @@
|
||||
note
|
||||
description: "Summary description for {ORDER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ORDER
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
|
||||
make ( an_id : detachable STRING_32; a_location: detachable STRING_32; a_status: detachable STRING_32)
|
||||
do
|
||||
create {ARRAYED_LIST [ITEM]} items.make (10)
|
||||
if an_id /= Void then
|
||||
set_id (an_id)
|
||||
else
|
||||
set_id ("")
|
||||
end
|
||||
if a_location /= Void then
|
||||
set_location (a_location)
|
||||
else
|
||||
set_location ("")
|
||||
end
|
||||
if a_status /= Void then
|
||||
set_status (a_status)
|
||||
else
|
||||
set_status ("")
|
||||
end
|
||||
revision := 0
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
id : STRING_32
|
||||
location : STRING_32
|
||||
items: LIST[ITEM]
|
||||
status : STRING_32
|
||||
revision : INTEGER
|
||||
|
||||
feature -- element change
|
||||
|
||||
set_id (an_id : STRING_32)
|
||||
do
|
||||
id := an_id
|
||||
ensure
|
||||
id_assigned : id.same_string (an_id)
|
||||
end
|
||||
|
||||
set_location (a_location : STRING_32)
|
||||
do
|
||||
location := a_location
|
||||
ensure
|
||||
location_assigned : location.same_string (a_location)
|
||||
end
|
||||
|
||||
set_status (a_status : STRING_32)
|
||||
do
|
||||
status := a_status
|
||||
ensure
|
||||
status_asigned : status.same_string (a_status)
|
||||
end
|
||||
|
||||
add_item (a_item : ITEM)
|
||||
require
|
||||
valid_item: a_item /= Void
|
||||
do
|
||||
items.force (a_item)
|
||||
ensure
|
||||
has_item : items.has (a_item)
|
||||
end
|
||||
|
||||
add_revision
|
||||
do
|
||||
revision := revision + 1
|
||||
ensure
|
||||
revision_incremented : old revision + 1 = revision
|
||||
end
|
||||
|
||||
feature -- Etag
|
||||
|
||||
etag : STRING_32
|
||||
-- Etag generation for Order objects
|
||||
do
|
||||
Result := hash_code.out + revision.out
|
||||
end
|
||||
|
||||
|
||||
feature -- Output
|
||||
|
||||
feature -- Report
|
||||
|
||||
hash_code: INTEGER_32
|
||||
-- Hash code value
|
||||
do
|
||||
from
|
||||
items.start
|
||||
Result := items.item.hash_code
|
||||
until
|
||||
items.off
|
||||
loop
|
||||
Result:= ((Result \\ 8388593) |<< 8) + items.item.hash_code
|
||||
items.forth
|
||||
end
|
||||
if items.count > 1 then
|
||||
Result := Result \\ items.count
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
56
examples/restbucksCRUD/src/domain/order_validation.e
Normal file
56
examples/restbucksCRUD/src/domain/order_validation.e
Normal file
@@ -0,0 +1,56 @@
|
||||
note
|
||||
description: "Summary description for {ORDER_TRANSITIONS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ORDER_VALIDATION
|
||||
feature -- Access
|
||||
|
||||
is_valid_status_state (a_status: STRING) : BOOLEAN
|
||||
--is `a_status' a valid coffee order state
|
||||
do
|
||||
a_status.to_lower
|
||||
Order_states.compare_objects
|
||||
Result := Order_states.has (a_status)
|
||||
end
|
||||
|
||||
Order_states : ARRAY[STRING]
|
||||
-- List of valid status states
|
||||
once
|
||||
Result := <<"submitted","pay","payed", "cancel","canceled","prepare","prepared","deliver","completed">>
|
||||
end
|
||||
|
||||
|
||||
is_valid_transition (order:ORDER a_status : STRING) :BOOLEAN
|
||||
-- Given the current order state, determine if the transition is valid
|
||||
do
|
||||
a_status.to_lower
|
||||
if order.status.same_string ("submitted") then
|
||||
Result := a_status.same_string ("pay") or a_status.same_string ("cancel") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("pay") then
|
||||
Result := a_status.same_string ("payed") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("cancel") then
|
||||
Result := a_status.same_string ("canceled") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("payed") then
|
||||
Result := a_status.same_string ("prepared") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("prepared") then
|
||||
Result := a_status.same_string ("deliver") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("deliver") then
|
||||
Result := a_status.same_string ("completed") or order.status.same_string (a_status)
|
||||
end
|
||||
end
|
||||
|
||||
is_state_valid_to_update ( a_status : STRING) : BOOLEAN
|
||||
-- Given the current state `a_status' of an order, is possible to update the order?
|
||||
do
|
||||
if a_status.same_string ("submitted") or else a_status.same_string ("pay") or else a_status.same_string ("payed") then
|
||||
Result := true
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
19
examples/restbucksCRUD/src/domain/shared_order_validation.e
Normal file
19
examples/restbucksCRUD/src/domain/shared_order_validation.e
Normal file
@@ -0,0 +1,19 @@
|
||||
note
|
||||
description: "Summary description for {SHARED_ORDER_VALIDATION}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SHARED_ORDER_VALIDATION
|
||||
|
||||
feature
|
||||
order_validation : ORDER_VALIDATION
|
||||
once
|
||||
create Result
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
363
examples/restbucksCRUD/src/resource/order_handler.e
Normal file
363
examples/restbucksCRUD/src/resource/order_handler.e
Normal file
@@ -0,0 +1,363 @@
|
||||
note
|
||||
description: "{ORDER_HANDLER} handle the resources that we want to expose"
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ORDER_HANDLER [C -> REQUEST_HANDLER_CONTEXT]
|
||||
inherit
|
||||
REQUEST_HANDLER [C]
|
||||
REQUEST_RESOURCE_HANDLER_HELPER [C]
|
||||
redefine
|
||||
do_get,
|
||||
do_post,
|
||||
do_put,
|
||||
do_delete
|
||||
end
|
||||
SHARED_DATABASE_API
|
||||
SHARED_EJSON
|
||||
REFACTORING_HELPER
|
||||
SHARED_ORDER_VALIDATION
|
||||
|
||||
feature -- execute
|
||||
|
||||
execute (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Execute request handler
|
||||
do
|
||||
execute_methods (ctx, req, res)
|
||||
end
|
||||
|
||||
feature -- API DOC
|
||||
|
||||
api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
|
||||
|
||||
feature -- HTTP Methods
|
||||
|
||||
do_get (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Using GET to retrieve resource information.
|
||||
-- If the GET request is SUCCESS, we response with
|
||||
-- 200 OK, and a representation of the order
|
||||
-- If the GET request is not SUCCESS, we response with
|
||||
-- 404 Resource not found
|
||||
-- If is a Condition GET and the resource does not change we send a
|
||||
-- 304, Resource not modifed
|
||||
local
|
||||
id : STRING
|
||||
do
|
||||
if attached req.orig_path_info as orig_path then
|
||||
id := get_order_id_from_path (orig_path)
|
||||
if attached retrieve_order (id) as l_order then
|
||||
if is_conditional_get (req, l_order) then
|
||||
handle_resource_not_modified_response ("The resource" + orig_path + "does not change", ctx, req, res)
|
||||
else
|
||||
compute_response_get (ctx, req, res, l_order)
|
||||
end
|
||||
else
|
||||
handle_resource_not_found_response ("The following resource" + orig_path + " is not found ", ctx, req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_conditional_get (req : WSF_REQUEST; l_order : ORDER) : BOOLEAN
|
||||
-- Check if If-None-Match is present and then if there is a representation that has that etag
|
||||
-- if the representation hasn't changed, we return TRUE
|
||||
-- then the response is a 304 with no entity body returned.
|
||||
local
|
||||
etag_util : ETAG_UTILS
|
||||
do
|
||||
if attached req.meta_string_variable ("HTTP_IF_NONE_MATCH") as if_none_match then
|
||||
create etag_util
|
||||
if if_none_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_get (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
etag_utils : ETAG_UTILS
|
||||
do
|
||||
create h.make
|
||||
create etag_utils
|
||||
h.put_status ({HTTP_STATUS_CODE}.ok)
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.request_time as time then
|
||||
h.add_header ("Date:" + time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
|
||||
end
|
||||
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.write_header_text (h.string)
|
||||
res.write_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
do_put (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Updating a resource with PUT
|
||||
-- A successful PUT request will not create a new resource, instead it will
|
||||
-- change the state of the resource identified by the current uri.
|
||||
-- If success we response with 200 and the updated order.
|
||||
-- 404 if the order is not found
|
||||
-- 400 in case of a bad request
|
||||
-- 500 internal server error
|
||||
-- If the request is a Conditional PUT, and it does not mat we response
|
||||
-- 415, precondition failed.
|
||||
local
|
||||
l_put: STRING
|
||||
l_order : detachable ORDER
|
||||
id : STRING
|
||||
do
|
||||
if attached req.orig_path_info as orig_path then
|
||||
id := get_order_id_from_path (orig_path)
|
||||
req.input.read_string (req.content_length_value.as_integer_32)
|
||||
l_put := req.input.last_string
|
||||
l_order := extract_order_request(l_put)
|
||||
if l_order /= Void and then db_access.orders.has_key (id) then
|
||||
l_order.set_id (id)
|
||||
if is_valid_to_update(l_order) then
|
||||
if is_conditional_put (req, l_order) then
|
||||
update_order( l_order)
|
||||
compute_response_put (ctx, req, res, l_order)
|
||||
else
|
||||
handle_precondition_fail_response ("", ctx, req, res)
|
||||
end
|
||||
else
|
||||
--| FIXME: Here we need to define the Allow methods
|
||||
handle_resource_conflict_response (l_put +"%N There is conflict while trying to update the order, the order could not be update in the current state", ctx, req, res)
|
||||
end
|
||||
else
|
||||
handle_bad_request_response (l_put +"%N is not a valid ORDER, maybe the order does not exist in the system", ctx, req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_conditional_put (req : WSF_REQUEST; order : ORDER) : BOOLEAN
|
||||
-- Check if If-Match is present and then if there is a representation that has that etag
|
||||
-- if the representation hasn't changed, we return TRUE
|
||||
local
|
||||
etag_util : ETAG_UTILS
|
||||
do
|
||||
if attached retrieve_order (order.id) as l_order then
|
||||
if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then
|
||||
create etag_util
|
||||
if if_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
|
||||
Result := True
|
||||
end
|
||||
else
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
compute_response_put (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
etag_utils : ETAG_UTILS
|
||||
do
|
||||
create h.make
|
||||
create joc.make
|
||||
create etag_utils
|
||||
json.add_converter(joc)
|
||||
|
||||
create h.make
|
||||
h.put_status ({HTTP_STATUS_CODE}.ok)
|
||||
h.put_content_type_application_json
|
||||
if attached req.request_time as time then
|
||||
h.add_header ("Date:" +time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
|
||||
end
|
||||
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
h.put_content_length (jv.representation.count)
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.write_header_text (h.string)
|
||||
res.write_string (jv.representation)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
do_delete (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Here we use DELETE to cancel an order, if that order is in state where
|
||||
-- it can still be canceled.
|
||||
-- 200 if is ok
|
||||
-- 404 Resource not found
|
||||
-- 405 if consumer and service's view of the resouce state is inconsisent
|
||||
-- 500 if we have an internal server error
|
||||
local
|
||||
id: STRING
|
||||
do
|
||||
if attached req.orig_path_info as orig_path then
|
||||
id := get_order_id_from_path (orig_path)
|
||||
if db_access.orders.has_key (id) then
|
||||
if is_valid_to_delete (id) then
|
||||
delete_order( id)
|
||||
compute_response_delete (ctx, req, res)
|
||||
else
|
||||
--| FIXME: Here we need to define the Allow methods
|
||||
handle_method_not_allowed_response (orig_path + "%N There is conflict while trying to delete the order, the order could not be deleted in the current state", ctx, req, res)
|
||||
end
|
||||
else
|
||||
handle_resource_not_found_response (orig_path + " not found in this server", ctx, req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_delete (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
local
|
||||
h : HTTP_HEADER
|
||||
do
|
||||
create h.make
|
||||
h.put_status ({HTTP_STATUS_CODE}.no_content)
|
||||
h.put_content_type_application_json
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.no_content)
|
||||
res.write_header_text (h.string)
|
||||
end
|
||||
|
||||
do_post (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Here the convention is the following.
|
||||
-- POST is used for creation and the server determines the URI
|
||||
-- of the created resource.
|
||||
-- If the request post is SUCCESS, the server will create the order and will response with
|
||||
-- HTTP_RESPONSE 201 CREATED, the Location header will contains the newly created order's URI
|
||||
-- if the request post is not SUCCESS, the server will response with
|
||||
-- HTTP_RESPONSE 400 BAD REQUEST, the client send a bad request
|
||||
-- HTTP_RESPONSE 500 INTERNAL_SERVER_ERROR, when the server can deliver the request
|
||||
local
|
||||
l_post: STRING
|
||||
do
|
||||
req.input.read_string (req.content_length_value.as_integer_32)
|
||||
l_post := req.input.last_string
|
||||
if attached extract_order_request (l_post) as l_order then
|
||||
save_order (l_order)
|
||||
compute_response_post (ctx, req, res, l_order)
|
||||
else
|
||||
handle_bad_request_response (l_post +"%N is not a valid ORDER", ctx, req, res)
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_post (ctx: C; req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
l_location : STRING
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create h.make
|
||||
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
|
||||
h.put_status ({HTTP_STATUS_CODE}.created)
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.http_host as host then
|
||||
l_location := "http://" + host + req.request_uri + "/" + l_order.id
|
||||
h.put_location (l_location)
|
||||
end
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.created)
|
||||
res.write_header_text (h.string)
|
||||
res.write_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- URI helper methods
|
||||
|
||||
get_order_id_from_path (a_path: READABLE_STRING_32) : STRING
|
||||
do
|
||||
Result := a_path.split ('/').at (3)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation Repository Layer
|
||||
|
||||
retrieve_order ( id : STRING) : detachable ORDER
|
||||
-- get the order by id if it exist, in other case, Void
|
||||
do
|
||||
Result := db_access.orders.item (id)
|
||||
end
|
||||
|
||||
save_order (an_order: ORDER)
|
||||
-- save the order to the repository
|
||||
local
|
||||
i : INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
until
|
||||
not db_access.orders.has_key ((db_access.orders.count + i).out)
|
||||
loop
|
||||
i := i + 1
|
||||
end
|
||||
an_order.set_id ((db_access.orders.count + i).out)
|
||||
an_order.set_status ("submitted")
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
|
||||
is_valid_to_delete ( an_id : STRING) : BOOLEAN
|
||||
-- Is the order identified by `an_id' in a state whre it can still be deleted?
|
||||
do
|
||||
if attached retrieve_order (an_id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_valid_to_update (an_order: ORDER) : BOOLEAN
|
||||
-- Check if there is a conflict while trying to update the order
|
||||
do
|
||||
if attached retrieve_order (an_order.id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) and then order_validation.is_valid_status_state (an_order.status) and then
|
||||
order_validation.is_valid_transition (l_order, an_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_order (an_order: ORDER)
|
||||
-- update the order to the repository
|
||||
do
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
delete_order (an_order: STRING)
|
||||
-- update the order to the repository
|
||||
do
|
||||
db_access.orders.remove (an_order)
|
||||
end
|
||||
|
||||
extract_order_request (l_post : STRING) : detachable ORDER
|
||||
-- extract an object Order from the request, or Void
|
||||
-- if the request is invalid
|
||||
local
|
||||
parser : JSON_PARSER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
create parser.make_parser (l_post)
|
||||
if attached parser.parse as jv and parser.is_parsed then
|
||||
Result ?= json.object (jv, "ORDER")
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
80
examples/restbucksCRUD/src/restbucks_server.e
Normal file
80
examples/restbucksCRUD/src/restbucks_server.e
Normal file
@@ -0,0 +1,80 @@
|
||||
note
|
||||
description : "Objects that ..."
|
||||
author : "$Author$"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class
|
||||
RESTBUCKS_SERVER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
|
||||
URI_TEMPLATE_ROUTED_SERVICE
|
||||
|
||||
ROUTED_SERVICE_HELPER
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
local
|
||||
s: DEFAULT_SERVICE
|
||||
do
|
||||
initialize_router
|
||||
create s.make_and_launch (agent execute)
|
||||
end
|
||||
|
||||
create_router
|
||||
do
|
||||
create router.make (2)
|
||||
end
|
||||
|
||||
setup_router
|
||||
local
|
||||
order_handler: ORDER_HANDLER [REQUEST_URI_TEMPLATE_HANDLER_CONTEXT]
|
||||
do
|
||||
create order_handler
|
||||
router.map_with_request_methods ("/order", order_handler, <<"POST">>)
|
||||
router.map_with_request_methods ("/order/{orderid}", order_handler, <<"GET", "DELETE", "PUT">>)
|
||||
end
|
||||
|
||||
feature -- Execution
|
||||
|
||||
execute_default (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- I'm using this method to handle the method not allowed response
|
||||
-- in the case that the given uri does not have a corresponding http method
|
||||
-- to handle it.
|
||||
local
|
||||
h : HTTP_HEADER
|
||||
l_description : STRING
|
||||
l_api_doc : STRING
|
||||
do
|
||||
if req.content_length_value > 0 then
|
||||
req.input.read_string (req.content_length_value.as_integer_32)
|
||||
end
|
||||
create h.make
|
||||
h.put_status ({HTTP_STATUS_CODE}.method_not_allowed)
|
||||
h.put_content_type_text_plain
|
||||
l_api_doc := "%NPlease check the API%NURI:/order METHOD: POST%NURI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
|
||||
l_description := req.request_method + req.request_uri + " is not allowed" + "%N" + l_api_doc
|
||||
h.put_content_length (l_description.count)
|
||||
h.put_current_date
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.method_not_allowed)
|
||||
res.write_header_text (h.string)
|
||||
res.write_string (l_description)
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
5949 Hollister Ave., Goleta, CA 93117 USA
|
||||
Telephone 805-685-1006, Fax 805-685-6869
|
||||
Website http://www.eiffel.com
|
||||
Customer support http://support.eiffel.com
|
||||
]"
|
||||
end
|
||||
33
examples/restbucksCRUD/src/utils/etag_utils.e
Normal file
33
examples/restbucksCRUD/src/utils/etag_utils.e
Normal file
@@ -0,0 +1,33 @@
|
||||
note
|
||||
description: "Summary description for {ETAG_UTILS}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ETAG_UTILS
|
||||
|
||||
inherit
|
||||
ARRAY_FACILITIES
|
||||
|
||||
feature -- Access
|
||||
|
||||
md5_digest (a_string: STRING): STRING
|
||||
-- Cryptographic hash function that produces a 128-bit (16-byte) hash value, based on `a_string'
|
||||
local
|
||||
md5: MD5
|
||||
output: SPECIAL [NATURAL_8]
|
||||
do
|
||||
create md5.make
|
||||
create output.make_filled (0, 16)
|
||||
md5.sink_string (a_string)
|
||||
md5.do_final (output, 0)
|
||||
Result := as_natural_32_be (output, 0).to_hex_string
|
||||
Result.append (as_natural_32_be (output, 4).to_hex_string)
|
||||
Result.append (as_natural_32_be (output, 8).to_hex_string)
|
||||
Result.append (as_natural_32_be (output, 12).to_hex_string)
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2011, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
Reference in New Issue
Block a user