├── chop-shop ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── src │ └── main │ │ ├── resources │ │ ├── static │ │ │ ├── style │ │ │ │ ├── custom.css │ │ │ │ └── ribbon.css │ │ │ ├── image │ │ │ │ ├── 1.jpg │ │ │ │ ├── 2.jpg │ │ │ │ ├── 3.jpg │ │ │ │ └── apisix.svg │ │ │ └── script │ │ │ │ ├── home.js │ │ │ │ └── checkout.js │ │ ├── application.properties │ │ └── templates │ │ │ ├── checkout.html │ │ │ └── home.html │ │ └── kotlin │ │ ├── Home.kt │ │ ├── Application.kt │ │ ├── Pricing.kt │ │ ├── Product.kt │ │ ├── Checkout.kt │ │ └── Cart.kt ├── sql │ ├── a_schema.sql │ └── b_data.sql ├── pom.xml ├── mvnw.cmd └── mvnw ├── apisix_conf └── config.yaml ├── renovate.json ├── pricing └── pricing.js ├── .gitignore ├── README.adoc ├── docker-compose.yml └── routes.sh /chop-shop/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrankel/chop-monolith/HEAD/chop-shop/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/style/custom.css: -------------------------------------------------------------------------------- 1 | i.bi { 2 | font-size: 1.2em; 3 | } 4 | 5 | a i.bi { 6 | color: black; 7 | } -------------------------------------------------------------------------------- /apisix_conf/config.yaml: -------------------------------------------------------------------------------- 1 | deployment: 2 | etcd: 3 | host: 4 | - "http://etcd:2397" 5 | admin: 6 | allow_admin: 7 | - 0.0.0.0/0 8 | -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/image/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrankel/chop-monolith/HEAD/chop-shop/src/main/resources/static/image/1.jpg -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/image/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrankel/chop-monolith/HEAD/chop-shop/src/main/resources/static/image/2.jpg -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/image/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrankel/chop-monolith/HEAD/chop-shop/src/main/resources/static/image/3.jpg -------------------------------------------------------------------------------- /chop-shop/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.r2dbc.url=r2dbc:mariadb://localhost:3306/chopshop 2 | spring.r2dbc.username=root 3 | spring.r2dbc.password=root 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "baseBranches": ["master", "api"] 7 | } 8 | -------------------------------------------------------------------------------- /chop-shop/sql/a_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS chopshop; 2 | 3 | USE chopshop; 4 | 5 | CREATE TABLE IF NOT EXISTS product ( 6 | id BIGINT NOT NULL PRIMARY KEY, 7 | name VARCHAR(50) NOT NULL, 8 | description VARCHAR(255), 9 | price DOUBLE NOT NULL, 10 | hero BOOLEAN NOT NULL 11 | ); 12 | -------------------------------------------------------------------------------- /chop-shop/sql/b_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO product(id, name, description, price, hero) VALUES 2 | (1, 'Stickers pack', 'A pack of rad stickers to display on your laptop or wherever you feel like. Show your love for Apache APISIX', 0.49, false), 3 | (2, 'Lapel pin', 'With this \'Powered by Apache APISIX\' lapel pin, support your favorite API Gateway and let everybody know about it.', 1.49, false), 4 | (3, 'Tee-Shirt', 'The classic geek product! At a conference, at home, at work, this tee-shirt will be your best friend.', 9.99, true) 5 | -------------------------------------------------------------------------------- /pricing/pricing.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (context, req) { 2 | context.log('Pricing computed from the function') 3 | const lines = req.body.lines 4 | context.log(`Received cart lines: ${JSON.stringify(lines)}`) 5 | const price = lines.reduce( 6 | (current, line) => { return current + line.first.price * line.second }, 7 | 0.0 8 | ) 9 | context.log(`Computed price: ${price}`) 10 | context.res = { 11 | body: { "price": price, "origin": "azure" } 12 | } 13 | context.done() 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | .env 35 | node_modules 36 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Home.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import org.springframework.web.reactive.function.server.* 4 | 5 | class HomeHandler(private val catalog: Catalog) { 6 | suspend fun displayPage(@Suppress("UNUSED_PARAMETER") req: ServerRequest) = 7 | ServerResponse.ok().renderAndAwait("home") 8 | 9 | suspend fun fetchCatalog(@Suppress("UNUSED_PARAMETER") req: ServerRequest) = 10 | ServerResponse.ok().bodyAndAwait(catalog.findAll()) 11 | } 12 | 13 | fun homeRoute(catalog: Catalog) = coRouter { 14 | val handler = HomeHandler(catalog) 15 | GET("/", handler::displayPage) 16 | GET("/catalog", handler::fetchCatalog) 17 | } 18 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Application.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.context.support.beans 6 | 7 | 8 | @SpringBootApplication 9 | class Application 10 | 11 | fun main(args: Array) { 12 | runApplication(*args) { 13 | addInitializers( 14 | beans { 15 | bean { productRoutes(ref()) } 16 | bean { homeRoute(ref()) } 17 | bean { cartRoutes(ref()) } 18 | bean { checkoutRoutes(ref()) } 19 | bean { pricingRoute() } 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | Before the demo, launch the MariaDB Docker image: 2 | 3 | [source,bash] 4 | ---- 5 | docker run --name maria --rm -it -e MARIADB_ROOT_PASSWORD=root -e MARIADB_DATABASE=chopshop -v `pwd`/chop-shop/sql:/docker-entrypoint-initdb.d:ro -p3306:3306 mariadb:10-focal 6 | ---- 7 | 8 | After the demo, stop the MariaDB Docker image: 9 | 10 | [source,bash] 11 | ---- 12 | docker stop maria 13 | ---- 14 | 15 | docker run --privileged --rm -it --hostname fn --name fnserver -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/app/data -e FN_DOCKER_NETWORKS=chopping-monolith_default -e FN_PORT=8008 -p 8008:8008 fnproject/fnserver 16 | 17 | export FN_API_URL=http://127.0.0.1:8008 18 | 19 | fn update context registry fndemouser 20 | 21 | fn create app pricingapp 22 | 23 | In the pricing folder: 24 | 25 | fn --verbose deploy --app pricingapp --local 26 | 27 | fn inspect f pricingapp pricing 28 | 29 | docker exec -it fnserver sh 30 | 31 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Pricing.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import kotlinx.coroutines.reactor.awaitSingle 4 | import org.springframework.web.reactive.function.server.* 5 | 6 | data class OriginPrice( 7 | val price: Double, 8 | val origin: String = "monolith" 9 | ) 10 | 11 | private fun Double.toOriginPrice() = OriginPrice(this) 12 | 13 | fun price(checkout: CheckoutView) = checkout.lines 14 | .fold(0.0) { current, line -> 15 | current + line.first.price * line.second 16 | }.toOriginPrice() 17 | 18 | class PricingHandler { 19 | suspend fun compute(req: ServerRequest): ServerResponse { 20 | val cart = req.bodyToMono().awaitSingle() 21 | val price = price(cart) 22 | return ServerResponse.ok().bodyValueAndAwait(price) 23 | } 24 | } 25 | 26 | fun pricingRoute() = coRouter { 27 | val handler = PricingHandler() 28 | POST("/price", handler::compute) 29 | } 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | apisix: 3 | image: apache/apisix:3.9.1-debian 4 | volumes: 5 | - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro 6 | restart: always 7 | ports: 8 | - "9080:9080" 9 | - "9180:9180" 10 | depends_on: 11 | - etcd 12 | etcd: 13 | image: bitnami/etcd:3.5.14 14 | environment: 15 | ETCD_ENABLE_V2: "true" 16 | ALLOW_NONE_AUTHENTICATION: "yes" 17 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2397" 18 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2397" 19 | chopshop: 20 | image: chopshop:1.3 21 | environment: 22 | SPRING_R2DBC_URL: "r2dbc:mariadb://mariadb:3306/chopshop" 23 | SPRING_DEVTOOLS_ADD-PROPERTIES: false 24 | depends_on: 25 | - mariadb 26 | mariadb: 27 | image: mariadb:11.3.2 28 | environment: 29 | MARIADB_ROOT_PASSWORD: "root" 30 | MARIADB_DATABASE: "chopshop" 31 | volumes: 32 | - ./chop-shop/sql:/docker-entrypoint-initdb.d:ro 33 | -------------------------------------------------------------------------------- /chop-shop/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 20 | -------------------------------------------------------------------------------- /routes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run --network chop-monolith_default --rm curlimages/curl:7.81.0 -v -i http://apisix:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' 3 | { 4 | "uri": "/*", 5 | "plugins" : { 6 | "response-rewrite": { 7 | "headers": { 8 | "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", 9 | "Expires": 0, 10 | "Pragma": "no-cache" 11 | } 12 | } 13 | }, 14 | "upstream": { 15 | "nodes": { 16 | "chopshop:8080": 1 17 | } 18 | } 19 | }' 20 | 21 | source .env 22 | 23 | docker run --network chop-monolith_default --rm curlimages/curl:7.81.0 -v -i http://apisix:9180/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' 24 | { 25 | "methods": ["POST"], 26 | "uris": ["/price"], 27 | "plugins": { 28 | "azure-functions": { 29 | "function_uri": "https://chopshoppricing.azurewebsites.net/api/JustPrice", 30 | "authorization": { 31 | "apikey": "'"$AZURE_FUNCTION_KEY1"'" 32 | }, 33 | "ssl_verify": false 34 | } 35 | } 36 | }' 37 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Product.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 5 | import org.springframework.web.reactive.function.server.ServerRequest 6 | import org.springframework.web.reactive.function.server.ServerResponse 7 | import org.springframework.web.reactive.function.server.bodyAndAwait 8 | import org.springframework.web.reactive.function.server.coRouter 9 | 10 | class Product( 11 | @Id 12 | val id: Long, 13 | val name: String, 14 | val description: String?, 15 | val price: Double, 16 | val hero: Boolean 17 | ) : Comparable { 18 | override fun compareTo(other: Product) = (id - other.id).toInt() 19 | } 20 | 21 | interface Catalog : CoroutineCrudRepository 22 | 23 | class ProductHandler(private val catalog: Catalog) { 24 | suspend fun getAll(@Suppress("UNUSED_PARAMETER") req: ServerRequest) = 25 | ServerResponse.ok().bodyAndAwait(catalog.findAll()) 26 | } 27 | 28 | fun productRoutes(catalog: Catalog) = coRouter { 29 | val handler = ProductHandler(catalog) 30 | GET("/product", handler::getAll) 31 | } 32 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Checkout.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import org.springframework.web.reactive.function.server.* 4 | 5 | @Suppress("UNUSED") 6 | class CheckoutView(val lines: List>) { 7 | constructor(cart: Cart): this(cart.content.entries.map { it.toPair() }) 8 | } 9 | 10 | internal fun Cart.toCheckout() = CheckoutView(this) 11 | 12 | class CheckoutHandler(private val catalog: Catalog) { 13 | suspend fun displayPage(@Suppress("UNUSED_PARAMETER") req: ServerRequest) = 14 | ServerResponse.ok().renderAndAwait("checkout") 15 | 16 | suspend fun fetchCheckout(@Suppress("UNUSED_PARAMETER") req: ServerRequest) = 17 | ServerResponse.ok().bodyValueAndAwait(req.cart().toCheckout()) 18 | 19 | suspend fun removeRow(req: ServerRequest): ServerResponse { 20 | val cart = req.cart() 21 | val productId = req.pathVariable("productId") 22 | val product = catalog.findById(productId.toLong()) 23 | product?.let { cart.remove(product) } 24 | return ServerResponse.ok().bodyValueAndAwait(req.cart().toCheckout()) 25 | } 26 | } 27 | 28 | fun checkoutRoutes(catalog: Catalog) = coRouter { 29 | val handler = CheckoutHandler(catalog) 30 | GET("/checkout", handler::displayPage) 31 | GET("/checkout/c", handler::fetchCheckout) 32 | DELETE("/checkout/remove/{productId}", handler::removeRow) 33 | } 34 | -------------------------------------------------------------------------------- /chop-shop/src/main/kotlin/Cart.kt: -------------------------------------------------------------------------------- 1 | package ch.frankel.chopshop 2 | 3 | import kotlinx.coroutines.reactor.awaitSingle 4 | import org.springframework.web.reactive.function.server.* 5 | 6 | class Cart { 7 | val content = sortedMapOf() 8 | 9 | fun add(product: Product, quantity: Int = 1) { 10 | content.merge(product, quantity) { current, _ -> 11 | current + quantity 12 | } 13 | } 14 | 15 | fun decrease(product: Product, quantity: Int = 1) { 16 | content.merge(product, quantity) { current, _ -> 17 | current - quantity 18 | } 19 | } 20 | 21 | fun remove(product: Product) { 22 | content.remove(product) 23 | } 24 | 25 | fun quantity() = content.values.sum() 26 | } 27 | 28 | class CartQuantityView(cart: Cart) { 29 | @Suppress("UNUSED") 30 | val quantity = cart.quantity() 31 | } 32 | 33 | suspend fun ServerRequest.cart(): Cart { 34 | val session = session().awaitSingle() 35 | val cart = session.getAttributeOrDefault("cart", Cart()) 36 | session.attributes["cart"] = cart 37 | return cart 38 | } 39 | 40 | class CartHandler(private val catalog: Catalog) { 41 | suspend fun add(req: ServerRequest): ServerResponse { 42 | val cart = req.cart() 43 | val productId = req.pathVariable("productId") 44 | val product = catalog.findById(productId.toLong()) 45 | return if (product == null) ServerResponse.badRequest().buildAndAwait() 46 | else { 47 | cart.add(product, 1) 48 | ServerResponse.ok().bodyValueAndAwait(CartQuantityView(cart)) 49 | } 50 | } 51 | 52 | suspend fun cartQuantity(req: ServerRequest) = 53 | ServerResponse.ok().bodyValueAndAwait(CartQuantityView(req.cart())) 54 | } 55 | 56 | fun cartRoutes(catalog: Catalog) = coRouter { 57 | val handler = CartHandler(catalog) 58 | GET("/cart/quantity", handler::cartQuantity) 59 | POST("/cart/add/{productId}", handler::add) 60 | } 61 | -------------------------------------------------------------------------------- /chop-shop/src/main/resources/templates/checkout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Checkout 11 | 12 | 13 | 21 |
22 |

Checkout

23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
ProductUnit priceQuantity
Grand total
43 |
44 |
45 | 46 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /chop-shop/src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Welcome of the Apache APISIX Chop Shop! 11 | 12 | 13 | 34 |
35 |
36 |
37 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/script/home.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const fetchCatalog = async event => { 3 | const response = await fetch('/catalog') 4 | displayProducts(await response.json()) 5 | attachEventHandlers() 6 | displayCart() 7 | } 8 | const displayProducts = products => { 9 | products.forEach(displayProduct) 10 | } 11 | const displayProduct = product => { 12 | const fragment = document.querySelector('#product').content.cloneNode(true) 13 | const card = fragment.querySelector('.card') 14 | if (!product.hero) { 15 | const ribbon = card.querySelector('.ribbon') 16 | ribbon.remove() 17 | } 18 | const img = card.querySelector('img') 19 | img.src = `/image/${product.id}.jpg` 20 | const title = card.querySelector('h5') 21 | title.innerText = product.name 22 | const text = card.querySelector('p') 23 | text.innerText = product.description 24 | const a = card.querySelector('a.btn') 25 | a.dataset.productId = product.id 26 | const icon = card.querySelector('i.bi') 27 | icon.dataset.productId = product.id 28 | const badge = card.querySelector('span.badge') 29 | badge.innerText = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD' }).format(product.price) 30 | badge.dataset.productId = product.id 31 | document.getElementById('products').appendChild(fragment) 32 | } 33 | const displayCart = async event => { 34 | const response = await fetch('/cart/quantity') 35 | updateQuantity(await response.json()) 36 | } 37 | const addToCart = async event => { 38 | const response = await fetch( 39 | `/cart/add/${event.target.dataset.productId}`, 40 | { method: 'POST' } 41 | ) 42 | updateQuantity(await response.json()) 43 | } 44 | const updateQuantity = async json => { 45 | let itemCount = document.getElementById('itemCount') 46 | itemCount.innerHTML = json.quantity 47 | let cartIcon = document.getElementById('cartIcon') 48 | cartIcon.classList.add('bi-cart-fill') 49 | cartIcon.classList.remove('bi-cart') 50 | } 51 | const attachEventHandlerToProductButton = button => { 52 | button.onclick = addToCart 53 | } 54 | const attachEventHandlers = () => { 55 | document.querySelectorAll('.add-to-cart').forEach(attachEventHandlerToProductButton) 56 | } 57 | window.onload = () => { 58 | fetchCatalog() 59 | } 60 | })() -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/script/checkout.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const fetchCheckout = async () => { 3 | const response = await fetch('/checkout/c') 4 | const json = await response.json() 5 | displayCheckout(json) 6 | fetchPrice(json) 7 | attachEventHandlers() 8 | } 9 | const displayCheckout = checkout => { 10 | const tbody = document.getElementById('checkout') 11 | while (tbody.firstChild) { 12 | tbody.removeChild(tbody.firstChild); 13 | } 14 | checkout.lines.forEach(displayCheckoutLine) 15 | } 16 | const fetchPrice = async checkout => { 17 | const response = await fetch('/price', { 18 | method: 'POST', 19 | body: JSON.stringify(checkout), 20 | headers: { 'Content-Type': 'application/json' } 21 | }) 22 | const price = await response.json() 23 | let total = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD' }).format(price.price) 24 | document.getElementById('total').innerHTML = total 25 | } 26 | const displayCheckoutLine = line => { 27 | const fragment = document.querySelector('#line').content.cloneNode(true) 28 | const image = fragment.querySelector('img') 29 | image.src = `/image/${line.first.id}.jpg` 30 | const nameCell = fragment.querySelectorAll('td')[1] 31 | nameCell.innerHTML = line.first.name 32 | const priceCell = fragment.querySelectorAll('td')[2] 33 | let price = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD' }).format(line.first.price) 34 | priceCell.innerHTML = price 35 | const qtyCell = fragment.querySelectorAll('td')[3] 36 | qtyCell.innerHTML = line.second 37 | const btn = fragment.querySelector('a') 38 | btn.dataset.productId = line.first.id 39 | const icon = fragment.querySelector('i') 40 | icon.dataset.productId = line.first.id 41 | document.getElementById('checkout').appendChild(fragment) 42 | } 43 | const removeFromCheckout = async event => { 44 | const response = await fetch( 45 | `/checkout/remove/${event.target.dataset.productId}`, 46 | { method: 'DELETE' } 47 | ) 48 | const json = await response.json() 49 | displayCheckout(json) 50 | fetchPrice(json) 51 | } 52 | const attachEventHandlerToRemoveButton = button => { 53 | button.onclick = removeFromCheckout 54 | } 55 | const attachEventHandlers = () => { 56 | document.querySelectorAll('.remove-from-checkout').forEach(attachEventHandlerToRemoveButton) 57 | } 58 | window.onload = () => { 59 | fetchCheckout() 60 | } 61 | })() -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/style/ribbon.css: -------------------------------------------------------------------------------- 1 | /* source: https://codepen.io/nxworld/pen/oLdoWb */ 2 | /* common */ 3 | .ribbon { 4 | width: 150px; 5 | height: 150px; 6 | overflow: hidden; 7 | position: absolute; 8 | } 9 | .ribbon::before, 10 | .ribbon::after { 11 | position: absolute; 12 | z-index: -1; 13 | content: ''; 14 | display: block; 15 | border: 5px solid #2980b9; 16 | } 17 | .ribbon span { 18 | position: absolute; 19 | display: block; 20 | width: 225px; 21 | padding: 15px 0; 22 | background-color: rgb(232,52,69); 23 | box-shadow: 0 5px 10px rgba(0,0,0,.1); 24 | color: #fff; 25 | font-style: 700 26 | font-size: 18px/1; 27 | text-shadow: 0 1px 1px rgba(0,0,0,.2); 28 | text-align: center; 29 | } 30 | 31 | /* top left*/ 32 | .ribbon-top-left { 33 | top: -10px; 34 | left: -10px; 35 | } 36 | .ribbon-top-left::before, 37 | .ribbon-top-left::after { 38 | border-top-color: transparent; 39 | border-left-color: transparent; 40 | } 41 | .ribbon-top-left::before { 42 | top: 0; 43 | right: 0; 44 | } 45 | .ribbon-top-left::after { 46 | bottom: 0; 47 | left: 0; 48 | } 49 | .ribbon-top-left span { 50 | right: -25px; 51 | top: 30px; 52 | transform: rotate(-45deg); 53 | } 54 | 55 | /* top right*/ 56 | .ribbon-top-right { 57 | top: -10px; 58 | right: -10px; 59 | } 60 | .ribbon-top-right::before, 61 | .ribbon-top-right::after { 62 | border-top-color: transparent; 63 | border-right-color: transparent; 64 | } 65 | .ribbon-top-right::before { 66 | top: 0; 67 | left: 0; 68 | } 69 | .ribbon-top-right::after { 70 | bottom: 0; 71 | right: 0; 72 | } 73 | .ribbon-top-right span { 74 | left: -25px; 75 | top: 30px; 76 | transform: rotate(45deg); 77 | } 78 | 79 | /* bottom left*/ 80 | .ribbon-bottom-left { 81 | bottom: -10px; 82 | left: -10px; 83 | } 84 | .ribbon-bottom-left::before, 85 | .ribbon-bottom-left::after { 86 | border-bottom-color: transparent; 87 | border-left-color: transparent; 88 | } 89 | .ribbon-bottom-left::before { 90 | bottom: 0; 91 | right: 0; 92 | } 93 | .ribbon-bottom-left::after { 94 | top: 0; 95 | left: 0; 96 | } 97 | .ribbon-bottom-left span { 98 | right: -25px; 99 | bottom: 30px; 100 | transform: rotate(225deg); 101 | } 102 | 103 | /* bottom right*/ 104 | .ribbon-bottom-right { 105 | bottom: -10px; 106 | right: -10px; 107 | } 108 | .ribbon-bottom-right::before, 109 | .ribbon-bottom-right::after { 110 | border-bottom-color: transparent; 111 | border-right-color: transparent; 112 | } 113 | .ribbon-bottom-right::before { 114 | bottom: 0; 115 | left: 0; 116 | } 117 | .ribbon-bottom-right::after { 118 | top: 0; 119 | right: 0; 120 | } 121 | .ribbon-bottom-right span { 122 | left: -25px; 123 | bottom: 30px; 124 | transform: rotate(-225deg); 125 | } -------------------------------------------------------------------------------- /chop-shop/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.1 9 | 10 | 11 | ch.frankel 12 | chop-shop 13 | 1.3 14 | 15 | 17 16 | 1.9.24 17 | 18 | 17 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-r2dbc 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-thymeleaf 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-webflux 32 | 33 | 34 | com.fasterxml.jackson.module 35 | jackson-module-kotlin 36 | 37 | 38 | io.projectreactor.kotlin 39 | reactor-kotlin-extensions 40 | 41 | 42 | org.jetbrains.kotlin 43 | kotlin-reflect 44 | 45 | 46 | org.jetbrains.kotlin 47 | kotlin-stdlib-jdk8 48 | 49 | 50 | org.jetbrains.kotlinx 51 | kotlinx-coroutines-reactor 52 | 53 | 54 | org.webjars 55 | bootstrap 56 | 5.3.3 57 | 58 | 59 | org.webjars.npm 60 | bootstrap-icons 61 | 1.11.3 62 | 63 | 64 | org.webjars 65 | webjars-locator 66 | 0.52 67 | 68 | 69 | org.mariadb 70 | r2dbc-mariadb 71 | 1.2.0 72 | runtime 73 | 74 | 75 | org.mariadb.jdbc 76 | mariadb-java-client 77 | runtime 78 | 79 | 80 | 81 | ${project.basedir}/src/main/kotlin 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | org.jetbrains.kotlin 89 | kotlin-maven-plugin 90 | 91 | 92 | -Xjsr305=strict 93 | -Xcontext-receivers 94 | 95 | 96 | spring 97 | 98 | 99 | 100 | 101 | org.jetbrains.kotlin 102 | kotlin-maven-allopen 103 | ${kotlin.version} 104 | 105 | 106 | 107 | 108 | com.google.cloud.tools 109 | jib-maven-plugin 110 | 3.4.2 111 | 112 | 113 | chopshop${version.qualifier}:${project.version} 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /chop-shop/src/main/resources/static/image/apisix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /chop-shop/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /chop-shop/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | --------------------------------------------------------------------------------