Updated, improved and cleaned RESTbucks example.

Moved it under "rest" subfolder.
This commit is contained in:
2017-02-13 16:23:38 +01:00
parent a44c4d9a16
commit b56aec67a9
35 changed files with 1826 additions and 1419 deletions

View File

@@ -0,0 +1,3 @@
The current example has a main target for the server: "restbucks"
But we also provide "policy_driven_restbucks" target which is using the
policy-driven framework than help coder fulfill HTTP expectations.

View 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

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-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>/.svn$</exclude>
<exclude>/EIFGENs$</exclude>
</file_rule>
<option warning="true" void_safety="all">
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="http_client" location="..\..\..\..\library\network\http_client\http_client-safe.ecf"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<cluster name="src" location=".\src\" recursive="true"/>
</target>
</system>

View File

@@ -0,0 +1,177 @@
note
description : "Objects that ..."
author : "$Author$"
date : "$Date$"
revision : "$Revision$"
class
RESTBUCK_CLIENT
create
make
feature {NONE} -- Initialization
make
-- Initialize `Current'.
local
h: NET_HTTP_CLIENT
sess: HTTP_CLIENT_SESSION
resp : detachable HTTP_CLIENT_RESPONSE
l_location : detachable READABLE_STRING_8
body : STRING
do
create h
sess := h.new_session ("http://127.0.0.1:" + server_port.out)
-- Uncomment the following 2 lines, if you use fiddler2 web debugging tool
-- sess.set_is_debug (True)
-- sess.set_proxy ("127.0.0.1", 8888)
-- Create Order
print ("> Create Order %N")
resp := create_order (sess)
display_response (resp)
-- Read the Order
l_location := resp.header ("Location")
if l_location /= Void then
print ("> Read Order from " + l_location + " %N")
resp := read_order (sess, l_location)
display_response (resp)
else
print ("> Previous order creation failed, no location returned!%N")
end
-- Update the Order
if resp /= Void and then attached resp.body as l_body then
body := l_body.as_string_8
body.replace_substring_all ("takeAway", "in Shop")
print ("> Update Order: change location from [takeAway] to [in Shop] %N")
resp := update_order (sess, l_location, body)
display_response (resp)
end
-- Pay the Order
if resp /= Void and then attached resp.body as l_body then
body := l_body.as_string_8
body.replace_substring_all ("%"status%":%"submitted%"", "%"status%":%"paid%"")
print ("> Pay Order: should trigger validation error!%N")
resp := update_order (sess, l_location, body)
display_response (resp)
end
end
update_order (sess: HTTP_CLIENT_SESSION; uri: detachable READABLE_STRING_8; a_body: STRING): detachable HTTP_CLIENT_RESPONSE
local
context : HTTP_CLIENT_REQUEST_CONTEXT
do
if uri /= Void then
sess.set_base_url (uri)
create context.make
context.headers.put ("application/json", "Content-Type")
Result := sess.put ("", context, a_body )
end
end
read_order (sess: HTTP_CLIENT_SESSION; uri: detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
do
if uri /= Void then
sess.set_base_url (uri)
Result := sess.get ("", Void)
end
end
create_order (sess: HTTP_CLIENT_SESSION) : HTTP_CLIENT_RESPONSE
local
s: READABLE_STRING_8
j: JSON_PARSER
context : HTTP_CLIENT_REQUEST_CONTEXT
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)
end
display_response (resp: detachable HTTP_CLIENT_RESPONSE)
do
io.error.put_string (create {STRING}.make_filled ('-', 40))
io.error.put_new_line
if resp = Void then
io.error.put_string ("ERROR: No response~%N")
else
if resp.error_occurred and attached resp.error_message as err_msg then
io.error.put_string ("[ERROR] ")
io.error.put_string (err_msg)
io.error.put_new_line
end
-- Display response status and header
io.error.put_string ("Status code: " + resp.status.out + "%N")
across
resp.headers as l_headers
loop
io.error.put_string (l_headers.item.name)
io.error.put_string (":")
io.error.put_string (l_headers.item.value)
io.error.put_new_line
end
-- Show the Response body
if not resp.error_occurred and attached resp.body as m then
io.error.put_string (m)
io.error.put_new_line
end
end
io.error.put_string (create {STRING}.make_filled ('-', 40))
io.error.put_new_line
end
feature {NONE} -- Implementation
server_port: INTEGER
local
f: PLAIN_TEXT_FILE
p: PATH
s: STRING
do
create p.make_current
p := p.extended ("..").extended ("server.ini")
create f.make_with_path (p)
if f.exists and then f.is_access_readable then
f.open_read
from
until
f.exhausted or f.end_of_file or Result > 0
loop
f.read_line
s := f.last_string
if s.starts_with_general ("port=") then
s.remove_head (5)
if s.is_integer then
Result := s.to_integer
end
end
end
f.close
end
if Result <= 0 then
Result := 80
end
end
end

View 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)"

View File

@@ -0,0 +1,278 @@
Restbuck Eiffel Implementation based on the book of REST in Practice
====================================================================
This is an implementation 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
-----------------
* Method `POST` with URI `/order` : Create a new order, and upon success, receive a Locationheader specifying the new order's URI.
* Method `GET` with URI-template `/order/{orderId}` : Request the current state of the order specified by the URI.
* Method `PUT` with URI-template `/order/{orderId}` : Update an order at the given URI with new information, providing the full representation.
* Method `DELETE` with URI-tempalte `/order/{orderId}` : Logically remove the order identified by the given URI.
Resource Represenation
----------------------
The previous tables shows a contrat, the URI or URI-template, allows us to indentify resources, now we will chose a
representation, for this particular case we will use JSON.
Note:
1. *A resource can have multiple URIs*.
2. *A resource can have multiple Representations*.
RESTBUCKS_SERVER
----------------
This class implement the main entry of our REST CRUD service, we are using a default connector (Standalone Connector,
using a WebServer written in Eiffel).
We are inheriting from `WSF_ROUTED_SKELETON_EXECUTION`, 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 in charge
of handling different types of request to the ORDER resource.
```
class RESTBUCKS_SERVER_EXECUTION
inherit
WSF_ROUTED_SKELETON_EXECUTION
undefine
requires_proxy
end
WSF_NO_PROXY_POLICY
SHARED_RESTBUCKS_API
create
make
feature {NONE} -- Initialization
setup_router
local
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
do
setup_order_handler (router)
create doc.make_hidden (router)
router.handle ("/api/doc", doc, router.methods_GET)
end
setup_order_handler (a_router: WSF_ROUTER)
local
order_handler: ORDER_HANDLER
do
create order_handler.make ("orderid", a_router)
router.handle ("/order", order_handler, router.methods_POST)
router.handle ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
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"
} ]
}
```
note:
curl -vv http://localhost:9090/order -H "Content-Type: application/json" -d "{\"location\":\"takeAway\",\"items\":[{\"name\":\"Late\",\"option\":\"skim\",\"size\":\"Small\",\"quantity\":1}]}" -X POST
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"
} ]
}
```
note:
curl -vv http://localhost:9090/order/1
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)

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="restbucks" uuid="2773FEAA-448F-410E-BEDE-9298C4749066">
<target name="restbucks_common">
<file_rule>
<exclude>/EIFGENs$</exclude>
<exclude>/\.git$</exclude>
<exclude>/\.svn$</exclude>
</file_rule>
<option void_safety="all">
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="conneg" location="..\..\..\library\network\protocol\content_negotiation\conneg-safe.ecf"/>
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf" readonly="false"/>
<library name="default_standalone" location="..\..\..\library\server\wsf\default\standalone-safe.ecf" readonly="false"/>
<library name="encoder" location="..\..\..\library\text\encoder\encoder-safe.ecf" readonly="false"/>
<library name="http" location="..\..\..\library\network\protocol\http\http-safe.ecf" readonly="false"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
<library name="uri_template" location="..\..\..\library\text\parser\uri_template\uri_template-safe.ecf" readonly="false"/>
<library name="wsf" location="..\..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/>
<library name="wsf_extension" location="..\..\..\library\server\wsf\wsf_extension-safe.ecf" readonly="false"/>
</target>
<target name="restbucks" extends="restbucks_common">
<root class="RESTBUCKS_SERVER" feature="make"/>
<option debug="true" warning="true" void_safety="all">
<debug name="standalone" enabled="true"/>
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
</option>
<cluster name="src" location="src\" recursive="true">
<file_rule>
<exclude>/policy_driven_resource$</exclude>
</file_rule>
</cluster>
</target>
<target name="policy_driven_restbucks" extends="restbucks_common">
<root class="RESTBUCKS_SERVER" feature="make"/>
<option debug="true" warning="true" void_safety="all">
<debug name="standalone" enabled="true"/>
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
</option>
<library name="wsf_policy_driven" location="..\..\..\library\server\wsf\wsf_policy_driven-safe.ecf" readonly="false"/>
<cluster name="src" location="src\" recursive="true">
<file_rule>
<exclude>/resource/order_handler.e$</exclude>
</file_rule>
</cluster>
</target>
</system>

View File

@@ -0,0 +1,2 @@
port=9090
verbose=true

View File

@@ -0,0 +1,167 @@
note
description: "Summary description for {ORDER_JSON_SERIALIZATION}."
date: "$Date$"
revision: "$Revision$"
class
ORDER_JSON_SERIALIZATION
inherit
JSON_SERIALIZER
JSON_DESERIALIZER
feature -- Conversion
to_json (obj: detachable ANY; ctx: JSON_SERIALIZER_CONTEXT): JSON_VALUE
-- JSON value representing the JSON serialization of Eiffel value `obj', in the eventual context `ctx'.
local
j_order: JSON_OBJECT
j_item: JSON_OBJECT
ja: JSON_ARRAY
do
has_error := False
if attached {ORDER} obj as l_order then
create j_order.make_with_capacity (4)
Result := j_order
j_order.put_string (l_order.id, id_key)
if attached l_order.location as loc then
j_order.put_string (loc, location_key)
end
j_order.put_string (l_order.status, status_key)
if attached l_order.items as l_items and then not l_items.is_empty then
create ja.make (l_items.count)
j_order.put (ja, items_key)
across
l_items as ic
loop
if attached {ORDER_ITEM} ic.item as l_item then
create j_item.make_with_capacity (4)
j_item.put_string (l_item.name, name_key)
j_item.put_string (l_item.size, size_key)
j_item.put_integer (l_item.quantity, quantity_key)
j_item.put_string (l_item.option, option_key)
ja.extend (j_item)
end
end
end
else
create {JSON_NULL} Result
has_error := True
end
end
from_json (a_json: detachable JSON_VALUE; ctx: JSON_DESERIALIZER_CONTEXT; a_type: detachable TYPE [detachable ANY]): detachable ORDER
-- <Precursor/>.
local
l_status: detachable STRING_32
q: NATURAL_8
is_valid_from_json: BOOLEAN
l_name, l_size, l_option: detachable READABLE_STRING_32
do
has_error := False
is_valid_from_json := True
if attached {JSON_OBJECT} a_json as jobj then
-- Either new order (i.e no id and no status)
-- or an existing order with `id` and `status` (could be Void, thus use default).
if attached {JSON_STRING} jobj.item (status_key) as j_status then
l_status := j_status.unescaped_string_32
end
if
attached {JSON_STRING} jobj.item (id_key) as j_id
then
-- Note: the id has to be valid string 8 value!
create Result.make (j_id.unescaped_string_8, l_status)
elseif attached {JSON_NUMBER} jobj.item (id_key) as j_id then
-- Be flexible and accept json number as id.
create Result.make (j_id.integer_64_item.out, l_status)
else
create Result.make_empty
if l_status /= Void then
Result.set_status (l_status)
end
end
if attached {JSON_STRING} jobj.item (location_key) as j_location then
Result.set_location (j_location.unescaped_string_32)
end
if attached {JSON_ARRAY} jobj.item (items_key) as j_items then
across
j_items as ic
loop
if attached {JSON_OBJECT} ic.item as j_item then
if
attached {JSON_NUMBER} j_item.item (quantity_key) as j_quantity and then
j_quantity.integer_64_item < {NATURAL_8}.Max_value
then
q := j_quantity.integer_64_item.to_natural_8
else
q := 0
end
if
attached {JSON_STRING} j_item.item (name_key) as j_name and then
attached {JSON_STRING} j_item.item (size_key) as j_size and then
attached {JSON_STRING} j_item.item (option_key) as j_option
then
l_name := j_name.unescaped_string_32
l_size := j_size.unescaped_string_32
l_option := j_option.unescaped_string_32
if is_valid_item_customization (l_name, l_size, l_option, q) then
Result.add_item (create {ORDER_ITEM}.make (l_name, l_size, l_option, q))
else
is_valid_from_json := False
end
else
is_valid_from_json := False
end
end
end
end
if not is_valid_from_json or Result.items.is_empty then
Result := Void
end
else
is_valid_from_json := a_json = Void or else attached {JSON_NULL} a_json
Result := Void
end
has_error := not is_valid_from_json
end
has_error: BOOLEAN
-- Error occurred during last `from_json` or `to_json` execution.
feature {NONE} -- Implementation
id_key: STRING = "id"
location_key: STRING = "location"
status_key: STRING = "status"
items_key: STRING = "items"
name_key: STRING = "name"
size_key: STRING = "size"
quantity_key: STRING = "quantity"
option_key: STRING = "option"
feature -- Validation
is_valid_item_customization (name: READABLE_STRING_GENERAL; size: READABLE_STRING_GENERAL; option: READABLE_STRING_GENERAL; quantity: NATURAL_8): BOOLEAN
local
ic: ORDER_ITEM_VALIDATION
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-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,44 @@
note
description: "[
Basic database for simple example.
(no concurrency access control, ...)
]"
date: "$Date$"
revision: "$Revision$"
deferred class
BASIC_DATABASE
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
deferred
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
deferred
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
deferred
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
deferred
ensure
has_id: cl_entry_id.item /= Void
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
require
has_item: has (a_entry_type, a_id)
deferred
ensure
has_not_item: not has (a_entry_type, a_id)
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,177 @@
note
description: "[
Basic database for simple example using JSON files.
(no concurrency access control, ...)
]"
date: "$Date$"
revision: "$Revision$"
class
BASIC_JSON_FS_DATABASE
inherit
BASIC_DATABASE
create
make
feature {NONE} -- Initialization
make (a_location: PATH)
local
d: DIRECTORY
do
location := a_location
create serialization
ensure_directory_exists (a_location)
end
feature -- Access serialization
serialization: JSON_SERIALIZATION
feature -- Access
location: PATH
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
local
d: DIRECTORY
do
create d.make_with_path (location.extended (entry_type_name (a_entry_type)))
if d.exists then
across
d.entries as ic
loop
if attached ic.item.extension as e and then e.is_case_insensitive_equal ("json") then
Result := Result + 1
end
end
end
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
local
fut: FILE_UTILITIES
do
Result := fut.file_path_exists (entry_path (a_entry_type, a_id))
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
local
f: RAW_FILE
s: STRING
do
create f.make_with_path (entry_path (a_entry_type, a_id))
if f.exists then
create s.make (f.count)
f.open_read
from
until
f.exhausted or f.end_of_file
loop
f.read_stream (1_024)
s.append (f.last_string)
end
f.close
Result := serialization.from_json_string (s, a_entry_type)
end
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
local
f: RAW_FILE
l_id: detachable READABLE_STRING_GENERAL
do
l_id := cl_entry_id.item
if l_id = Void then
l_id := next_identifier (a_entry_type)
cl_entry_id.replace (l_id)
end
create f.make_with_path (entry_path (a_entry_type, l_id))
ensure_directory_exists (f.path.parent)
f.open_write
f.put_string (serialization.to_json (a_entry).representation)
f.close
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
local
f: RAW_FILE
do
create f.make_with_path (entry_path (a_entry_type, a_id))
if f.exists and then f.is_access_writable then
f.delete
end
end
feature {NONE} -- Implementation
ensure_directory_exists (dn: PATH)
local
d: DIRECTORY
do
create d.make_with_path (dn)
if not d.exists then
d.recursive_create_dir
end
end
entry_path (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): PATH
do
Result := location.extended (entry_type_name (a_entry_type)).extended (a_id).appended_with_extension ("json")
end
entry_type_name (a_entry_type: TYPE [detachable ANY]): STRING
do
Result := a_entry_type.name.as_lower
Result.prune_all ('!')
end
last_id_file_path (a_entry_type: TYPE [detachable ANY]): PATH
do
Result := location.extended (entry_type_name (a_entry_type)).extended ("last-id")
end
next_identifier (a_entry_type: TYPE [detachable ANY]): STRING_8
local
i: NATURAL_64
f: RAW_FILE
s: STRING
do
create f.make_with_path (last_id_file_path (a_entry_type))
ensure_directory_exists (f.path.parent)
if f.exists then
create s.make (f.count)
f.open_read
f.read_line
s := f.last_string
f.close
if s.is_natural_64 then
i := s.to_natural_64
end
end
from
i := i + 1
Result := i.out
until
not has (a_entry_type, Result)
loop
i := i + 1
Result := i.out
end
f.open_write
f.put_string (Result)
f.close
end
invariant
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,102 @@
note
description: "[
Basic database for simple example based on memory.
]"
date: "$Date$"
revision: "$Revision$"
class
BASIC_MEMORY_DATABASE
inherit
BASIC_DATABASE
create
make
feature {NONE} -- Initialization
make
do
create collections.make (0)
end
collections: HASH_TABLE [STRING_TABLE [detachable ANY], TYPE [detachable ANY]]
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
do
if attached collections.item (a_entry_type) as tb then
Result := tb.count
end
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
do
if attached collections.item (a_entry_type) as tb then
Result := tb.has_key (a_id)
end
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
do
if attached collections.item (a_entry_type) as tb then
Result := tb.item (a_id)
end
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
local
tb: detachable STRING_TABLE [detachable ANY]
l_id: detachable READABLE_STRING_GENERAL
do
tb := collections.item (a_entry_type)
if tb = Void then
create tb.make (100)
collections.force (tb, a_entry_type)
end
l_id := cl_entry_id.item
if l_id = Void then
l_id := next_identifier (a_entry_type)
cl_entry_id.replace (l_id)
end
tb.force (a_entry, l_id)
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
do
if attached collections.item (a_entry_type) as tb then
tb.remove (a_id)
end
end
feature {NONE} -- Implementation
next_identifier (a_entry_type: TYPE [detachable ANY]): STRING_8
local
i: INTEGER
f: RAW_FILE
s: STRING
do
if attached collections.item (a_entry_type) as tb then
i := tb.count
else
i := 0
end
from
i := i + 1
Result := i.out
until
not has (a_entry_type, Result)
loop
i := i + 1
Result := i.out
end
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,173 @@
note
description: "Summary description for {RESTBUCKS_API}."
date: "$Date$"
revision: "$Revision$"
class
RESTBUCKS_API
create
make
feature {NONE} -- Initialization
make
local
db: BASIC_JSON_FS_DATABASE
do
create db.make (database_path)
db.serialization.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
database := db
end
feature -- Access
orders_count: INTEGER
-- Number of existing orders.
do
Result := database.count_of ({ORDER})
end
has_order (a_id: READABLE_STRING_GENERAL): BOOLEAN
do
Result := database.has ({ORDER}, a_id)
end
order (a_id: READABLE_STRING_GENERAL): detachable ORDER
do
if attached {ORDER} database.item ({ORDER}, a_id) as o then
Result := o
end
end
feature -- Element change
submit_order (a_order: ORDER)
-- Submit new order `a_order`.
require
no_id: not a_order.has_id
do
a_order.mark_submitted
save_order (a_order)
end
save_order (a_order: ORDER)
local
cl: CELL [detachable READABLE_STRING_GENERAL]
do
a_order.add_revision
if a_order.has_id then
create cl.put (a_order.id)
else
create cl.put (Void)
end
database.save ({ORDER}, a_order, cl)
if attached cl.item as l_new_id then
if l_new_id.is_valid_as_string_8 then
a_order.set_id (l_new_id.to_string_8)
else
check valid_id: False end
end
end
ensure
has_id: a_order.has_id
incremented_revision: a_order.revision > old (a_order.revision)
end
delete_order (a_order: ORDER)
do
database.delete ({ORDER}, a_order.id)
end
feature -- Access: order status
is_valid_status_state (a_status: STRING): BOOLEAN
-- Is `a_status' a valid order state
do
Result := Order_states.has (a_status.as_lower)
end
Order_states : ARRAY [STRING]
-- List of valid status states
once
Result := <<
status_unset,
status_submitted,
status_pay, status_payed,
status_cancel, status_canceled,
status_prepare, status_prepared,
status_deliver,
status_completed
>>
Result.compare_objects
end
is_valid_transition (a_order: ORDER; a_new_status: STRING): BOOLEAN
-- Is transition from `a_order.status` to `a_new_status` valid for `a_order`?
local
l_order_status: READABLE_STRING_GENERAL
l_new_status: STRING
do
l_order_status := a_order.status
l_new_status := a_new_status.as_lower
if l_order_status.same_string (l_new_status) then
-- Same status is valid, if it is a valid status
Result := is_valid_status_state (l_new_status)
else
if l_order_status.same_string (status_submitted) then
Result := l_new_status.same_string (status_pay)
or l_new_status.same_string (status_cancel)
elseif l_order_status.same_string (status_pay) then
Result := l_new_status.same_string (status_payed)
elseif l_order_status.same_string (status_cancel) then
Result := l_new_status.same_string (status_canceled)
elseif l_order_status.same_string (status_payed) then
Result := l_new_status.same_string (status_prepared)
elseif l_order_status.same_string (status_prepared) then
Result := l_new_status.same_string (status_deliver)
elseif l_order_status.same_string (status_deliver) then
Result := l_new_status.same_string (status_completed)
end
end
end
is_state_valid_to_update (a_status : STRING) : BOOLEAN
-- Is it possible to update order with status `a_status`?
do
if
a_status.same_string (status_submitted)
or else a_status.same_string (status_pay)
or else a_status.same_string (status_payed)
then
Result := True
end
end
feature -- Constants: order status
status_unset: STRING = "unset"
status_submitted: STRING = "submitted"
status_pay: STRING = "pay"
status_payed: STRING = "payed"
status_cancel: STRING = "cancel"
status_canceled: STRING = "canceled"
status_prepare: STRING = "prepare"
status_prepared: STRING = "prepared"
status_deliver: STRING = "deliver"
status_completed: STRING = "completed"
feature {NONE} -- Access
database: BASIC_DATABASE
feature {NONE} -- Implementation
database_path: PATH
once
create Result.make_from_string ("db")
end
;note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,61 @@
note
description: "Summary description for {SHARED_RESTBUCKS_API}."
date: "$Date$"
revision: "$Revision$"
class
SHARED_RESTBUCKS_API
feature -- Access: bridget to api
has_order (a_id: READABLE_STRING_GENERAL): BOOLEAN
do
Result := api.has_order (a_id)
end
order (a_id: READABLE_STRING_GENERAL): detachable ORDER
do
Result := api.order (a_id)
end
feature -- Element change
submit_order (a_order: ORDER)
-- Submit new order `a_order`.
require
no_id: not a_order.has_id
do
api.submit_order (a_order)
ensure
a_order.has_id
a_order.is_submitted
end
update_order (a_order: ORDER)
-- Update the order to the repository
require
a_order.has_id
do
api.save_order (a_order)
ensure
a_order_with_id: a_order.has_id
end
delete_order (a_order: ORDER)
require
a_order_with_id: a_order.has_id
do
api.delete_order (a_order)
end
feature -- Access
api: RESTBUCKS_API
once
create Result.make
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,135 @@
note
description: "Summary description for {ORDER}."
date: "$Date$"
revision: "$Revision$"
class
ORDER
create
make,
make_empty
feature -- Initialization
make_empty
do
create {ARRAYED_LIST [ORDER_ITEM]} items.make (10)
revision := 0
set_id ("")
status := {RESTBUCKS_API}.status_unset
end
make (a_id: READABLE_STRING_8; a_status: detachable READABLE_STRING_GENERAL)
do
make_empty
set_id (a_id)
if a_status /= Void then
set_status (a_status)
else
check status.same_string_general ({RESTBUCKS_API}.status_unset) end
end
end
feature -- Access
id: IMMUTABLE_STRING_8
location: detachable STRING_32
status: STRING_32
-- Status of the order, see {RESTBUCKS_API}.Order_states
revision: INTEGER
items: LIST [ORDER_ITEM]
feature -- Status report
has_id: BOOLEAN
-- Has valid identifier `id`?
do
Result := not id.is_whitespace and not id.is_case_insensitive_equal ("0")
end
is_submitted: BOOLEAN
do
Result := status.is_case_insensitive_equal_general ({RESTBUCKS_API}.status_submitted)
end
feature -- element change
set_id (a_id: READABLE_STRING_8)
do
if attached {IMMUTABLE_STRING_8} a_id as l_id then
id := l_id
else
create id.make_from_string (a_id)
end
ensure
id_assigned : a_id.same_string (id)
end
set_location (a_location: detachable READABLE_STRING_GENERAL)
do
if a_location = Void then
location := Void
else
create location.make_from_string_general (a_location)
end
ensure
location_assigned: (a_location = Void implies location = Void)
or (a_location /= Void implies attached location as loc and then a_location.same_string (loc))
end
mark_submitted
do
status := {RESTBUCKS_API}.status_submitted
end
set_status (a_status: READABLE_STRING_GENERAL)
do
create status.make_from_string_general (a_status)
ensure
status_assigned : a_status.same_string (status)
end
add_item (a_item: ORDER_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 -- 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-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,93 @@
note
description: "Summary description for {ORDER_ITEM}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER_ITEM
inherit
ORDER_ITEM_VALIDATION
create
make
feature -- Initialization
make (a_name: STRING_32; a_size: STRING_32; a_option: STRING_32; a_quantity: NATURAL_8)
do
set_name (a_name)
set_size (a_size)
set_option (a_option)
set_quantity (a_quantity)
end
feature -- Access
name: READABLE_STRING_32
-- product name type of Coffee(Late, Cappuccino, Expresso)
option: READABLE_STRING_32
-- customization option Milk (skim, semi, whole)
size: READABLE_STRING_32
-- small, mediumm large
quantity: NATURAL_8
feature -- Element Change
set_name (a_name: like name)
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: like size)
require
valid_size: is_valid_size_option (a_size)
do
size := a_size
ensure
size_assigned: size.same_string (a_size)
end
set_option (a_option: like option)
require
valid_option: is_valid_milk_type (a_option)
do
option := a_option
ensure
option_assigned: option.same_string (a_option)
end
set_quantity (a_quantity: NATURAL_8)
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-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,51 @@
note
description: "Summary description for {ORDER_ITEM_VALIDATION}."
date: "$Date$"
revision: "$Revision$"
class
ORDER_ITEM_VALIDATION
feature -- Access
is_valid_coffee_type (a_type: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_type' a valid coffee type
do
Result := across coffe_types as ic some a_type.is_case_insensitive_equal (ic.item) end
end
Coffe_types: ARRAY [STRING]
-- List of valid Coffee types
once
Result := <<"late", "cappuccino", "expresso">>
end
is_valid_milk_type (a_type: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_type' a valid milk type
do
Result := across milk_types as ic some a_type.is_case_insensitive_equal (ic.item) end
end
Milk_types: ARRAY [STRING]
-- List of valid Milk types
once
Result := <<"skim", "semi", "whole">>
end
is_valid_size_option (a_option: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_option' a valid size option
do
Result := across size_options as ic some a_option.is_case_insensitive_equal (ic.item) end
end
Size_options: ARRAY [STRING]
-- List of valid Size_options
once
Result := <<"small", "medium", "large">>
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,559 @@
note
description: "{ORDER_HANDLER} handle the resources that we want to expose"
author: ""
date: "$Date$"
revision: "$Revision$"
class ORDER_HANDLER
inherit
WSF_SKELETON_HANDLER
SHARED_RESTBUCKS_API
REFACTORING_HELPER
WSF_RESOURCE_HANDLER_HELPER
rename
execute_options as helper_execute_options,
handle_internal_server_error as helper_handle_internal_server_error
end
create
make
feature {NONE} -- Initialization
make (a_orderid_path_parameter_name: READABLE_STRING_GENERAL; a_router: WSF_ROUTER)
do
orderid_path_parameter_name := a_orderid_path_parameter_name
make_with_router (a_router)
end
orderid_path_parameter_name: READABLE_STRING_GENERAL
feature -- Execution variables
Order_execution_variable: STRING = "ORDER"
-- Execution variable used by application
Generated_content_execution_variable: STRING = "GENERATED_CONTENT"
-- Execution variable used by application
Extracted_order_execution_variable: STRING = "EXTRACTED_ORDER"
-- Execution variable used by application
feature -- Documentation
description: READABLE_STRING_GENERAL
-- General description for self-generated documentation;
-- The specific URI templates supported will be described automatically
do
Result := "Create, Read, Update or Delete an ORDER."
end
feature -- Access
is_chunking (req: WSF_REQUEST): BOOLEAN
-- Will the response to `req' using chunked transfer encoding?
do
-- No.
end
includes_response_entity (req: WSF_REQUEST): BOOLEAN
-- Does the response to `req' include an entity?
-- Method will be DELETE, POST, PUT or an extension method.
do
Result := False
-- At present, there is no support for this except for DELETE.
end
conneg (req: WSF_REQUEST): SERVER_CONTENT_NEGOTIATION
-- Content negotiatior for all requests
once
create Result.make ({HTTP_MIME_TYPES}.application_json, "en", "UTF-8", "identity")
end
mime_types_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<{HTTP_MIME_TYPES}.application_json>>)
Result.compare_objects
end
languages_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Language header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"en">>)
Result.compare_objects
end
charsets_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Charset header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"UTF-8">>)
Result.compare_objects
end
encodings_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Encoding header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"identity">>)
Result.compare_objects
end
max_age (req: WSF_REQUEST): NATURAL
-- Maximum age in seconds before response to `req` is considered stale;
-- This is used to generate a Cache-Control: max-age header.
-- Return 0 to indicate already expired.
-- Return Never_expires to indicate never expires.
do
-- All our responses are considered stale.
end
is_freely_cacheable (req: WSF_REQUEST): BOOLEAN
-- Should the response to `req' be freely cachable in shared caches?
-- If `True', then a Cache-Control: public header will be generated.
do
-- definitely not!
end
private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
-- Header names intended for a single user.
-- If non-Void, then a Cache-Control: private header will be generated.
-- Returning an empty list prevents the entire response from being served from a shared cache.
do
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
end
non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
-- Header names that will not be sent from a cache without revalidation;
-- If non-Void, then a Cache-Control: no-cache header will be generated.
-- Returning an empty list prevents the response being served from a cache
-- without revalidation.
do
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
end
is_sensitive (req: WSF_REQUEST): BOOLEAN
-- Is the response to `req' of a sensitive nature?
-- If `True' then a Cache-Control: no-store header will be generated.
do
Result := True
-- since it's commercial data.
end
allowed_cross_origins (req: WSF_REQUEST): detachable STRING
-- Value for Access-Control-Allow-Origin header;
-- If supplied, should be a single URI, or the values "*" or "null".
-- This is currently supported only for GET requests, and POSTs that functions as GET.
do
if req.is_get_head_request_method then
Result := "*"
end
end
matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN
-- Is `a_etag' a match for resource requested in `req'?
-- If `a_strong' then the strong comparison function must be used.
local
l_id: STRING
do
l_id := order_id_from_request (req)
if l_id /= Void and then has_order (l_id) then
check attached order (l_id) as l_order then
-- postcondition of `has_key'
Result := a_etag.same_string (order_etag (l_order))
end
end
end
etag (req: WSF_REQUEST): detachable READABLE_STRING_8
-- Optional Etag for `req' in the requested variant
do
if attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
Result := order_etag (l_order)
end
end
last_modified (req: WSF_REQUEST): detachable DATE_TIME
-- When representation of resource selected in `req' was last modified;
-- SHOULD be set whenever it can reasonably be determined.
do
end
modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN
-- Has resource requested in `req' been modified since `a_date_time' (UTC)?
do
-- We don't track this information. It is safe to always say yes.
Result := True
end
feature -- Measurement
content_length (req: WSF_REQUEST): NATURAL
-- Length of entity-body of the response to `req'
do
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
-- We only call this for GET/HEAD in this example.
Result := l_response.count.as_natural_32
end
end
allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN
-- The resource named in `req' does not exist, and this is a POST. Do we allow it?
do
-- No.
end
feature -- Status report
finished (req: WSF_REQUEST): BOOLEAN
-- Has the last chunk been generated for `req'?
do
-- precondition is never met
end
feature -- Execution
check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER)
-- Call `a_helper.set_resource_exists' to indicate that `req.path_translated'
-- is the name of an existing resource.
-- We also put the order into `req.execution_variable (Order_execution_variable)' for GET or HEAD responses.
local
l_id: STRING
do
if req.is_post_request_method then
a_helper.set_resource_exists
-- because only /order is defined to this handler for POST
else
-- the request is of the form /order/{orderid}
l_id := order_id_from_request (req)
if l_id /= Void and then has_order (l_id) then
a_helper.set_resource_exists
if req.is_get_head_request_method then
check attached order (l_id) as l_order then
-- postcondition `item_if_found' of `has_key'
req.set_execution_variable (Order_execution_variable, l_order)
end
end
end
end
ensure then
order_saved_only_for_get_head: attached {ORDER} req.execution_variable (Order_execution_variable) implies req.is_get_head_request_method
end
feature -- GET/HEAD content
ensure_content_available (req: WSF_REQUEST)
-- Commence generation of response text (entity-body).
-- If not chunked, then this will create the entire entity-body so as to be available
-- for a subsequent call to `content'.
-- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions
-- are used, then this will also generate the chunk extension for the first chunk.
-- We save the text in `req.execution_variable (Generated_content_execution_variable)'
-- We ignore the results of content negotiation, as there is only one possible combination.
do
check attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
-- precondition get_or_head and postcondition order_saved_only_for_get_head of `check_resource_exists' and
if attached order_to_json (l_order) as jv then
req.set_execution_variable (Generated_content_execution_variable, jv.representation)
else
req.set_execution_variable (Generated_content_execution_variable, "")
end
end
ensure then
generated_content_set_for_get_head: req.is_get_head_request_method implies
attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable)
end
content (req: WSF_REQUEST): READABLE_STRING_8
-- Non-chunked entity body in response to `req';
-- We only call this for GET/HEAD in this example.
do
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
Result := l_response
end
end
next_chunk (req: WSF_REQUEST): TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8]
-- Next chunk of entity body in response to `req';
-- The second field of the result is an optional chunk extension.
do
-- precondition `is_chunking' is never met, but we need a dummy `Result'
-- to satisfy the compiler in void-safe mode
Result := ["", Void]
end
generate_next_chunk (req: WSF_REQUEST)
-- Prepare next chunk (including optional chunk extension) of entity body in response to `req'.
-- This is not called for the first chunk.
do
-- precondition `is_chunking' is never met
end
feature -- DELETE
delete (req: WSF_REQUEST)
-- Delete resource named in `req' or set an error on `req.error_handler'.
do
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_valid_to_delete (l_order) then
delete_order (l_order)
else
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.method_not_allowed, "DELETE not valid",
"There is conflict while trying to delete the order, the order could not be deleted in the current state")
end
else
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.not_found, "DELETE not valid",
"There is no such order to delete")
end
end
delete_queued (req: WSF_REQUEST): BOOLEAN
-- Has resource named by `req' been queued for deletion?
do
-- No
end
feature -- PUT/POST
is_entity_too_large (req: WSF_REQUEST): BOOLEAN
-- Is the entity stored in `req.execution_variable (Request_entity_execution_variable)' too large for the application?
do
-- No. We don't care for this example.
end
check_content_headers (req: WSF_REQUEST)
-- Check we can support all content headers on request entity.
-- Set `req.execution_variable (Content_check_code_execution_variable)' to {NATURAL} zero if OK, or 415 or 501 if not.
do
-- We don't bother for this example. Note that this is equivalent to setting zero.
end
create_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Create new resource in response to a PUT request when `check_resource_exists' returns `False'.
-- Implementor must set error code of 200 OK or 500 Server Error.
do
-- We don't support creating a new resource with PUT. But this can't happen
-- with our router mappings, so we don't bother to set a 500 response.
end
append_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Create new resource in response to a POST request.
-- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
submit_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response ("Not a valid order", req, res)
end
end
check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Check we can support all content headers on request entity.
-- Set `req.execution_variable (Conflict_check_code_execution_variable)' to {NATURAL} zero if OK, or 409 if not.
-- In the latter case, write the full error response to `res'.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
if not is_valid_to_update (l_order) then
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
handle_resource_conflict_response (l_order.out +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res)
end
else
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
--| This ought to be a 500, as if attached should probably be check attached. But as yet I lack a proof.
handle_resource_conflict_response ("There is conflict while trying to update the order, the order could not be update in the current state", req, res)
end
end
check_request (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Check that the request entity is a valid request.
-- The entity is available as `req.execution_variable (Conflict_check_code_execution_variable)'.
-- Set `req.execution_variable (Request_check_code_execution_variable)' to {NATURAL} zero if OK, or 400 if not.
-- In the latter case, write the full error response to `res'.
local
l_order: detachable ORDER
l_id: STRING
do
if attached {READABLE_STRING_8} req.execution_variable (Request_entity_execution_variable) as l_request then
l_order := order_from_request_input_data (l_request)
if req.is_put_request_method then
l_id := order_id_from_request (req)
if l_order /= Void and then l_id /= Void and then has_order (l_id) then
l_order.set_id (l_id)
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
req.set_execution_variable (Extracted_order_execution_variable, l_order)
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
handle_bad_request_response (l_request +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res)
end
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
req.set_execution_variable (Extracted_order_execution_variable, l_order)
end
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
handle_bad_request_response ("Request is not a valid ORDER", req, res)
end
end
update_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Perform the update requested in `req'.
-- Write a response to `res' with a code of 204 or 500.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
update_order (l_order)
compute_response_put (req, res, l_order)
else
handle_internal_server_error (res)
end
end
feature -- HTTP Methods
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
do
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
if attached order_to_json (l_order) as jv then
res.header.put_content_length (jv.representation.count)
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_string (jv.representation)
end
end
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
l_msg : STRING
do
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
res.header.put_content_length (l_msg.count)
res.header.put_location (req.absolute_script_url (req.request_uri + "/" + l_order.id))
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.created)
res.put_string (l_msg)
end
end
feature {NONE} -- URI helper methods
order_id_from_request (req: WSF_REQUEST): detachable STRING_8
do
if attached {WSF_STRING} req.path_parameter (orderid_path_parameter_name) as p_id then
Result := p_id.url_encoded_value -- the ORDER id has to be valid STRING 8 value.
end
end
feature {NONE} -- Implementation Repository Layer
is_valid_to_delete (a_order: ORDER): BOOLEAN
-- Is the order identified by `a_id' in a state whre it can still be deleted?
do
if attached order (a_order.id) as l_order then
if api.is_state_valid_to_update (l_order.status) then
Result := True
end
end
end
is_valid_to_update (a_order: ORDER): BOOLEAN
-- Check if there is a conflict while trying to update the order.
do
if attached order (a_order.id) as l_existing_order then
if
api.is_state_valid_to_update (l_existing_order.status) and then
api.is_valid_status_state (a_order.status) and then
api.is_valid_transition (l_existing_order, a_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
feature -- Helpers
order_etag (a_order: ORDER): STRING_8
local
etag_utils: ETAG_UTILS
do
create etag_utils
Result := etag_utils.md5_digest (a_order.hash_code.out + a_order.revision.out)
end
order_from_request (req: WSF_REQUEST): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
l_data: STRING
do
create l_data.make (req.content_length_value.to_integer_32)
req.read_input_data_into (l_data)
Result := order_from_request_input_data (l_data)
end
order_from_request_input_data (a_data: READABLE_STRING_8): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
parser : JSON_PARSER
do
create parser.make_with_string (a_data)
parser.parse_content
if
parser.is_valid and then
attached parser.parsed_json_value as jv
then
Result := order_from_json (jv)
end
end
feature {NONE} -- Conversion
order_to_json (obj: ORDER): JSON_VALUE
do
Result := order_serialization.to_json (obj)
end
order_from_json (jv: JSON_VALUE): detachable ORDER
do
if attached {ORDER} order_serialization.from_json (jv, {ORDER}) as o then
Result := o
end
end
order_serialization: JSON_SERIALIZATION
do
create Result
Result.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,356 @@
note
description: "{ORDER_HANDLER} handle the resources that we want to expose"
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER_HANDLER
inherit
WSF_URI_TEMPLATE_HANDLER
WSF_RESOURCE_HANDLER_HELPER
redefine
do_get,
do_post,
do_put,
do_delete
end
SHARED_RESTBUCKS_API
REFACTORING_HELPER
WSF_SELF_DOCUMENTED_HANDLER
create
make
feature {NONE} -- Initialization
make (a_orderid_path_parameter_name: READABLE_STRING_GENERAL; a_router: WSF_ROUTER)
do
orderid_path_parameter_name := a_orderid_path_parameter_name
end
orderid_path_parameter_name: READABLE_STRING_GENERAL
feature -- Execute
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute request handler
do
execute_methods (req, res)
end
feature -- Request parameters
order_id_from_request (req: WSF_REQUEST): detachable STRING_8
do
if attached {WSF_STRING} req.path_parameter (orderid_path_parameter_name) as p_id then
Result := p_id.url_encoded_value -- the ORDER id has to be valid STRING 8 value.
end
end
feature -- API DOC
api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
feature -- Documentation
mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION
do
create Result.make (m)
if a_request_methods /= Void then
if a_request_methods.has_method_post then
Result.add_description ("URI:/order METHOD: POST")
elseif
a_request_methods.has_method_get
or a_request_methods.has_method_put
or a_request_methods.has_method_delete
then
Result.add_description ("URI:/order/{orderid} METHOD: GET, PUT, DELETE")
end
end
end
feature -- HTTP Methods
do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
-- <Precursor>
do
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_conditional_get (req, l_order) then
handle_resource_not_modified_response ("The resource" + req.percent_encoded_path_info + " does not change", req, res)
else
compute_response_get (req, res, l_order)
end
else
handle_resource_not_found_response ("The following resource" + req.percent_encoded_path_info + " is not found ", req, res)
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 (req: WSF_REQUEST; res: WSF_RESPONSE; l_order: ORDER)
local
l_msg : STRING
do
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
res.header.put_content_length (l_msg.count)
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_string (l_msg)
end
end
do_put (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.
do
if attached order_id_from_request (req) as l_id then
if has_order (l_id) then
if attached order_from_request (req) as l_order then
if l_order.has_id and then not l_id.same_string (l_order.id) then
-- If request input data define an order id different from the one in request path!
handle_resource_conflict_response ("ERROR: conflict between the url defining order id [" + l_id + "] and request data of order with id [" + l_order.id + "]!", req, res)
else
l_order.set_id (l_id)
if is_valid_to_update (l_order) then
if is_conditional_put (req, l_order) then
update_order (l_order)
compute_response_put (req, res, l_order)
else
handle_precondition_fail_response ("ERROR", req, res)
end
else
--| TODO: build message explaining the conflict to the client!
--| FIXME: Here we need to define the Allow methods
handle_resource_conflict_response ("ERROR: order could not be updated!", req, res)
end
end
else
handle_bad_request_response ("ERROR: invalid request input data!", req, res)
end
else
handle_bad_request_response ("ERROR: Order [" + l_id.out + "] was not found!", req, res)
end
else
handle_bad_request_response ("ERROR: Missing ORDER id information!", req, res)
end
end
is_conditional_put (req: WSF_REQUEST; a_order: ORDER): BOOLEAN
-- Check if If-Match is present and then if `a_order` is matching that etag,
-- if etag from `a_order` is unchanged, return True.
do
if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then
if attached order (a_order.id) as l_order then
if if_match.same_string (order_etag (l_order)) then
Result := True
end
else
Result := False
end
else
Result := True
end
end
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
do
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
if attached order_to_json (l_order) as jv then
res.header.put_content_length (jv.representation.count)
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_string (jv.representation)
end
end
do_delete (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
do
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_valid_to_delete (l_order) then
delete_order (l_order)
compute_response_delete (req, res)
else
--| FIXME: Here we need to define the Allow methods
handle_method_not_allowed_response (req.percent_encoded_path_info + "%N Conflict while trying to delete the order! The order could not be deleted in the current state!", req, res)
end
else
handle_resource_not_found_response ("Order not found", req, res)
end
end
compute_response_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
do
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.no_content)
end
do_post (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
do
if attached order_from_request (req) as l_order then
submit_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response ("ERROR: invalid request input data!", req, res)
end
end
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
l_msg : STRING
do
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
res.header.put_content_length (l_msg.count)
res.header.put_location (req.absolute_script_url (req.request_uri + "/" + l_order.id))
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.created)
res.put_string (l_msg)
end
end
feature {NONE} -- URI helper methods
get_order_id_from_path (a_path: READABLE_STRING_32): READABLE_STRING_32
do
Result := a_path.split ('/').at (3)
end
feature {NONE} -- Implementation Repository Layer
order_etag (a_order: ORDER): STRING_8
local
etag_utils: ETAG_UTILS
do
create etag_utils
Result := etag_utils.md5_digest (a_order.hash_code.out + a_order.revision.out)
end
is_valid_to_delete (a_order: ORDER): BOOLEAN
-- Is the order identified by `a_id' in a state whre it can still be deleted?
do
if attached order (a_order.id) as l_order then
if api.is_state_valid_to_update (l_order.status) then
Result := True
end
end
end
is_valid_to_update (a_order: ORDER): BOOLEAN
-- Check if there is a conflict while trying to update the order.
do
if attached order (a_order.id) as l_existing_order then
if
api.is_state_valid_to_update (l_existing_order.status) and then
api.is_valid_status_state (a_order.status) and then
api.is_valid_transition (l_existing_order, a_order.status)
then
Result := True
end
end
end
order_from_request (req: WSF_REQUEST): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
l_data: STRING
do
create l_data.make (req.content_length_value.to_integer_32)
req.read_input_data_into (l_data)
Result := order_from_request_input_data (l_data)
end
order_from_request_input_data (a_data: READABLE_STRING_8): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
parser : JSON_PARSER
do
create parser.make_with_string (a_data)
parser.parse_content
if
parser.is_valid and then
attached parser.parsed_json_value as jv
then
Result := order_from_json (jv)
end
end
feature {NONE} -- Conversion
order_to_json (obj: ORDER): JSON_VALUE
do
Result := order_serialization.to_json (obj)
end
order_from_json (jv: JSON_VALUE): detachable ORDER
do
if attached {ORDER} order_serialization.from_json (jv, {ORDER}) as o then
Result := o
end
end
order_serialization: JSON_SERIALIZATION
do
create Result
Result.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,33 @@
note
description : "REST Buck server"
date : "$Date$"
revision : "$Revision$"
class RESTBUCKS_SERVER
inherit
WSF_DEFAULT_SERVICE [RESTBUCKS_SERVER_EXECUTION]
create
make
feature {NONE} -- Initialization
make
do
set_service_option ("port", 9090)
import_service_options (create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI}.make_from_file ("server.ini"))
make_and_launch
end
note
copyright: "2011-2017, 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

View File

@@ -0,0 +1,90 @@
note
description : "REST Buck server"
date : "$Date$"
revision : "$Revision$"
class RESTBUCKS_SERVER_EXECUTION
inherit
WSF_ROUTED_SKELETON_EXECUTION
undefine
requires_proxy
end
WSF_NO_PROXY_POLICY
SHARED_RESTBUCKS_API
create
make
feature {NONE} -- Initialization
setup_router
local
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
do
setup_order_handler (router)
create doc.make_hidden (router)
router.handle ("/api/doc", doc, router.methods_GET)
-- Those 2 following routes are not for the REST api, but mainly to make simpler to test this example.
router.handle ("/", create {WSF_URI_AGENT_HANDLER}.make (agent handle_home), router.methods_GET)
router.handle ("/new_order", create {WSF_URI_AGENT_HANDLER}.make (agent handle_new_order), router.methods_GET)
end
setup_order_handler (a_router: WSF_ROUTER)
local
order_handler: ORDER_HANDLER
do
create order_handler.make ("orderid", a_router)
router.handle ("/order", order_handler, router.methods_POST)
router.handle ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
end
feature -- Handler
handle_home (req: WSF_REQUEST; res: WSF_RESPONSE)
local
l_page: WSF_HTML_PAGE_RESPONSE
l_html: STRING
do
create l_page.make
create l_html.make_empty
l_html.append ("<h1>RESTbucks example (a Create-Read-Update-Delete REST API)</h1>%N")
l_html.append ("<ul>")
l_html.append ("<li>To test this example: create <a href=%"/new_order%">new ORDER entry</a> , and get its json representation right away.</li>")
l_html.append ("<li>See the auto-generated <a href=%"/api/doc%">documentation</a>.</li>")
l_html.append ("</ul>")
l_page.set_body (l_html)
l_page.set_title ("RESTbucks example")
res.send (l_page)
end
handle_new_order (req: WSF_REQUEST; res: WSF_RESPONSE)
local
o: ORDER
redir: WSF_REDIRECTION_RESPONSE
do
create o.make_empty
o.set_location ("TakeAway")
o.add_item (create {ORDER_ITEM}.make ("late", "large", "whole", 1))
o.add_item (create {ORDER_ITEM}.make ("expresso", "small", "skim", 2))
submit_order (o)
create redir.make (req.absolute_script_url ("/order/" + o.id))
redir.set_content ("Order " + o.id + " created.", Void)
res.send (redir)
end
note
copyright: "2011-2017, 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

View File

@@ -0,0 +1,24 @@
note
description: "Summary description for {ETAG_UTILS}."
date: "$Date$"
revision: "$Revision$"
class
ETAG_UTILS
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
do
create md5.make
md5.update_from_string (a_string)
Result := md5.digest_as_string
end
note
copyright: "2011-2014, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end