@ -0,0 +1 @@ |
|||
A Sample Shopping Website (for training purposes) |
|||
@ -0,0 +1,8 @@ |
|||
# You have to source this file from your shell ! |
|||
# |
|||
# . set-go-path.sh |
|||
# |
|||
|
|||
export GOPATH="$PWD" |
|||
echo "GOPATH=$GOPATH" |
|||
|
|||
@ -0,0 +1 @@ |
|||
code.google.com |
|||
@ -0,0 +1,208 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"code.google.com/p/gorest" |
|||
"net/http" |
|||
"fmt" |
|||
"strings" |
|||
"io/ioutil" |
|||
"encoding/json" |
|||
) |
|||
|
|||
type Category struct { |
|||
Id string |
|||
Name string |
|||
} |
|||
|
|||
var Categories []Category = []Category { |
|||
{ "fringues", "Habillage" }, |
|||
{ "cuisine", "Cuisine" }, |
|||
{ "digital", "Digital" }, |
|||
{ "maison", "Bricolage" } } |
|||
|
|||
type Product struct { |
|||
Id int |
|||
Name string |
|||
Category string |
|||
Image string |
|||
Description string |
|||
Price float32 |
|||
Stock int |
|||
VendorId string |
|||
VendorName string |
|||
VendorProductId string |
|||
IsDigital bool |
|||
} |
|||
|
|||
type BuyResponse struct { |
|||
ResponseCode string |
|||
DownloadUrl string |
|||
} |
|||
|
|||
type CallbackResponse struct { |
|||
ResponseCode string `json:"code"` |
|||
RedirectUrl string `json:"redirect_url"` |
|||
} |
|||
|
|||
type BuyCallback struct { |
|||
VendorId string |
|||
VendorName string |
|||
VendorProductId string |
|||
} |
|||
|
|||
var Products []Product = []Product { |
|||
{ 0, "T-Shirt", "fringues", "brice.jpg", "Le T-Shirt de Brice de Nice.", 99.9, 1, "", "", "", false }, |
|||
{ 1, "Pull col roulé", "fringues", "pull.jpg", "Un pull à col roulé de couleur marron.", 2.00, 10, "", "", "", false }, |
|||
{ 2, "Cocotte minute", "cuisine", "cocotte.jpg", "La cocotte minute 'Presto'.", 45.00, 2, "", "", "", false }, |
|||
{ 3, "Marteau-Piqueur", "maison", "mp.jpg", "Le marteau piqueur 'DESTRUCTOR 2000'.", 600, 5, "", "", "", false } } |
|||
// { 4, "Visseuse Ultrasonique", "maison", "visseuse.jpg", "La visseuse-dévisseuse de chez Méga Store.", 600, 5, "mega-store", "Méga Store", "0001", false },
|
|||
// { 5, "DVD de Harry Poter", "digital", "dvd.jpg", "L'histoire de Ari l'empotteur au pays des merveilles.", 29.9, -1, "zouba-books", "Zouba Books", "12345", true } }
|
|||
|
|||
func main() { |
|||
gorest.RegisterService(new(MyShopService)) // Register our service
|
|||
http.Handle("/api/",gorest.Handle()) |
|||
http.Handle("/", http.FileServer(http.Dir("www-root"))) |
|||
http.ListenAndServe(":8787", nil) |
|||
} |
|||
|
|||
// REST Service Definition
|
|||
type MyShopService struct { |
|||
gorest.RestService `root:"/api/" consumes:"application/json" produces:"application/json"` |
|||
getCategories gorest.EndPoint `method:"GET" path:"/shop/category/" output:"[]Category"` |
|||
getProductsByCategory gorest.EndPoint `method:"GET" path:"/shop/product/?{category:string}" output:"[]Product"` |
|||
searchProducts gorest.EndPoint `method:"GET" path:"/shop/search/{criteria:string}" output:"[]Product"` |
|||
getProduct gorest.EndPoint `method:"GET" path:"/shop/product/{id:int}" output:"Product"` |
|||
addProduct gorest.EndPoint `method:"POST" path:"/market/product/" postdata:"Product"` |
|||
buyProduct gorest.EndPoint `method:"GET" path:"/shop/product/{id:int}/buy" output:"BuyResponse"` |
|||
} |
|||
|
|||
func(serv MyShopService) SearchProducts(criteria string) []Product { |
|||
fmt.Println(">>> SearchProducts: criteria = ", criteria) |
|||
if (criteria == "") { |
|||
return Products |
|||
} |
|||
|
|||
sliceofcriteria := strings.Split(criteria, " ") |
|||
|
|||
FilteredProducts := []Product {} |
|||
for _, p := range Products { |
|||
var selected bool = false |
|||
name := strings.ToLower(p.Name) |
|||
desc := strings.ToLower(p.Description) |
|||
|
|||
for _, c := range sliceofcriteria { |
|||
c = strings.ToLower(c) |
|||
if strings.Contains(name, c) || strings.Contains(desc, c) { |
|||
selected = true |
|||
} |
|||
} |
|||
|
|||
if selected { |
|||
FilteredProducts = append(FilteredProducts, p) |
|||
} |
|||
} |
|||
|
|||
return FilteredProducts |
|||
} |
|||
|
|||
func(serv MyShopService) GetProducts() []Product { |
|||
fmt.Println(">>> GetProducts") |
|||
return Products |
|||
} |
|||
|
|||
func(serv MyShopService) BuyProduct(id int) (resp BuyResponse) { |
|||
fmt.Println(">>> BuyProduct: id = ", id) |
|||
if id > len(Products) - 1 { |
|||
serv.ResponseBuilder().SetResponseCode(404).Overide(true) |
|||
return |
|||
} |
|||
|
|||
if Products[id].Stock == 0 { |
|||
serv.ResponseBuilder().SetResponseCode(409).Overide(true) |
|||
return |
|||
} |
|||
|
|||
if Products[id].Stock >0 { |
|||
Products[id].Stock-- |
|||
} |
|||
|
|||
return_code := "order-accepted" |
|||
redirect_url := "" |
|||
if Products[id].VendorId != "" { |
|||
elements := []string { "http://api.the-vendor.test:8080/api/vendor", Products[id].VendorId, "callback", Products[id].VendorProductId, "1" } |
|||
callback := strings.Join(elements, "/") |
|||
fmt.Println(">>> BuyProduct: firing callback to vendor", Products[id].VendorName, "with URL =", callback) |
|||
|
|||
client := &http.Client{} |
|||
req, err := http.NewRequest("GET", callback, nil) |
|||
resp, err := client.Do(req) |
|||
if err != nil { |
|||
fmt.Println(">>> BuyProduct: ERROR", err) |
|||
return_code = "error" |
|||
serv.ResponseBuilder().SetResponseCode(500).Overide(true) |
|||
} else { |
|||
defer resp.Body.Close() |
|||
fmt.Println(">>> BuyProduct: response Status:", resp.Status) |
|||
if resp.Status != "200 OK" { |
|||
body, _ := ioutil.ReadAll(resp.Body) |
|||
fmt.Println(">>> BuyProduct: response Body:", string(body)) |
|||
return_code = "error" |
|||
serv.ResponseBuilder().SetResponseCode(500).Overide(true) |
|||
} else { |
|||
if Products[id].IsDigital { |
|||
fmt.Println(">>> BuyProduct: Decoding JSON response") |
|||
json_resp := new(CallbackResponse) |
|||
json.NewDecoder(resp.Body).Decode(json_resp) |
|||
redirect_url = json_resp.RedirectUrl; |
|||
} else { |
|||
fmt.Println(">>> BuyProduct: Ignoring JSON response") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fmt.Println(">>> BuyProduct: return_code =", return_code, "redirect_url =", redirect_url) |
|||
|
|||
resp = BuyResponse { return_code, redirect_url } |
|||
return |
|||
} |
|||
|
|||
|
|||
func(serv MyShopService) GetProduct(id int) (p Product) { |
|||
fmt.Println(">>> GetProduct: id =", id) |
|||
if id > len(Products) - 1 { |
|||
serv.ResponseBuilder().SetResponseCode(404).Overide(true) |
|||
return |
|||
} |
|||
p = Products[id] |
|||
return |
|||
} |
|||
|
|||
func(serv MyShopService) AddProduct(posted Product) { |
|||
fmt.Println(">>> AddProduct: posted =", posted) |
|||
id := len(Products) |
|||
posted.Id = id |
|||
Products = append(Products, posted); |
|||
serv.ResponseBuilder().Created("/api/shop/product/"+string(id)) |
|||
} |
|||
|
|||
func(serv MyShopService) GetProductsByCategory(category string) []Product { |
|||
fmt.Println(">>> GetProductsByCategory: category =", category) |
|||
if (category == "") { |
|||
return Products |
|||
} |
|||
|
|||
FilteredProducts := []Product {} |
|||
for _, p := range Products { |
|||
if p.Category == category { |
|||
FilteredProducts = append(FilteredProducts, p) |
|||
} |
|||
} |
|||
|
|||
return FilteredProducts |
|||
} |
|||
|
|||
func(serv MyShopService) GetCategories() []Category { |
|||
fmt.Println(">>> GetCategories") |
|||
return Categories |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
curl -H "Content-Type: application/json" \ |
|||
-d '{ "Name": "A New Product", "Category": "maison", "Image": "logo.png", "Description": "My brand new product", "Price": 1, "Stock": 1 }' \ |
|||
-X POST \ |
|||
-D - \ |
|||
http://localhost:8787/api/shop/product/ |
|||
|
|||
@ -0,0 +1 @@ |
|||
.DS_Store |
|||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,31 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>My Shop</title> |
|||
<script type="text/javascript" src="js/dojo/dojo.js"></script> |
|||
<script type="text/javascript" src="js/shop.js"></script> |
|||
<link rel="stylesheet" type="text/css" href="style.css"> |
|||
<link rel="stylesheet" href="js/dijit/themes/claro/claro.css" /> |
|||
</head> |
|||
<body class="claro"> |
|||
<div id="main_pane"> |
|||
<div id="site_header"> |
|||
<div id="site_logo"><img src="/img/logo.png" height="75px"/></div> |
|||
</div> |
|||
<div id="pages_container"> |
|||
<div style="position: relative;"> |
|||
<div id="page_title"> |
|||
<h2><span id="title">Boutique</span></h2> |
|||
<h5><span id="subtitle">Subtitle</span></h5> |
|||
</div> |
|||
|
|||
<div id="content_pane"> |
|||
<div id="search_bar">Recherche : <input type="text" id="search_textbox" /><div id="search_results"></div></div> |
|||
<div id="category_bar"><div id="category_list_placeholder"></div></div> |
|||
<div id="products_pane"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,3 @@ |
|||
dojo |
|||
dojox |
|||
dijit |
|||
@ -0,0 +1,217 @@ |
|||
var apibase = "/api/shop"; |
|||
var dialog; |
|||
|
|||
require([ "dojo/ready", "dojo/request/xhr", "dojo/dom-construct", "dojo/dom", "dojo/on", "dojo/dom-style", "dojo/dom-attr", "dijit/Dialog" ], |
|||
function(ready, xhr, domConstruct, dom, on, domStyle, domAttr, Dialog) { |
|||
|
|||
function setOnCategoryClickHandler(node, catid, catname) { |
|||
on(node, "click", function() { |
|||
loadProducts(catid); |
|||
setSubTitle(catname); |
|||
}); |
|||
} |
|||
|
|||
function setOnProductClickHandler(node, id) { |
|||
on(node, "click", function() { |
|||
displayProduct(id); |
|||
}); |
|||
} |
|||
|
|||
function setSearchHandler() { |
|||
on(dom.byId("search_textbox"), "keyup", doSearch); |
|||
on(dom.byId("search_textbox"), "blur", function () { |
|||
console.log("SEARCH TEXT BOX >> Blur"); |
|||
window.setInterval(function () { console.log("SEARCH TEXT BOX >> Je la cache"); domStyle.set("search_results", "visibility", "hidden"); }, 100); |
|||
}); |
|||
on(dom.byId("search_textbox"), "focus", function () { |
|||
console.log("SEARCH TEXT BOX >> Focus"); |
|||
var searchCriteria = domAttr.get("search_textbox", "value"); |
|||
if (searchCriteria != "") { |
|||
domStyle.set("search_results", "visibility", "visible"); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function setBuyHandler(node, id) { |
|||
on(node, "click", function() { |
|||
buyProduct(id); |
|||
}); |
|||
} |
|||
|
|||
function buyProduct(id) { |
|||
xhr(apibase + "/product/" + encodeURI(id) + "/buy", |
|||
{ handleAs: "json" } |
|||
).then(function (data) { |
|||
dialog.set("title", "Commande acceptée"); |
|||
dialog.set("content", "La commande est partie. Vous allez très prochainement recevoir le produit."); |
|||
console.log("test..."); |
|||
if (data.DownloadUrl != null && data.DownloadUrl != "") { |
|||
console.log("popup !"); |
|||
window.location.href = data.DownloadUrl; |
|||
} |
|||
dialog.show(); |
|||
displayProduct(id); // Refresh UI
|
|||
}, function (err) { |
|||
if (err.response != null && err.response.status == 409) { |
|||
dialog.set("title", "Erreur"); |
|||
dialog.set("content", "Le produit n'est plus en stock. Désolé."); |
|||
dialog.show(); |
|||
} else { |
|||
dialog.set("title", "OOPS"); |
|||
dialog.set("content", "Erreur interne. Désolé."); |
|||
dialog.show(); |
|||
} |
|||
console.log(err); |
|||
displayProduct(id); // Refresh UI
|
|||
}, function (evt) { |
|||
|
|||
}); |
|||
} |
|||
|
|||
function doSearch(evt) { |
|||
var searchCriteria = domAttr.get("search_textbox", "value"); |
|||
if (searchCriteria == "") { |
|||
domStyle.set("search_results", "visibility", "hidden"); |
|||
return; |
|||
} else { |
|||
domStyle.set("search_results", "visibility", "visible"); |
|||
} |
|||
|
|||
xhr(apibase + "/search/" + encodeURI(searchCriteria), |
|||
{ handleAs: "json" } |
|||
).then(function (data) { |
|||
domConstruct.empty("search_results"); |
|||
var placeholder = dom.byId("search_results"); |
|||
for (var i = 0; i < data.length; i++) { |
|||
var div = domConstruct.create("div", {}, placeholder); |
|||
domConstruct.create("img", { width: "32px", src: "/img/" + data[i].Image }, div); |
|||
domConstruct.create("span", { textContent: data[i].Name }, div); |
|||
setOnProductClickHandler(div, data[i].Id); |
|||
} |
|||
if (data.length == 0) { |
|||
domConstruct.create("span", { textContent: "Aucun résultat", 'class': "no_result" }, placeholder); |
|||
} |
|||
}, function (err) { |
|||
console.log(err); |
|||
}, function (evt) { |
|||
|
|||
}); |
|||
} |
|||
|
|||
function displayProduct(id) { |
|||
xhr(apibase + "/product/" + encodeURI(id), |
|||
{ handleAs: "json" } |
|||
).then(function (data) { |
|||
domConstruct.empty("products_pane"); |
|||
var placeholder = dom.byId("products_pane"); |
|||
var div = dojo.create("div", { 'class': 'product_detail' }, placeholder); |
|||
dojo.create("h1", { 'class': "product_name", textContent: data.Name }, div); |
|||
var div2 = dojo.create("div", {}, div) |
|||
dojo.create("img", { src: "/img/" + data.Image, height: "200px" }, div2); |
|||
dojo.create("span", { 'class': "product_price", textContent: data.Price + " €" }, div2); |
|||
if (data.Stock != "-1") { |
|||
dojo.create("span", { 'class': "product_stock", textContent: "En Stock: " + data.Stock }, div2); |
|||
} |
|||
if (data.VendorName != "") { |
|||
dojo.create("span", { 'class': "sold_by", textContent: "Vendu par: " + data.VendorName }, div2); |
|||
} |
|||
|
|||
var buy_button = dojo.create("div", { 'class': "buy_button" }, div2); |
|||
dojo.create("div", { textContent: "Acheter !" }, buy_button); |
|||
setBuyHandler(buy_button, data.Id); |
|||
|
|||
dojo.create("div", { 'class': "product_description", textContent: data.Description }, div); |
|||
}, function (err) { |
|||
if (err.response != null && err.response.status == 404) { |
|||
dialog.set("title", "Erreur"); |
|||
dialog.set("content", "Le produit a été retiré de la vente. Désolé."); |
|||
dialog.show(); |
|||
} else { |
|||
dialog.set("title", "OOPS"); |
|||
dialog.set("content", "Erreur interne. Désolé."); |
|||
dialog.show(); |
|||
} |
|||
console.log(err); |
|||
}, function (evt) { |
|||
|
|||
}); |
|||
} |
|||
|
|||
function setSubTitle(name) { |
|||
domConstruct.empty("subtitle"); |
|||
dom.byId("subtitle").textContent = name; |
|||
} |
|||
|
|||
function loadProducts(catid) { |
|||
var queryString = ""; |
|||
if (catid != null) { |
|||
queryString = "?category=" + encodeURI(catid); |
|||
} |
|||
xhr(apibase + "/product/" + queryString, |
|||
{ handleAs: "json" } |
|||
).then(function (data) { |
|||
domConstruct.empty("products_pane"); |
|||
var placeholder = dom.byId("products_pane"); |
|||
var table = domConstruct.create("table", { 'class': "product_table" }, placeholder); |
|||
var current_tr = null; |
|||
var i = 0; |
|||
for (; i < data.length; i++) { |
|||
if (i % 3 == 0) { |
|||
current_tr = domConstruct.create("tr", {}, table); |
|||
} |
|||
var td = domConstruct.create("td", {}, current_tr); |
|||
domConstruct.create("img", { width: "100px", src: "/img/" + data[i].Image }, td); |
|||
domConstruct.create("span", { textContent: data[i].Name }, td); |
|||
setOnProductClickHandler(td, data[i].Id); |
|||
} |
|||
// Fill remaining columns if less than 3 products
|
|||
for (; i < 3; i++) { |
|||
domConstruct.create("td", {}, current_tr); |
|||
} |
|||
}, function (err) { |
|||
dialog.set("title", "OOPS"); |
|||
dialog.set("content", "Erreur interne. Désolé."); |
|||
dialog.show(); |
|||
console.log(err); |
|||
}, function (evt) { |
|||
|
|||
}); |
|||
} |
|||
|
|||
function loadCategories() { |
|||
xhr(apibase + "/category/", |
|||
{ handleAs: "json" } |
|||
).then(function (data) { |
|||
var placeholder = dom.byId("category_list_placeholder"); |
|||
domConstruct.empty("category_list_placeholder"); |
|||
var all_products_node = domConstruct.create("span", { textContent: "Tous les produits", 'class': "category_item" }, placeholder); |
|||
setOnCategoryClickHandler(all_products_node, null, "Tous les produits"); |
|||
for (var i = 0; i < data.length; i++) { |
|||
var catid = data[i].Id; |
|||
var catname = data[i].Name; |
|||
var node = domConstruct.create("span", { textContent: data[i].Name, 'class': "category_item" }, placeholder); |
|||
setOnCategoryClickHandler(node, catid, catname); |
|||
} |
|||
}, function (err) { |
|||
dialog.set("title", "OOPS"); |
|||
dialog.set("content", "Erreur interne. Désolé."); |
|||
dialog.show(); |
|||
console.log(err); |
|||
}, function (evt) { |
|||
|
|||
}); |
|||
} |
|||
|
|||
ready(function() { |
|||
loadCategories(); |
|||
loadProducts(null); |
|||
setSubTitle("Tous les produits"); |
|||
setSearchHandler(); |
|||
dialog = new Dialog({ |
|||
id: "global_dialog", |
|||
title: "...", |
|||
content: "...", |
|||
style: "width: 500px; display: none;" |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,218 @@ |
|||
#search_bar { |
|||
position: absolute; |
|||
top: 0px; |
|||
right: 0px; |
|||
width: 450px; |
|||
height: 50px; |
|||
} |
|||
|
|||
#search_textbox { |
|||
width: 350px; |
|||
height: 20px; |
|||
position: absolute; |
|||
top: 0px; |
|||
right: 0px; |
|||
background-color: gray; |
|||
} |
|||
|
|||
#search_results { |
|||
width: 350px; |
|||
position: absolute; |
|||
top: 30px; |
|||
right: 0px; |
|||
background-color: white; |
|||
border: 1px solid lightgrey; |
|||
z-index: 255; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
#search_results span, #search_results img { |
|||
display:inline-block; |
|||
vertical-align:middle; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
#search_results div { |
|||
cursor: pointer; cursor: hand; |
|||
border: 1px solid white; |
|||
} |
|||
|
|||
#search_results div:hover { |
|||
border: 1px solid gray; |
|||
} |
|||
|
|||
.no_result { |
|||
color: lightgrey; |
|||
font-style: italic; |
|||
} |
|||
|
|||
#category_bar { |
|||
position: absolute; |
|||
top: 100px; |
|||
left: 0px; |
|||
width: 200px; |
|||
} |
|||
|
|||
#products_pane { |
|||
position: absolute; |
|||
top: 100px; |
|||
left: 250px; |
|||
right: 0px; |
|||
min-height: 300px; |
|||
} |
|||
|
|||
#content_pane { |
|||
position: absolute; |
|||
top: 0px; |
|||
left: 30px; |
|||
right: 30px; |
|||
} |
|||
|
|||
body { |
|||
background-color: #AAAAAA; |
|||
font-family: Sans-Serif; |
|||
} |
|||
|
|||
#main_pane { |
|||
position: relative; |
|||
width: 80%; |
|||
margin: auto; |
|||
background-color: #EEEEEE; |
|||
min-height: 800px; |
|||
} |
|||
|
|||
#site_header { |
|||
position: absolute; |
|||
top: 0px; |
|||
height: 116px; |
|||
left: 30px; |
|||
right: 30px; |
|||
} |
|||
|
|||
#site_logo { |
|||
position: absolute; |
|||
top: 40px; |
|||
width: 345px; |
|||
left: 40px; |
|||
} |
|||
|
|||
|
|||
#pages_container { |
|||
position: absolute; |
|||
top: 140px; |
|||
bottom: 30px; |
|||
right: 30px; |
|||
left: 60px; |
|||
} |
|||
|
|||
#page_title { |
|||
position: absolute; |
|||
top: 10px; |
|||
right: 30px; |
|||
left: 30px; |
|||
font: normal normal normal 25px/1.2em sans-serif; |
|||
} |
|||
|
|||
#page_title > h2 { |
|||
line-height: 1.1em; |
|||
font: normal normal normal 22px/1.1em sans-serif; |
|||
color: #00CCFF; |
|||
margin : 0; |
|||
padding : 0; |
|||
border : 0; |
|||
outline : 0; |
|||
} |
|||
|
|||
#page_title > h5 { |
|||
color: #7F7F7F; |
|||
line-height: 1.2em; |
|||
letter-spacing: normal; |
|||
margin : 0; |
|||
padding : 0; |
|||
border : 0; |
|||
outline : 0; |
|||
} |
|||
|
|||
.product_table { |
|||
width: 100%; |
|||
border-spacing: 10px; |
|||
border-collapse: separate; |
|||
} |
|||
|
|||
.product_table span, .product_table img { |
|||
display: block; |
|||
margin: auto; |
|||
} |
|||
|
|||
.product_table img { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.product_table td { |
|||
padding-top: 10px; |
|||
padding-bottom: 10px; |
|||
text-align: center; |
|||
background-color: white; |
|||
width: 33%; |
|||
color: darkgrey; |
|||
cursor: pointer; cursor: hand; |
|||
} |
|||
|
|||
.category_item { |
|||
display: block; |
|||
margin-bottom: 10px; |
|||
text-decoration: none; |
|||
cursor: pointer; cursor: hand; |
|||
} |
|||
|
|||
.category_item:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.product_price { |
|||
position: absolute; |
|||
top: 20px; |
|||
left: 300px; |
|||
color: red; |
|||
font-size: x-large; |
|||
} |
|||
|
|||
.product_detail > div { |
|||
position: relative; |
|||
} |
|||
|
|||
.product_stock { |
|||
position: absolute; |
|||
top: 60px; |
|||
left: 300px; |
|||
color: darkgreen; |
|||
} |
|||
|
|||
.sold_by { |
|||
position: absolute; |
|||
top: 80px; |
|||
left: 300px; |
|||
color: darkgreen; |
|||
} |
|||
|
|||
.product_description { |
|||
margin-top: 20px; |
|||
color: darkgrey; |
|||
} |
|||
|
|||
.buy_button { |
|||
position: absolute; |
|||
top: 150px; |
|||
left: 300px; |
|||
background-color: red; |
|||
color: white; |
|||
width: 100px; |
|||
height: 30px; |
|||
cursor: pointer; cursor: hand; |
|||
} |
|||
|
|||
.buy_button > div { |
|||
text-align: center; |
|||
line-height: 30px; |
|||
vertical-align: middle; |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
#!/bin/sh |
|||
|
|||
(cd src && javac fr/itix/soapbackend/*.java) |
|||
|
|||
BASE=tomcat/webapps/axis2/WEB-INF/services/ |
|||
NAME=VendorBackend |
|||
mkdir -p build/$NAME/META-INF build/$NAME/fr/itix/soapbackend/ |
|||
cp src/fr/itix/soapbackend/*.class build/$NAME/fr/itix/soapbackend/ |
|||
cp src/services.xml build/$NAME/META-INF/ |
|||
|
|||
cp -rv build/$NAME $BASE/ |
|||
|
|||
|
|||
@ -0,0 +1 @@ |
|||
*.class |
|||
@ -0,0 +1,14 @@ |
|||
package fr.itix.soapbackend; |
|||
|
|||
public class VendorBackend { |
|||
public String NotifySale(String productId, int number, String callerID) { |
|||
if ("12345".equals(productId) && "zouba-books".equals(callerID)) { |
|||
return "OK;http://online-shop.zouba-books.test:8787/book-1234.pdf"; |
|||
} |
|||
|
|||
if ("0001".equals(productId) && "mega-store".equals(callerID)) { |
|||
return "OK;"; |
|||
} |
|||
throw new RuntimeException("Unknown caller id or wrong product id"); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<service name="VendorBackend" scope="application"> |
|||
<description> |
|||
Vendor Backend |
|||
</description> |
|||
<messageReceivers> |
|||
<messageReceiver |
|||
mep="http://www.w3.org/2004/08/wsdl/in-only" |
|||
class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver"/> |
|||
<messageReceiver |
|||
mep="http://www.w3.org/2004/08/wsdl/in-out" |
|||
class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/> |
|||
</messageReceivers> |
|||
<parameter name="ServiceClass"> |
|||
fr.itix.soapbackend.VendorBackend |
|||
</parameter> |
|||
</service> |
|||
@ -0,0 +1,22 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2015 Nicolas MASSE |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
|
|||
@ -0,0 +1,12 @@ |
|||
## WebAPI-Samples |
|||
Sample Code for various Web API Protocols (SOAP, REST, etc.) |
|||
|
|||
### SOAP samples |
|||
- [MTOM Attachments in Java](./SOAP/MTOM/) |
|||
|
|||
### REST samples |
|||
- TODO |
|||
|
|||
### Utils |
|||
- [A Reverse Proxy written in GO](./Utils/ReverseProxy/) |
|||
|
|||
@ -0,0 +1 @@ |
|||
tomcat |
|||
@ -0,0 +1,29 @@ |
|||
# MTOM Sample Server Code |
|||
|
|||
## Introduction to MTOM |
|||
|
|||
MTOM is a standard to attach a file to a SOAP message (others way to do so are: MIME Attachments or in-line base64). |
|||
|
|||
For a good introduction to MTOM read : |
|||
- http://www.mkyong.com/webservices/jax-ws/jax-ws-attachment-with-mtom/ |
|||
- https://axis.apache.org/axis2/java/core/docs/mtom-guide.html |
|||
- http://stackoverflow.com/questions/215741/how-does-mtom-work |
|||
|
|||
### How to know when a SOAP Message use MTOM ? |
|||
|
|||
The easy way : |
|||
- The HTTP request is a mime multipart |
|||
``` |
|||
POST /path/to/ws HTTP/1.1 |
|||
Content-type: multipart/related; start="bla"; type="application/xop+xml"; boundary="uuid:bla..."; |
|||
``` |
|||
- The SOAP Payload contains `Include` elements in the namespace `http://www.w3.org/2004/08/xop/include` |
|||
``` |
|||
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" |
|||
href="bla bla bla"> |
|||
``` |
|||
|
|||
## MTOM Server Code Setup |
|||
|
|||
See [the documentation](./doc/README.md). |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
#!/bin/bash |
|||
|
|||
NAME=SoapBackend |
|||
|
|||
function die() { |
|||
echo "ERROR: $@" |
|||
exit 1 |
|||
} |
|||
|
|||
cd src || die "No source code" |
|||
javac $(find . -iname *.java) || die "Compilation error" |
|||
jar cvf ../build/$NAME.aar $(find . -iname *.class -or -iname *.xml) || die "Cannot build jar" |
|||
|
|||
|
|||
@ -0,0 +1 @@ |
|||
*.aar |
|||
@ -0,0 +1,104 @@ |
|||
## MTOM Server Code Setup |
|||
|
|||
### Pre-requisites |
|||
|
|||
To make this sample code work, you need: |
|||
- Tomcat (tested against version 7) |
|||
- Axis2 (tested against version 1.6) |
|||
|
|||
Get tomcat7 and install it in a `tomcat` folder. |
|||
``` |
|||
$ wget http://www.eu.apache.org/dist/tomcat/tomcat-7/v7.0.62/bin/apache-tomcat-7.0.62.tar.gz && tar zxvf apache-tomcat-*.tar.gz && mv apache-tomcat-*/ tomcat |
|||
``` |
|||
|
|||
Get axis2 and install the axis2.jar in `tomcat/webapps/axis2` |
|||
|
|||
``` |
|||
$ cd tomcat/webapps |
|||
$ wget http://www.eu.apache.org/dist//axis/axis2/java/core/1.6.2/axis2-1.6.2-war.zip && unzip axis2-1.6.2-war.zip axis2.war |
|||
$ cd .. |
|||
$ ./bin/startup.sh |
|||
``` |
|||
|
|||
### Build |
|||
|
|||
Build the .aar archive and deploy it. |
|||
|
|||
 |
|||
|
|||
### Change the Axis Configuration to enable MTOM |
|||
|
|||
Edit the Axis2 configuration file: `tomcat/webapps/axis2/WEB-INF/conf/axis2.xml` and set the `enableMTOM` parameter to `true`. |
|||
|
|||
 |
|||
|
|||
**DO NOT FORGET TO RESTART TOMCAT !!!** |
|||
|
|||
## Test the MTOM attachment |
|||
|
|||
### Retrieve the WSDL |
|||
|
|||
Make sure Tomcat is started and the .aar archive is deployed. Then go to http://localhost:8080/axis2/services/listServices. |
|||
|
|||
Click on the `MTOMService` to get the WSDL. |
|||
|
|||
*Note:* make sure you save the file in its original format (File > Save As...). **Do not use copy paste that may break the XML format.** |
|||
|
|||
Just in case, you can find a copy of the WSDL [here](../wsdl/MtomService.wsdl). |
|||
|
|||
|
|||
### Test in SOAP UI |
|||
|
|||
1. Get SOAP UI (tested with version 5) |
|||
2. Import the WSDL |
|||
3. Open the auto-generated request for the `MtomServiceSoap12Binding` binding |
|||
 |
|||
4. Make sure `Enable MTOM` and `Force MTOM` in the bottom left pane are set to `true`. |
|||
 |
|||
5. Import your attachment in the bottom pane. |
|||
 |
|||
6. Make sure to reference the attachment in your SOAP message using `cid:attachment-name`. |
|||
 |
|||
7. Make sure the attachement part in the bottom pane is set and the attachment type is set to `XOP` |
|||
 |
|||
8. Fire the request and observe the result ! |
|||
 |
|||
|
|||
Note: by clicking on the raw button in the request pane, you can have a look at the complete HTTP request. |
|||
|
|||
``` |
|||
POST http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap12Endpoint/ HTTP/1.1 |
|||
Accept-Encoding: gzip,deflate |
|||
Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart@soapui.org>"; start-info="application/soap+xml"; action="urn:countBytes"; boundary="----=_Part_1_223849750.1433937520698" |
|||
MIME-Version: 1.0 |
|||
Content-Length: 912 |
|||
Host: localhost:8080 |
|||
Connection: Keep-Alive |
|||
User-Agent: Apache-HttpClient/4.1.1 (java 1.5) |
|||
|
|||
|
|||
------=_Part_1_223849750.1433937520698 |
|||
Content-Type: application/xop+xml; charset=UTF-8; type="application/soap+xml"; action="countBytes" |
|||
Content-Transfer-Encoding: 8bit |
|||
Content-ID: <rootpart@soapui.org> |
|||
|
|||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:mtom="http://itix.fr/soap/mtom"> |
|||
<soap:Header/> |
|||
<soap:Body> |
|||
<mtom:countBytes> |
|||
<!--Optional:--> |
|||
<mtom:args0><inc:Include href="cid:my-attachment.txt" xmlns:inc="http://www.w3.org/2004/08/xop/include"/></mtom:args0> |
|||
</mtom:countBytes> |
|||
</soap:Body> |
|||
</soap:Envelope> |
|||
------=_Part_1_223849750.1433937520698 |
|||
Content-Type: text/plain; charset=us-ascii; name=my-attachment.txt |
|||
Content-Transfer-Encoding: 7bit |
|||
Content-ID: <my-attachment.txt> |
|||
Content-Disposition: attachment; name="my-attachment.txt"; filename="my-attachment.txt" |
|||
|
|||
Hello World ! |
|||
|
|||
------=_Part_1_223849750.1433937520698-- |
|||
``` |
|||
|
|||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 52 KiB |
@ -0,0 +1,16 @@ |
|||
<service name="VendorBackend" scope="application"> |
|||
<description> |
|||
SOAP Backend that exposes an MTOM enabled service |
|||
</description> |
|||
<messageReceivers> |
|||
<messageReceiver |
|||
mep="http://www.w3.org/2004/08/wsdl/in-only" |
|||
class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver"/> |
|||
<messageReceiver |
|||
mep="http://www.w3.org/2004/08/wsdl/in-out" |
|||
class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/> |
|||
</messageReceivers> |
|||
<parameter name="ServiceClass"> |
|||
fr.itix.soapbackend.MTOMService |
|||
</parameter> |
|||
</service> |
|||
@ -0,0 +1 @@ |
|||
*.class |
|||
@ -0,0 +1,16 @@ |
|||
package fr.itix.soapbackend; |
|||
|
|||
import javax.jws.WebMethod; |
|||
import javax.jws.WebService; |
|||
import javax.xml.ws.soap.MTOM; |
|||
|
|||
@MTOM |
|||
@WebService(name="MtomPortType", |
|||
serviceName="MtomService", |
|||
targetNamespace="http://itix.fr/soap/mtom") |
|||
public class MTOMService { |
|||
@WebMethod |
|||
public int countBytes(byte[] bytes) { |
|||
return bytes.length; |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://itix.fr/soap/mtom" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:ns1="http://org.apache.axis2/xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://itix.fr/soap/mtom"> |
|||
<wsdl:documentation>VendorBackend</wsdl:documentation> |
|||
<wsdl:types> |
|||
<xs:schema attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://itix.fr/soap/mtom"> |
|||
<xs:element name="countBytes"> |
|||
<xs:complexType> |
|||
<xs:sequence> |
|||
<xs:element minOccurs="0" name="args0" nillable="true" type="xs:base64Binary"/> |
|||
</xs:sequence> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
<xs:element name="countBytesResponse"> |
|||
<xs:complexType> |
|||
<xs:sequence> |
|||
<xs:element minOccurs="0" name="return" type="xs:int"/> |
|||
</xs:sequence> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
</wsdl:types> |
|||
<wsdl:message name="countBytesRequest"> |
|||
<wsdl:part name="parameters" element="ns:countBytes"/> |
|||
</wsdl:message> |
|||
<wsdl:message name="countBytesResponse"> |
|||
<wsdl:part name="parameters" element="ns:countBytesResponse"/> |
|||
</wsdl:message> |
|||
<wsdl:portType name="MtomServicePortType"> |
|||
<wsdl:operation name="countBytes"> |
|||
<wsdl:input message="ns:countBytesRequest" wsaw:Action="urn:countBytes"/> |
|||
<wsdl:output message="ns:countBytesResponse" wsaw:Action="urn:countBytesResponse"/> |
|||
</wsdl:operation> |
|||
</wsdl:portType> |
|||
<wsdl:binding name="MtomServiceSoap11Binding" type="ns:MtomServicePortType"> |
|||
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> |
|||
<wsdl:operation name="countBytes"> |
|||
<soap:operation soapAction="urn:countBytes" style="document"/> |
|||
<wsdl:input> |
|||
<soap:body use="literal"/> |
|||
</wsdl:input> |
|||
<wsdl:output> |
|||
<soap:body use="literal"/> |
|||
</wsdl:output> |
|||
</wsdl:operation> |
|||
</wsdl:binding> |
|||
<wsdl:binding name="MtomServiceSoap12Binding" type="ns:MtomServicePortType"> |
|||
<soap12:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> |
|||
<wsdl:operation name="countBytes"> |
|||
<soap12:operation soapAction="urn:countBytes" style="document"/> |
|||
<wsdl:input> |
|||
<soap12:body use="literal"/> |
|||
</wsdl:input> |
|||
<wsdl:output> |
|||
<soap12:body use="literal"/> |
|||
</wsdl:output> |
|||
</wsdl:operation> |
|||
</wsdl:binding> |
|||
<wsdl:binding name="MtomServiceHttpBinding" type="ns:MtomServicePortType"> |
|||
<http:binding verb="POST"/> |
|||
<wsdl:operation name="countBytes"> |
|||
<http:operation location="countBytes"/> |
|||
<wsdl:input> |
|||
<mime:content type="application/xml" part="parameters"/> |
|||
</wsdl:input> |
|||
<wsdl:output> |
|||
<mime:content type="application/xml" part="parameters"/> |
|||
</wsdl:output> |
|||
</wsdl:operation> |
|||
</wsdl:binding> |
|||
<wsdl:service name="MtomService"> |
|||
<wsdl:port name="MtomServiceHttpSoap11Endpoint" binding="ns:MtomServiceSoap11Binding"> |
|||
<soap:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap11Endpoint/"/> |
|||
</wsdl:port> |
|||
<wsdl:port name="MtomServiceHttpSoap12Endpoint" binding="ns:MtomServiceSoap12Binding"> |
|||
<soap12:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap12Endpoint/"/> |
|||
</wsdl:port> |
|||
<wsdl:port name="MtomServiceHttpEndpoint" binding="ns:MtomServiceHttpBinding"> |
|||
<http:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpEndpoint/"/> |
|||
</wsdl:port> |
|||
</wsdl:service> |
|||
</wsdl:definitions> |
|||
@ -0,0 +1 @@ |
|||
This a sample WSDL, generated by Axis |
|||
@ -0,0 +1,27 @@ |
|||
## Reverse Proxy written in GO |
|||
This reverse proxy listens on a local port and forward all requests to a named host. It honors the proxy environment variables. |
|||
|
|||
### Initial Need |
|||
|
|||
Once upon a time, I had to circumvent a bug in a product that could not handle correctly an HTTPS connection to a proxy. |
|||
|
|||
Since it was an HTTPS connection, I could not setup a transparent proxy. |
|||
|
|||
### What it does |
|||
|
|||
It opens a local port and listen to HTTP requests, forwards the requests to a named host and send back the response. |
|||
|
|||
### How to use it |
|||
|
|||
```bash |
|||
go run src/itix.fr/forward/main.go -local-port 8080 -target https://www.opentrust.com |
|||
curl -D - http://localhost:8080/robots.txt |
|||
``` |
|||
|
|||
If you want to go through a proxy, do not forget to set the ```http_proxy``` and ```https_proxy``` variables ! |
|||
|
|||
```bash |
|||
export http_proxy=http://my.proxy:8888/ |
|||
export https_proxy=http://my.proxy:8888/ |
|||
``` |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
export GOPATH="$PWD" |
|||
echo "GOPATH=$GOPATH" |
|||
|
|||
@ -0,0 +1,96 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"os" |
|||
"fmt" |
|||
"flag" |
|||
"net/http" |
|||
"net/http/httputil" |
|||
"net/url" |
|||
) |
|||
|
|||
// The local port on which we should listen to
|
|||
var local_port int |
|||
|
|||
// The target URL on which we should redirect requests
|
|||
var target string |
|||
|
|||
func init() { |
|||
// --local-port=9009
|
|||
flag.IntVar(&local_port, "local-port", 9090, "the TCP port to listen to") |
|||
|
|||
// --target=http://www.perdu.com
|
|||
flag.StringVar(&target, "target", "http://www.perdu.com", "the target URL to redirect the request to") |
|||
} |
|||
|
|||
// The MyResponseWriter is a wrapper around the standard http.ResponseWriter
|
|||
// We need it to retrieve the http status code of an http response
|
|||
type MyResponseWriter struct { |
|||
Underlying http.ResponseWriter |
|||
Status int |
|||
} |
|||
|
|||
func (mrw *MyResponseWriter) Header() http.Header { |
|||
return mrw.Underlying.Header() |
|||
} |
|||
|
|||
func (mrw *MyResponseWriter) Write(b []byte) (int, error) { |
|||
return mrw.Underlying.Write(b) |
|||
} |
|||
|
|||
func (mrw *MyResponseWriter) WriteHeader(s int) { |
|||
mrw.Status = s |
|||
mrw.Underlying.WriteHeader(s) |
|||
} |
|||
|
|||
func main() { |
|||
// Parse the command line arguments
|
|||
flag.Parse() |
|||
|
|||
// Parse the target URL and perform some sanity checks
|
|||
url, err := url.Parse(target) |
|||
if err != nil { |
|||
panic(err) |
|||
} |
|||
|
|||
// Initialize a Reverse Proxy object with a custom director
|
|||
proxy := httputil.NewSingleHostReverseProxy(url) |
|||
underlying_director := proxy.Director |
|||
proxy.Director = func(req *http.Request) { |
|||
// Let the underlying director do the mandatory job
|
|||
underlying_director(req) |
|||
|
|||
// Custom Handling
|
|||
// ---------------
|
|||
//
|
|||
// Filter out the "Host" header sent by the client
|
|||
// otherwise the target server won't be able to find the
|
|||
// matching virtual host. The correct host header will be
|
|||
// added automatically by the net/http package.
|
|||
req.Host = "" |
|||
} |
|||
|
|||
http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { |
|||
// Log the incoming request (including headers)
|
|||
fmt.Printf("%v %v HTTP/1.1\n", req.Method, req.URL) |
|||
req.Header.Write(os.Stdout) |
|||
fmt.Println() |
|||
|
|||
// Wrap the standard response writer with our own
|
|||
// implementation because we need the status code of the
|
|||
// response and that field is not exported by default
|
|||
mrw := &MyResponseWriter{ Underlying: rw } |
|||
|
|||
// Let the reverse proxy handle the request
|
|||
proxy.ServeHTTP(mrw, req) |
|||
|
|||
// Log the response
|
|||
fmt.Printf("%v %v\n", mrw.Status, http.StatusText(mrw.Status)) |
|||
mrw.Header().Write(os.Stdout) |
|||
fmt.Println() |
|||
}) |
|||
|
|||
fmt.Printf("Listening on port %v for incoming requests...\n", local_port) |
|||
http.ListenAndServe(fmt.Sprintf(":%v", local_port), nil) |
|||
} |
|||
|
|||