├── .dockerignore ├── .gitignore ├── README.md ├── docker-compose-v1.yml ├── docker-compose-v2.yml ├── docker-compose-v3.yml ├── init_swarm_google.sh ├── init_swarm_vbox.sh ├── likes ├── Dockerfile ├── Godeps │ ├── Godeps.json │ └── Readme ├── README.md ├── app │ ├── IndieFlower.ttf │ ├── angular.min.js │ ├── app.js │ ├── index.html │ └── style.css ├── likes.go └── vendor │ └── github.com │ └── gorilla │ ├── context │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── context.go │ └── doc.go │ └── mux │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── doc.go │ ├── mux.go │ ├── regexp.go │ └── route.go ├── push.sh ├── web ├── Dockerfile ├── default.conf ├── nginx.conf └── static │ ├── IndieFlower.ttf │ ├── angular.min.js │ ├── app.js │ ├── index.html │ ├── share.png │ └── style.css ├── words-dispatcher ├── Dockerfile └── dispatcher.go └── words-java ├── Dockerfile ├── pom.xml └── src └── main └── java ├── AddHostName.java ├── Main.java ├── WordResponse.java └── Words.java /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | Dockerfile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | target/ 4 | docker-compose.yml 5 | images.tar 6 | certificates -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cadavres Exquis "Swarm edition" 2 | 3 | ## Warm-up 4 | 5 | If you plan on attending this workshop, you need: 6 | 7 | * A laptop + power cord 8 | * Install Docker 1.12: 9 | * With [Docker for Mac](https://download.docker.com/mac/beta/Docker.dmg) or [Docker for Windows](https://download.docker.com/win/beta/InstallDocker.msi) 10 | * On Windows older than Windows 10 Pro, install [Docker Toolbox](https://github.com/docker/toolbox/releases/download/v1.12.0/DockerToolbox-1.12.0.exe) instead 11 | * On Linux, grab [docker-compose v1.8.1](https://github.com/docker/compose/releases/download/1.8.1/docker-compose-Linux-x86_64) and put it in your path somewhere. 12 | * Test that your docker installation works fine: 13 | * `docker version` should show version `1.12` on both Client and Server side. 14 | * Run `docker run hello-world` and check you see the welcome message. 15 | * Get the source of the lab by `git clone git@github.com:CodeStory/lab-docker.git` 16 | * During the workshop, we'll distribute USB keys with the docker images we need for the lab: 17 | * `cd lab-docker` 18 | * `docker load -i images.tar` will load all the images you need. 19 | * As an alternate solution, you can load the images directly from the hub: 20 | * `docker pull dockerdemos/lab-web` 21 | * `docker pull dockerdemos/lab-words-dispatcher` 22 | * `docker pull dockerdemos/lab-words-java` 23 | * `docker pull mongo-express:0.31.0` 24 | * `docker pull mongo:3.3.15` 25 | 26 | ## 1 - Look Ma', micro-services on my laptop 27 | 28 | Our first version of the application is composed of four micro-services: 29 | 30 | - A `web` service that uses `nginx` running on port `80` to serve an HTML5/Js 31 | application written in angularJs. 32 | - A `words-java` service that runs a `java` web server on a random port. This 33 | server connects to the database and exposes a Rest Api to the `web`. 34 | - A `db` service that runs a `mongoDb` database on a random port. 35 | - A `db-ui` service that runs a web UI on port `8081` to edit the content of 36 | the database 37 | 38 | ## Let's run the application 39 | 40 | 1. Point Docker CLI to the Docker daemon: 41 | 42 | - If you have `Docker for Mac` or `Docker for Windows`, there's nothing to be 43 | done. Run `docker info` to check that everything is up and running. 44 | 45 | - If you have `Docker Toolbox`, either open the `Quick Start` terminal or run 46 | `docker-machine env` to show the command you have to **run** to point to the 47 | Docker daemon running on the VirtualBox VM. On OSX, it's typically: 48 | 49 | ``` 50 | eval $(docker-machine env default) 51 | ``` 52 | 53 | 2. Configure *Docker Compose* to use the first configuration file: 54 | 55 | ``` 56 | cd lab-docker 57 | cp docker-compose-v1.yml docker-compose.yml 58 | ``` 59 | 60 | 3. Build and start the application: 61 | 62 | ``` 63 | docker-compose up -d 64 | ``` 65 | 66 | 4. Take a look at the logs, to see if there's any error: 67 | 68 | ``` 69 | docker-compose logs 70 | ``` 71 | 72 | 5. List the running containers: 73 | 74 | ``` 75 | docker-compose ps 76 | 77 | Name Command State Ports 78 | --------------------------------------------------------------------------------------------- 79 | labdocker_db-ui_1 tini -- node app Up 0.0.0.0:8081->8081/tcp 80 | labdocker_db_1 /entrypoint.sh mongod Up 27017/tcp 81 | labdocker_web_1 nginx -g daemon off; Up 443/tcp, 0.0.0.0:80->80/tcp 82 | labdocker_words-java_1 java -DPROD_MODE=true -Xmx ... Up 8080/tcp 83 | ``` 84 | 85 | ## Let's use the application 86 | 87 | With Docker for Mac and Docker for Windows, you can open a browser on "http://localhost". 88 | With Docker Toolbox, get the ip address of the VM with `docker-machine ip default` and open a browser on "http://[THE_IP]". 89 | 90 | You should see a random composed of 5 random words: a noun, an adjective, 91 | a verb, a noun and an adjective. That's a "Cadavre Exquis"! You did it! 92 | 93 | However, you'll notice that it's always the same sentence that's displayed. 94 | We have to fix that! And will do it without touching the code... 95 | 96 | ## How does it work? 97 | 98 | The angularJs application served by the `nginx` based `web` service sends 5 99 | http `GET` queries to the `nginx` that proxies the `words` REST service. 100 | 101 | That was easy because with docker, each service can be reached on the network 102 | via it's name. 103 | 104 | On each query, the `words` service loads all the words from the `mongo` database, 105 | chooses a random one and memoizes it so that future queries are served from the 106 | memory and not from the database. 107 | 108 | ## What to explore in this step 109 | 110 | 1. The `db-ui` web UI can be used to configure the list of words in the database. 111 | * Use this command to find the url for the UI: `docker-compose port db-ui 8081` 112 | * Add some nouns, adjectives and verbs, use non-plural and male noun and adjectives, or the grammar will not be correct. 113 | * **Careful**, all words added to the database at this stage will be lost for the next stages. 114 | 115 | 2. You can improve the web UI: 116 | * Change something in `web/static/index.html` 117 | * Then `docker-compose stop web; docker-compose rm -f web; docker-compose build web; docker up -d web`, see how this updates a single micro service. 118 | 119 | 3. Things to check in this step: 120 | * Notice the db connexion string in `words-java/src/main/java/Main.java` 121 | * Notice the nginx configuration in `web/default.conf` on `location /words/` and check the corresponding code in `web/static/app.js` 122 | * Notice in the `docker-compose-v1.yml` file some services have `build` and `image` instructions while others have only `image` 123 | * Notice the `ports` vs `expose` instructions, try to find a way to call the `/verb` instruction on the `words-java`, without changing the `yml` file. 124 | 125 | # 2 - Run the application with a dispatcher 126 | 127 | We are going to change the micro-service based architecture of our application 128 | without changing its code. That's neat! 129 | 130 | Our idea is to introduce an additional micro-service between the `web` and the 131 | `java` rest api. This new component is a Go based web server that will later 132 | help dispatch word queries to multiple java REST backends. 133 | 134 | ## Let's use the application 135 | 136 | 1. Stop the application currently running: 137 | 138 | ``` 139 | cd lab-docker 140 | docker-compose stop 141 | docker-compose rm -f 142 | ``` 143 | 144 | 2. Configure *Docker Compose* to use the second configuration file: 145 | 146 | ``` 147 | cp docker-compose-v2.yml docker-compose.yml 148 | ``` 149 | 150 | 3. Build and start the application: 151 | 152 | ``` 153 | docker-compose up -d 154 | docker-compose logs 155 | ``` 156 | 157 | As a user, you should see no difference compared to the original application. 158 | That's the whole point! 159 | 160 | ## How is that possible? 161 | 162 | The `web`'s expectation is that a `words` host exists on the network and that 163 | it responds on port `8080`. What we did it renamed the `words` service to 164 | `words-java` and introduced a new `go` based service under the name of `words`. 165 | 166 | > Step 1, we had: web:80 -> words:8080 (java) 167 | > Now, we have: web:80 -> words:8080 (go) -> words-java:8080 (java) 168 | 169 | Thanks to Docker networking and the `expose` configuration, we can have two 170 | services running on port `8080` without a conflict. An automatic translation 171 | will be done by the network. We don't have to change our code. How cool is that? 172 | 173 | ## What to explore in this step 174 | 175 | 1. Check the logs and see the dispatcher in action 176 | * Run `docker-compose logs -f` and refresh your page at will, check the dispatcher work described in the logs. 177 | 178 | 2. Check the dispatcher code 179 | * Especially the `forward` function in the `words-dispatcher/dispatcher.go` source. 180 | * **careful** this code is not really efficient but it serves well the purpose of this workshop 181 | 182 | # 3 - Run the application on a shared Swarm with Docker 1.12 services 183 | 184 | We are going to the Cloud! Your containers will be send to a shared Swarm 185 | composed of multiple nodes. We have already setup the Swarm for you before the talk. 186 | You just need to point your Docker CLI to the Swarm rather than to your local 187 | Docker daemon. 188 | This is done through environment variables. And because our Swarm has TLS enabled, 189 | you need a copy of our certificates. We'll pass along a couple of USB keys with 190 | the certificates on them. Then follow the instructions below: 191 | 192 | 1. Stop the application currently running: 193 | 194 | ``` 195 | cd lab-docker 196 | docker-compose stop 197 | docker-compose rm -f 198 | ``` 199 | 200 | 2. Copy the provided `certificates` from the USB key. 201 | 202 | 3. Point your docker client to the proper machine: 203 | 204 | If you are on the Google Cloud Swarm cluster: 205 | 206 | ``` 207 | export DOCKER_TLS_VERIFY="1" 208 | export DOCKER_HOST="tcp://104.155.53.144:2376" 209 | export DOCKER_CERT_PATH="$(pwd)/certificates" 210 | ``` 211 | 212 | 4. Confirm that `docker node ls` shows multiple nodes. 213 | 214 | 5. Configure *Docker Compose* to use the third configuration file: 215 | 216 | ``` 217 | cd lab-docker 218 | cp docker-compose-v3.yml docker-compose.yml 219 | ``` 220 | 221 | 5. Create a bundle file: 222 | 223 | ``` 224 | docker-compose bundle -o MY-UNIQUE-TEAM-NAME.dab 225 | docker deploy MY-UNIQUE-TEAM-NAME 226 | docker service ls 227 | ``` 228 | 229 | 6. Get the port of the `web` service. 230 | 231 | ``` 232 | docker service inspect --pretty MY-UNIQUE-TEAM-NAME_web | tail -n1 233 | ``` 234 | 235 | 7. Open the browser on `http://104.155.53.144:PORT` 236 | 237 | The same application that ran on you machine now runs in the Cloud on a shared Swarm. 238 | 239 | ## How is that possible? 240 | 241 | If you compare [docker-compose-v2.yml](docker-compose-v2.yml) and [docker-compose-v3.yml](docker-compose-v3.yml) 242 | you'll see that all the services now use a private network now. This network is 243 | created by *Docker Compose*. Its name is `private`, prefixed by the name of your 244 | project (ie your team name). It's a network available to your containers only. 245 | 246 | Thanks to this private network, multiple similar applications can coexist on a 247 | Swarm. 248 | 249 | All the services with the same name or alias on a shared network 250 | will be reachable on the same DNS name. A client can get all the IPs for 251 | the DNS name and start load balancing between the nodes. Nothing complicated 252 | to setup! 253 | 254 | That's exactly what the `words-dispatcher` does. To bypass the DNS cache, it 255 | searches for all the IPs for the `works-java` services and uses a random one 256 | each time. This effectively load balances queries among all the teams. 257 | 258 | ## What to explore in this step 259 | 260 | 1. You can increase the numbers of `words-java` nodes and see how the dispatcher react. 261 | * Add more `words-java` node with `docker service scale MY-UNIQUE-TEAM-NAME_words-java=4`. 262 | * You have now 4 `words-java` containers. Check their numbers with `docker service ls` 263 | 264 | 2. You can kill containers and see them respawned 265 | 266 | # Docker Features demonstrated 267 | 268 | * Multi-host Networking - *Docker 1.9* 269 | * New compose file - *Docker 1.10* 270 | * Use links in networks - *Docker 1.10* 271 | * Network-wide container aliases - *Docker 1.10* 272 | * DNS discovery - *Docker 1.11* 273 | * Build in docker-compose up - *Docker-Compose 1.7* 274 | * Bundles and Services - *Docker 1.12* 275 | 276 | # About 'Cadavres Exquis' 277 | 278 | Cadavres Exquis is a French word game, you'll find more on 279 | [wikipedia page](https://fr.wikipedia.org/wiki/Cadavre_exquis_(jeu)) (in French) 280 | 281 | # How did we create the Swarm? 282 | 283 | The Swarm has been created on Google Cloud with the [init_swarm_google.sh](init_swarm_google.sh) script. Take a look to what we do there but you'll need an account and this may cost you money. 284 | You can also try it on your own laptop by running the [init_swarm_vbox.sh](init_swarm_vbox.sh), you'll need virtualbox and `docker-machine` 285 | -------------------------------------------------------------------------------- /docker-compose-v1.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | web: 5 | build: web 6 | image: dockerdemos/lab-web 7 | ports: ["80:80"] 8 | 9 | words: 10 | build: words-java 11 | image: dockerdemos/lab-words-java 12 | expose: ["8080"] 13 | 14 | db-ui: 15 | image: mongo-express:0.31.0 16 | ports: ["8081:8081"] 17 | 18 | mongo: 19 | image: mongo:3.3.15 20 | expose: ["27017"] 21 | -------------------------------------------------------------------------------- /docker-compose-v2.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | web: 5 | build: web 6 | image: dockerdemos/lab-web 7 | ports: ["80:80"] 8 | 9 | words: 10 | build: words-dispatcher 11 | image: dockerdemos/lab-words-dispatcher 12 | expose: ["8080"] 13 | 14 | words-java: 15 | build: words-java 16 | image: dockerdemos/lab-words-java 17 | expose: ["8080"] 18 | 19 | db-ui: 20 | image: mongo-express:0.31.0 21 | ports: ["8081:8081"] 22 | 23 | mongo: 24 | image: mongo:3.3.15 25 | expose: ["27017"] 26 | -------------------------------------------------------------------------------- /docker-compose-v3.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | web: 5 | image: dockerdemos/lab-web 6 | ports: ["80"] 7 | networks: ["private"] 8 | 9 | words: 10 | image: dockerdemos/lab-words-dispatcher 11 | networks: ["private"] 12 | 13 | words-java: 14 | image: dockerdemos/lab-words-java 15 | networks: ["private"] 16 | 17 | mongo: 18 | image: mongo:3.3.15 19 | networks: ["private"] 20 | 21 | networks: 22 | private: 23 | -------------------------------------------------------------------------------- /init_swarm_google.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export GOOGLE_PROJECT="code-story-blog" 6 | export GOOGLE_ZONE="europe-west1-d" 7 | export GOOGLE_DISK_SIZE="1000" 8 | export GOOGLE_MACHINE_TYPE="custom-2-8192" 9 | 10 | docker-machine create -d google gce-manager 11 | docker-machine create -d google gce-worker1 12 | docker-machine create -d google gce-worker2 13 | docker-machine create -d google gce-worker3 14 | 15 | docker $(docker-machine config gce-manager) \ 16 | swarm init \ 17 | --listen-addr $(gcloud compute instances list gce-manager --format='value(networkInterfaces.networkIP)'):2377 \ 18 | --advertise-addr $(gcloud compute instances list gce-manager --format='value(networkInterfaces.networkIP)'):2377 19 | 20 | docker $(docker-machine config gce-worker1) \ 21 | swarm join $(docker-machine ip gce-manager):2377 \ 22 | --listen-addr $(gcloud compute instances list gce-worker1 --format='value(networkInterfaces.networkIP)'):2377 \ 23 | --token $(docker $(docker-machine config gce-manager) swarm join-token worker -q) 24 | 25 | docker $(docker-machine config gce-worker2) \ 26 | swarm join $(docker-machine ip gce-manager):2377 \ 27 | --listen-addr $(gcloud compute instances list gce-worker2 --format='value(networkInterfaces.networkIP)'):2377 \ 28 | --token $(docker $(docker-machine config gce-manager) swarm join-token worker -q) 29 | 30 | docker $(docker-machine config gce-worker3) \ 31 | swarm join $(docker-machine ip gce-manager):2377 \ 32 | --listen-addr $(gcloud compute instances list gce-worker3 --format='value(networkInterfaces.networkIP)'):2377 \ 33 | --token $(docker $(docker-machine config gce-manager) swarm join-token worker -q) 34 | 35 | docker $(docker-machine config gce-manager) info 36 | -------------------------------------------------------------------------------- /init_swarm_vbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | docker-machine create -d virtualbox manager 6 | docker-machine create -d virtualbox worker1 7 | docker-machine create -d virtualbox worker2 8 | docker-machine create -d virtualbox worker3 9 | 10 | docker $(docker-machine config manager) \ 11 | swarm init \ 12 | --listen-addr $(docker-machine ip manager):2377 \ 13 | --advertise-addr $(docker-machine ip manager):2377 14 | 15 | docker $(docker-machine config worker1) \ 16 | swarm join $(docker-machine ip manager):2377 \ 17 | --listen-addr $(docker-machine ip worker1):2377 \ 18 | --token $(docker $(docker-machine config manager) swarm join-token worker -q) 19 | 20 | docker $(docker-machine config worker2) \ 21 | swarm join $(docker-machine ip manager):2377 \ 22 | --listen-addr $(docker-machine ip worker2):2377 \ 23 | --token $(docker $(docker-machine config manager) swarm join-token worker -q) 24 | 25 | docker $(docker-machine config worker3) \ 26 | swarm join $(docker-machine ip manager):2377 \ 27 | --listen-addr $(docker-machine ip worker3):2377 \ 28 | --token $(docker $(docker-machine config manager) swarm join-token worker -q) 29 | 30 | docker $(docker-machine config manager) info 31 | -------------------------------------------------------------------------------- /likes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.6.1-alpine 2 | 3 | EXPOSE 8080 4 | WORKDIR /go/src/likes 5 | CMD ["./likes"] 6 | 7 | COPY . ./ 8 | RUN go get -d -v && go build -v 9 | -------------------------------------------------------------------------------- /likes/Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/dgageot/lab-docker/likes", 3 | "GoVersion": "go1.6", 4 | "GodepVersion": "v62", 5 | "Deps": [ 6 | { 7 | "ImportPath": "github.com/gorilla/context", 8 | "Rev": "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 9 | }, 10 | { 11 | "ImportPath": "github.com/gorilla/mux", 12 | "Comment": "v1.1", 13 | "Rev": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /likes/Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /likes/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | $ http GET localhost:8080/likes 3 | HTTP/1.1 200 OK 4 | Content-Length: 31 5 | Content-Type: application/json 6 | Date: Sat, 09 Apr 2016 14:10:07 GMT 7 | 8 | [ 9 | { 10 | "Name": "foo" 11 | }, 12 | { 13 | "Name": "bar" 14 | } 15 | ] 16 | 17 | $ http PUT localhost:8080/likes name=polka 18 | HTTP/1.1 201 Created 19 | Content-Length: 0 20 | Content-Type: application/json 21 | Date: Sat, 09 Apr 2016 14:10:14 GMT 22 | 23 | 24 | 25 | $ http GET localhost:8080/likes 26 | HTTP/1.1 200 OK 27 | Content-Length: 48 28 | Content-Type: application/json 29 | Date: Sat, 09 Apr 2016 14:10:17 GMT 30 | 31 | [ 32 | { 33 | "Name": "foo" 34 | }, 35 | { 36 | "Name": "bar" 37 | }, 38 | { 39 | "Name": "polka" 40 | } 41 | ] 42 | ``` 43 | -------------------------------------------------------------------------------- /likes/app/IndieFlower.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/lab-docker/7a8e12822aadc28aa91e3c4c59e6cb044f2229d9/likes/app/IndieFlower.ttf -------------------------------------------------------------------------------- /likes/app/app.js: -------------------------------------------------------------------------------- 1 | var likeApp = angular.module('likeApp', []); 2 | 3 | likeApp.controller('likeController', function ($scope, $http) { 4 | $http.get('/likes').success(function(data){ 5 | $scope.likes = data; 6 | }); 7 | }); -------------------------------------------------------------------------------- /likes/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Like 6 | 7 | 8 | 9 |

Cadavre Exquis

10 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /likes/app/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Indie Flower'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Indie Flower'), local('IndieFlower'), url('IndieFlower.ttf') format('woff2'); 6 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 7 | } 8 | 9 | body { 10 | font-family: 'Indie Flower', cursive; 11 | } 12 | 13 | h1 { 14 | margin:0; 15 | text-align: center; 16 | font-size: 80px; 17 | } 18 | 19 | ul 20 | { 21 | list-style-type: none; 22 | } 23 | 24 | li { 25 | text-decoration: none; 26 | font-size: 60px; 27 | } 28 | -------------------------------------------------------------------------------- /likes/likes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "io/ioutil" 7 | "github.com/gorilla/mux" 8 | "encoding/json" 9 | ) 10 | 11 | // Like a rolling stone 12 | type Like struct { 13 | Name string `json:"name"` 14 | } 15 | 16 | // Server serves 17 | type Server struct { 18 | likes []Like 19 | } 20 | 21 | func (server *Server) start(port int) { 22 | router := mux.NewRouter() 23 | router.HandleFunc("/likes", server.options).Methods("OPTIONS") 24 | router.HandleFunc("/likes", server.allLike).Methods("GET") 25 | router.HandleFunc("/likes", server.addLike).Methods("POST") 26 | router.PathPrefix("/").Handler(http.FileServer(http.Dir("./app"))) 27 | http.Handle("/", router) 28 | 29 | fmt.Println("Listening on", port) 30 | http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 31 | } 32 | 33 | func main() { 34 | server := Server{[]Like{Like{"foo"},Like{"bar"}}} 35 | server.start(8080) 36 | } 37 | 38 | func (server *Server) addLike(writer http.ResponseWriter, request *http.Request ) { 39 | bytes, err := ioutil.ReadAll(request.Body) 40 | if err != nil { 41 | http.Error(writer, err.Error(), http.StatusInternalServerError) 42 | return 43 | } 44 | like := Like{} 45 | err = json.Unmarshal(bytes, &like) 46 | if err != nil { 47 | http.Error(writer, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | server.likes = append([]Like{like}, server.likes...) 51 | writeCORSHeader(writer) 52 | writer.Header().Set("Content-Type", "application/json") 53 | writer.WriteHeader(http.StatusCreated) 54 | } 55 | 56 | func (server *Server) allLike(writer http.ResponseWriter, request *http.Request ) { 57 | payload, err := json.Marshal(server.likes) 58 | if err != nil { 59 | http.Error(writer, err.Error(), http.StatusInternalServerError) 60 | return 61 | } 62 | writer.Header().Set("Content-Type", "application/json") 63 | writer.Write(payload) 64 | } 65 | 66 | func (server *Server) options(writer http.ResponseWriter, request *http.Request ) { 67 | writeCORSHeader(writer) 68 | writer.WriteHeader(http.StatusOK) 69 | } 70 | 71 | func writeCORSHeader(writer http.ResponseWriter) { 72 | writer.Header().Set("Access-Control-Allow-Origin","*") 73 | writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") 74 | writer.Header().Set("Access-Control-Allow-Headers", "X-DOCKERFTW, Content-Type") 75 | } -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/context/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.3 7 | - go: 1.4 8 | - go: 1.5 9 | - go: 1.6 10 | - go: tip 11 | 12 | install: 13 | - go get golang.org/x/tools/cmd/vet 14 | 15 | script: 16 | - go get -t -v ./... 17 | - diff -u <(echo -n) <(gofmt -d .) 18 | - go tool vet . 19 | - go test -v -race ./... 20 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/context/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/context/README.md: -------------------------------------------------------------------------------- 1 | context 2 | ======= 3 | [![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) 4 | 5 | gorilla/context is a general purpose registry for global request variables. 6 | 7 | Read the full documentation here: http://www.gorillatoolkit.org/pkg/context 8 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "net/http" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ( 14 | mutex sync.RWMutex 15 | data = make(map[*http.Request]map[interface{}]interface{}) 16 | datat = make(map[*http.Request]int64) 17 | ) 18 | 19 | // Set stores a value for a given key in a given request. 20 | func Set(r *http.Request, key, val interface{}) { 21 | mutex.Lock() 22 | if data[r] == nil { 23 | data[r] = make(map[interface{}]interface{}) 24 | datat[r] = time.Now().Unix() 25 | } 26 | data[r][key] = val 27 | mutex.Unlock() 28 | } 29 | 30 | // Get returns a value stored for a given key in a given request. 31 | func Get(r *http.Request, key interface{}) interface{} { 32 | mutex.RLock() 33 | if ctx := data[r]; ctx != nil { 34 | value := ctx[key] 35 | mutex.RUnlock() 36 | return value 37 | } 38 | mutex.RUnlock() 39 | return nil 40 | } 41 | 42 | // GetOk returns stored value and presence state like multi-value return of map access. 43 | func GetOk(r *http.Request, key interface{}) (interface{}, bool) { 44 | mutex.RLock() 45 | if _, ok := data[r]; ok { 46 | value, ok := data[r][key] 47 | mutex.RUnlock() 48 | return value, ok 49 | } 50 | mutex.RUnlock() 51 | return nil, false 52 | } 53 | 54 | // GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. 55 | func GetAll(r *http.Request) map[interface{}]interface{} { 56 | mutex.RLock() 57 | if context, ok := data[r]; ok { 58 | result := make(map[interface{}]interface{}, len(context)) 59 | for k, v := range context { 60 | result[k] = v 61 | } 62 | mutex.RUnlock() 63 | return result 64 | } 65 | mutex.RUnlock() 66 | return nil 67 | } 68 | 69 | // GetAllOk returns all stored values for the request as a map and a boolean value that indicates if 70 | // the request was registered. 71 | func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { 72 | mutex.RLock() 73 | context, ok := data[r] 74 | result := make(map[interface{}]interface{}, len(context)) 75 | for k, v := range context { 76 | result[k] = v 77 | } 78 | mutex.RUnlock() 79 | return result, ok 80 | } 81 | 82 | // Delete removes a value stored for a given key in a given request. 83 | func Delete(r *http.Request, key interface{}) { 84 | mutex.Lock() 85 | if data[r] != nil { 86 | delete(data[r], key) 87 | } 88 | mutex.Unlock() 89 | } 90 | 91 | // Clear removes all values stored for a given request. 92 | // 93 | // This is usually called by a handler wrapper to clean up request 94 | // variables at the end of a request lifetime. See ClearHandler(). 95 | func Clear(r *http.Request) { 96 | mutex.Lock() 97 | clear(r) 98 | mutex.Unlock() 99 | } 100 | 101 | // clear is Clear without the lock. 102 | func clear(r *http.Request) { 103 | delete(data, r) 104 | delete(datat, r) 105 | } 106 | 107 | // Purge removes request data stored for longer than maxAge, in seconds. 108 | // It returns the amount of requests removed. 109 | // 110 | // If maxAge <= 0, all request data is removed. 111 | // 112 | // This is only used for sanity check: in case context cleaning was not 113 | // properly set some request data can be kept forever, consuming an increasing 114 | // amount of memory. In case this is detected, Purge() must be called 115 | // periodically until the problem is fixed. 116 | func Purge(maxAge int) int { 117 | mutex.Lock() 118 | count := 0 119 | if maxAge <= 0 { 120 | count = len(data) 121 | data = make(map[*http.Request]map[interface{}]interface{}) 122 | datat = make(map[*http.Request]int64) 123 | } else { 124 | min := time.Now().Unix() - int64(maxAge) 125 | for r := range data { 126 | if datat[r] < min { 127 | clear(r) 128 | count++ 129 | } 130 | } 131 | } 132 | mutex.Unlock() 133 | return count 134 | } 135 | 136 | // ClearHandler wraps an http.Handler and clears request values at the end 137 | // of a request lifetime. 138 | func ClearHandler(h http.Handler) http.Handler { 139 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 | defer Clear(r) 141 | h.ServeHTTP(w, r) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/context/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package context stores values shared during a request lifetime. 7 | 8 | For example, a router can set variables extracted from the URL and later 9 | application handlers can access those values, or it can be used to store 10 | sessions values to be saved at the end of a request. There are several 11 | others common uses. 12 | 13 | The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: 14 | 15 | http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 16 | 17 | Here's the basic usage: first define the keys that you will need. The key 18 | type is interface{} so a key can be of any type that supports equality. 19 | Here we define a key using a custom int type to avoid name collisions: 20 | 21 | package foo 22 | 23 | import ( 24 | "github.com/gorilla/context" 25 | ) 26 | 27 | type key int 28 | 29 | const MyKey key = 0 30 | 31 | Then set a variable. Variables are bound to an http.Request object, so you 32 | need a request instance to set a value: 33 | 34 | context.Set(r, MyKey, "bar") 35 | 36 | The application can later access the variable using the same key you provided: 37 | 38 | func MyHandler(w http.ResponseWriter, r *http.Request) { 39 | // val is "bar". 40 | val := context.Get(r, foo.MyKey) 41 | 42 | // returns ("bar", true) 43 | val, ok := context.GetOk(r, foo.MyKey) 44 | // ... 45 | } 46 | 47 | And that's all about the basic usage. We discuss some other ideas below. 48 | 49 | Any type can be stored in the context. To enforce a given type, make the key 50 | private and wrap Get() and Set() to accept and return values of a specific 51 | type: 52 | 53 | type key int 54 | 55 | const mykey key = 0 56 | 57 | // GetMyKey returns a value for this package from the request values. 58 | func GetMyKey(r *http.Request) SomeType { 59 | if rv := context.Get(r, mykey); rv != nil { 60 | return rv.(SomeType) 61 | } 62 | return nil 63 | } 64 | 65 | // SetMyKey sets a value for this package in the request values. 66 | func SetMyKey(r *http.Request, val SomeType) { 67 | context.Set(r, mykey, val) 68 | } 69 | 70 | Variables must be cleared at the end of a request, to remove all values 71 | that were stored. This can be done in an http.Handler, after a request was 72 | served. Just call Clear() passing the request: 73 | 74 | context.Clear(r) 75 | 76 | ...or use ClearHandler(), which conveniently wraps an http.Handler to clear 77 | variables at the end of a request lifetime. 78 | 79 | The Routers from the packages gorilla/mux and gorilla/pat call Clear() 80 | so if you are using either of them you don't need to clear the context manually. 81 | */ 82 | package context 83 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.2 7 | - go: 1.3 8 | - go: 1.4 9 | - go: 1.5 10 | - go: 1.6 11 | - go: tip 12 | 13 | install: 14 | - go get golang.org/x/tools/cmd/vet 15 | 16 | script: 17 | - go get -t -v ./... 18 | - diff -u <(echo -n) <(gofmt -d .) 19 | - go tool vet . 20 | - go test -v -race ./... 21 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/README.md: -------------------------------------------------------------------------------- 1 | mux 2 | === 3 | [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) 4 | [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) 5 | 6 | http://www.gorillatoolkit.org/pkg/mux 7 | 8 | Package `gorilla/mux` implements a request router and dispatcher. 9 | 10 | The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: 11 | 12 | * Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. 13 | * URL hosts and paths can have variables with an optional regular expression. 14 | * Registered URLs can be built, or "reversed", which helps maintaining references to resources. 15 | * Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. 16 | * It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. 17 | 18 | Let's start registering a couple of URL paths and handlers: 19 | 20 | ```go 21 | func main() { 22 | r := mux.NewRouter() 23 | r.HandleFunc("/", HomeHandler) 24 | r.HandleFunc("/products", ProductsHandler) 25 | r.HandleFunc("/articles", ArticlesHandler) 26 | http.Handle("/", r) 27 | } 28 | ``` 29 | 30 | Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. 31 | 32 | Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: 33 | 34 | ```go 35 | r := mux.NewRouter() 36 | r.HandleFunc("/products/{key}", ProductHandler) 37 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 38 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 39 | ``` 40 | 41 | The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: 42 | 43 | ```go 44 | vars := mux.Vars(request) 45 | category := vars["category"] 46 | ``` 47 | 48 | And this is all you need to know about the basic usage. More advanced options are explained below. 49 | 50 | Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: 51 | 52 | ```go 53 | r := mux.NewRouter() 54 | // Only matches if domain is "www.example.com". 55 | r.Host("www.example.com") 56 | // Matches a dynamic subdomain. 57 | r.Host("{subdomain:[a-z]+}.domain.com") 58 | ``` 59 | 60 | There are several other matchers that can be added. To match path prefixes: 61 | 62 | ```go 63 | r.PathPrefix("/products/") 64 | ``` 65 | 66 | ...or HTTP methods: 67 | 68 | ```go 69 | r.Methods("GET", "POST") 70 | ``` 71 | 72 | ...or URL schemes: 73 | 74 | ```go 75 | r.Schemes("https") 76 | ``` 77 | 78 | ...or header values: 79 | 80 | ```go 81 | r.Headers("X-Requested-With", "XMLHttpRequest") 82 | ``` 83 | 84 | ...or query values: 85 | 86 | ```go 87 | r.Queries("key", "value") 88 | ``` 89 | 90 | ...or to use a custom matcher function: 91 | 92 | ```go 93 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 94 | return r.ProtoMajor == 0 95 | }) 96 | ``` 97 | 98 | ...and finally, it is possible to combine several matchers in a single route: 99 | 100 | ```go 101 | r.HandleFunc("/products", ProductsHandler). 102 | Host("www.example.com"). 103 | Methods("GET"). 104 | Schemes("http") 105 | ``` 106 | 107 | Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". 108 | 109 | For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: 110 | 111 | ```go 112 | r := mux.NewRouter() 113 | s := r.Host("www.example.com").Subrouter() 114 | ``` 115 | 116 | Then register routes in the subrouter: 117 | 118 | ```go 119 | s.HandleFunc("/products/", ProductsHandler) 120 | s.HandleFunc("/products/{key}", ProductHandler) 121 | s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 122 | ``` 123 | 124 | The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. 125 | 126 | Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. 127 | 128 | There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: 129 | 130 | ```go 131 | r := mux.NewRouter() 132 | s := r.PathPrefix("/products").Subrouter() 133 | // "/products/" 134 | s.HandleFunc("/", ProductsHandler) 135 | // "/products/{key}/" 136 | s.HandleFunc("/{key}/", ProductHandler) 137 | // "/products/{key}/details" 138 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 139 | ``` 140 | 141 | Now let's see how to build registered URLs. 142 | 143 | Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: 144 | 145 | ```go 146 | r := mux.NewRouter() 147 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 148 | Name("article") 149 | ``` 150 | 151 | To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: 152 | 153 | ```go 154 | url, err := r.Get("article").URL("category", "technology", "id", "42") 155 | ``` 156 | 157 | ...and the result will be a `url.URL` with the following path: 158 | 159 | ``` 160 | "/articles/technology/42" 161 | ``` 162 | 163 | This also works for host variables: 164 | 165 | ```go 166 | r := mux.NewRouter() 167 | r.Host("{subdomain}.domain.com"). 168 | Path("/articles/{category}/{id:[0-9]+}"). 169 | HandlerFunc(ArticleHandler). 170 | Name("article") 171 | 172 | // url.String() will be "http://news.domain.com/articles/technology/42" 173 | url, err := r.Get("article").URL("subdomain", "news", 174 | "category", "technology", 175 | "id", "42") 176 | ``` 177 | 178 | All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. 179 | 180 | Regex support also exists for matching Headers within a route. For example, we could do: 181 | 182 | ```go 183 | r.HeadersRegexp("Content-Type", "application/(text|json)") 184 | ``` 185 | 186 | ...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` 187 | 188 | There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: 189 | 190 | ```go 191 | // "http://news.domain.com/" 192 | host, err := r.Get("article").URLHost("subdomain", "news") 193 | 194 | // "/articles/technology/42" 195 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 196 | ``` 197 | 198 | And if you use subrouters, host and path defined separately can be built as well: 199 | 200 | ```go 201 | r := mux.NewRouter() 202 | s := r.Host("{subdomain}.domain.com").Subrouter() 203 | s.Path("/articles/{category}/{id:[0-9]+}"). 204 | HandlerFunc(ArticleHandler). 205 | Name("article") 206 | 207 | // "http://news.domain.com/articles/technology/42" 208 | url, err := r.Get("article").URL("subdomain", "news", 209 | "category", "technology", 210 | "id", "42") 211 | ``` 212 | 213 | ## Full Example 214 | 215 | Here's a complete, runnable example of a small `mux` based server: 216 | 217 | ```go 218 | package main 219 | 220 | import ( 221 | "net/http" 222 | 223 | "github.com/gorilla/mux" 224 | ) 225 | 226 | func YourHandler(w http.ResponseWriter, r *http.Request) { 227 | w.Write([]byte("Gorilla!\n")) 228 | } 229 | 230 | func main() { 231 | r := mux.NewRouter() 232 | // Routes consist of a path and a handler function. 233 | r.HandleFunc("/", YourHandler) 234 | 235 | // Bind to a port and pass our router in 236 | http.ListenAndServe(":8000", r) 237 | } 238 | ``` 239 | 240 | ## License 241 | 242 | BSD licensed. See the LICENSE file for details. 243 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package mux implements a request router and dispatcher. 7 | 8 | The name mux stands for "HTTP request multiplexer". Like the standard 9 | http.ServeMux, mux.Router matches incoming requests against a list of 10 | registered routes and calls a handler for the route that matches the URL 11 | or other conditions. The main features are: 12 | 13 | * Requests can be matched based on URL host, path, path prefix, schemes, 14 | header and query values, HTTP methods or using custom matchers. 15 | * URL hosts and paths can have variables with an optional regular 16 | expression. 17 | * Registered URLs can be built, or "reversed", which helps maintaining 18 | references to resources. 19 | * Routes can be used as subrouters: nested routes are only tested if the 20 | parent route matches. This is useful to define groups of routes that 21 | share common conditions like a host, a path prefix or other repeated 22 | attributes. As a bonus, this optimizes request matching. 23 | * It implements the http.Handler interface so it is compatible with the 24 | standard http.ServeMux. 25 | 26 | Let's start registering a couple of URL paths and handlers: 27 | 28 | func main() { 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", HomeHandler) 31 | r.HandleFunc("/products", ProductsHandler) 32 | r.HandleFunc("/articles", ArticlesHandler) 33 | http.Handle("/", r) 34 | } 35 | 36 | Here we register three routes mapping URL paths to handlers. This is 37 | equivalent to how http.HandleFunc() works: if an incoming request URL matches 38 | one of the paths, the corresponding handler is called passing 39 | (http.ResponseWriter, *http.Request) as parameters. 40 | 41 | Paths can have variables. They are defined using the format {name} or 42 | {name:pattern}. If a regular expression pattern is not defined, the matched 43 | variable will be anything until the next slash. For example: 44 | 45 | r := mux.NewRouter() 46 | r.HandleFunc("/products/{key}", ProductHandler) 47 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 48 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 49 | 50 | The names are used to create a map of route variables which can be retrieved 51 | calling mux.Vars(): 52 | 53 | vars := mux.Vars(request) 54 | category := vars["category"] 55 | 56 | And this is all you need to know about the basic usage. More advanced options 57 | are explained below. 58 | 59 | Routes can also be restricted to a domain or subdomain. Just define a host 60 | pattern to be matched. They can also have variables: 61 | 62 | r := mux.NewRouter() 63 | // Only matches if domain is "www.example.com". 64 | r.Host("www.example.com") 65 | // Matches a dynamic subdomain. 66 | r.Host("{subdomain:[a-z]+}.domain.com") 67 | 68 | There are several other matchers that can be added. To match path prefixes: 69 | 70 | r.PathPrefix("/products/") 71 | 72 | ...or HTTP methods: 73 | 74 | r.Methods("GET", "POST") 75 | 76 | ...or URL schemes: 77 | 78 | r.Schemes("https") 79 | 80 | ...or header values: 81 | 82 | r.Headers("X-Requested-With", "XMLHttpRequest") 83 | 84 | ...or query values: 85 | 86 | r.Queries("key", "value") 87 | 88 | ...or to use a custom matcher function: 89 | 90 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 91 | return r.ProtoMajor == 0 92 | }) 93 | 94 | ...and finally, it is possible to combine several matchers in a single route: 95 | 96 | r.HandleFunc("/products", ProductsHandler). 97 | Host("www.example.com"). 98 | Methods("GET"). 99 | Schemes("http") 100 | 101 | Setting the same matching conditions again and again can be boring, so we have 102 | a way to group several routes that share the same requirements. 103 | We call it "subrouting". 104 | 105 | For example, let's say we have several URLs that should only match when the 106 | host is "www.example.com". Create a route for that host and get a "subrouter" 107 | from it: 108 | 109 | r := mux.NewRouter() 110 | s := r.Host("www.example.com").Subrouter() 111 | 112 | Then register routes in the subrouter: 113 | 114 | s.HandleFunc("/products/", ProductsHandler) 115 | s.HandleFunc("/products/{key}", ProductHandler) 116 | s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 117 | 118 | The three URL paths we registered above will only be tested if the domain is 119 | "www.example.com", because the subrouter is tested first. This is not 120 | only convenient, but also optimizes request matching. You can create 121 | subrouters combining any attribute matchers accepted by a route. 122 | 123 | Subrouters can be used to create domain or path "namespaces": you define 124 | subrouters in a central place and then parts of the app can register its 125 | paths relatively to a given subrouter. 126 | 127 | There's one more thing about subroutes. When a subrouter has a path prefix, 128 | the inner routes use it as base for their paths: 129 | 130 | r := mux.NewRouter() 131 | s := r.PathPrefix("/products").Subrouter() 132 | // "/products/" 133 | s.HandleFunc("/", ProductsHandler) 134 | // "/products/{key}/" 135 | s.HandleFunc("/{key}/", ProductHandler) 136 | // "/products/{key}/details" 137 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 138 | 139 | Now let's see how to build registered URLs. 140 | 141 | Routes can be named. All routes that define a name can have their URLs built, 142 | or "reversed". We define a name calling Name() on a route. For example: 143 | 144 | r := mux.NewRouter() 145 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 146 | Name("article") 147 | 148 | To build a URL, get the route and call the URL() method, passing a sequence of 149 | key/value pairs for the route variables. For the previous route, we would do: 150 | 151 | url, err := r.Get("article").URL("category", "technology", "id", "42") 152 | 153 | ...and the result will be a url.URL with the following path: 154 | 155 | "/articles/technology/42" 156 | 157 | This also works for host variables: 158 | 159 | r := mux.NewRouter() 160 | r.Host("{subdomain}.domain.com"). 161 | Path("/articles/{category}/{id:[0-9]+}"). 162 | HandlerFunc(ArticleHandler). 163 | Name("article") 164 | 165 | // url.String() will be "http://news.domain.com/articles/technology/42" 166 | url, err := r.Get("article").URL("subdomain", "news", 167 | "category", "technology", 168 | "id", "42") 169 | 170 | All variables defined in the route are required, and their values must 171 | conform to the corresponding patterns. These requirements guarantee that a 172 | generated URL will always match a registered route -- the only exception is 173 | for explicitly defined "build-only" routes which never match. 174 | 175 | Regex support also exists for matching Headers within a route. For example, we could do: 176 | 177 | r.HeadersRegexp("Content-Type", "application/(text|json)") 178 | 179 | ...and the route will match both requests with a Content-Type of `application/json` as well as 180 | `application/text` 181 | 182 | There's also a way to build only the URL host or path for a route: 183 | use the methods URLHost() or URLPath() instead. For the previous route, 184 | we would do: 185 | 186 | // "http://news.domain.com/" 187 | host, err := r.Get("article").URLHost("subdomain", "news") 188 | 189 | // "/articles/technology/42" 190 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 191 | 192 | And if you use subrouters, host and path defined separately can be built 193 | as well: 194 | 195 | r := mux.NewRouter() 196 | s := r.Host("{subdomain}.domain.com").Subrouter() 197 | s.Path("/articles/{category}/{id:[0-9]+}"). 198 | HandlerFunc(ArticleHandler). 199 | Name("article") 200 | 201 | // "http://news.domain.com/articles/technology/42" 202 | url, err := r.Get("article").URL("subdomain", "news", 203 | "category", "technology", 204 | "id", "42") 205 | */ 206 | package mux 207 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/mux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "path" 12 | "regexp" 13 | 14 | "github.com/gorilla/context" 15 | ) 16 | 17 | // NewRouter returns a new router instance. 18 | func NewRouter() *Router { 19 | return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} 20 | } 21 | 22 | // Router registers routes to be matched and dispatches a handler. 23 | // 24 | // It implements the http.Handler interface, so it can be registered to serve 25 | // requests: 26 | // 27 | // var router = mux.NewRouter() 28 | // 29 | // func main() { 30 | // http.Handle("/", router) 31 | // } 32 | // 33 | // Or, for Google App Engine, register it in a init() function: 34 | // 35 | // func init() { 36 | // http.Handle("/", router) 37 | // } 38 | // 39 | // This will send all incoming requests to the router. 40 | type Router struct { 41 | // Configurable Handler to be used when no route matches. 42 | NotFoundHandler http.Handler 43 | // Parent route, if this is a subrouter. 44 | parent parentRoute 45 | // Routes to be matched, in order. 46 | routes []*Route 47 | // Routes by name for URL building. 48 | namedRoutes map[string]*Route 49 | // See Router.StrictSlash(). This defines the flag for new routes. 50 | strictSlash bool 51 | // If true, do not clear the request context after handling the request 52 | KeepContext bool 53 | } 54 | 55 | // Match matches registered routes against the request. 56 | func (r *Router) Match(req *http.Request, match *RouteMatch) bool { 57 | for _, route := range r.routes { 58 | if route.Match(req, match) { 59 | return true 60 | } 61 | } 62 | 63 | // Closest match for a router (includes sub-routers) 64 | if r.NotFoundHandler != nil { 65 | match.Handler = r.NotFoundHandler 66 | return true 67 | } 68 | return false 69 | } 70 | 71 | // ServeHTTP dispatches the handler registered in the matched route. 72 | // 73 | // When there is a match, the route variables can be retrieved calling 74 | // mux.Vars(request). 75 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 76 | // Clean path to canonical form and redirect. 77 | if p := cleanPath(req.URL.Path); p != req.URL.Path { 78 | 79 | // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. 80 | // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: 81 | // http://code.google.com/p/go/issues/detail?id=5252 82 | url := *req.URL 83 | url.Path = p 84 | p = url.String() 85 | 86 | w.Header().Set("Location", p) 87 | w.WriteHeader(http.StatusMovedPermanently) 88 | return 89 | } 90 | var match RouteMatch 91 | var handler http.Handler 92 | if r.Match(req, &match) { 93 | handler = match.Handler 94 | setVars(req, match.Vars) 95 | setCurrentRoute(req, match.Route) 96 | } 97 | if handler == nil { 98 | handler = http.NotFoundHandler() 99 | } 100 | if !r.KeepContext { 101 | defer context.Clear(req) 102 | } 103 | handler.ServeHTTP(w, req) 104 | } 105 | 106 | // Get returns a route registered with the given name. 107 | func (r *Router) Get(name string) *Route { 108 | return r.getNamedRoutes()[name] 109 | } 110 | 111 | // GetRoute returns a route registered with the given name. This method 112 | // was renamed to Get() and remains here for backwards compatibility. 113 | func (r *Router) GetRoute(name string) *Route { 114 | return r.getNamedRoutes()[name] 115 | } 116 | 117 | // StrictSlash defines the trailing slash behavior for new routes. The initial 118 | // value is false. 119 | // 120 | // When true, if the route path is "/path/", accessing "/path" will redirect 121 | // to the former and vice versa. In other words, your application will always 122 | // see the path as specified in the route. 123 | // 124 | // When false, if the route path is "/path", accessing "/path/" will not match 125 | // this route and vice versa. 126 | // 127 | // Special case: when a route sets a path prefix using the PathPrefix() method, 128 | // strict slash is ignored for that route because the redirect behavior can't 129 | // be determined from a prefix alone. However, any subrouters created from that 130 | // route inherit the original StrictSlash setting. 131 | func (r *Router) StrictSlash(value bool) *Router { 132 | r.strictSlash = value 133 | return r 134 | } 135 | 136 | // ---------------------------------------------------------------------------- 137 | // parentRoute 138 | // ---------------------------------------------------------------------------- 139 | 140 | // getNamedRoutes returns the map where named routes are registered. 141 | func (r *Router) getNamedRoutes() map[string]*Route { 142 | if r.namedRoutes == nil { 143 | if r.parent != nil { 144 | r.namedRoutes = r.parent.getNamedRoutes() 145 | } else { 146 | r.namedRoutes = make(map[string]*Route) 147 | } 148 | } 149 | return r.namedRoutes 150 | } 151 | 152 | // getRegexpGroup returns regexp definitions from the parent route, if any. 153 | func (r *Router) getRegexpGroup() *routeRegexpGroup { 154 | if r.parent != nil { 155 | return r.parent.getRegexpGroup() 156 | } 157 | return nil 158 | } 159 | 160 | func (r *Router) buildVars(m map[string]string) map[string]string { 161 | if r.parent != nil { 162 | m = r.parent.buildVars(m) 163 | } 164 | return m 165 | } 166 | 167 | // ---------------------------------------------------------------------------- 168 | // Route factories 169 | // ---------------------------------------------------------------------------- 170 | 171 | // NewRoute registers an empty route. 172 | func (r *Router) NewRoute() *Route { 173 | route := &Route{parent: r, strictSlash: r.strictSlash} 174 | r.routes = append(r.routes, route) 175 | return route 176 | } 177 | 178 | // Handle registers a new route with a matcher for the URL path. 179 | // See Route.Path() and Route.Handler(). 180 | func (r *Router) Handle(path string, handler http.Handler) *Route { 181 | return r.NewRoute().Path(path).Handler(handler) 182 | } 183 | 184 | // HandleFunc registers a new route with a matcher for the URL path. 185 | // See Route.Path() and Route.HandlerFunc(). 186 | func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, 187 | *http.Request)) *Route { 188 | return r.NewRoute().Path(path).HandlerFunc(f) 189 | } 190 | 191 | // Headers registers a new route with a matcher for request header values. 192 | // See Route.Headers(). 193 | func (r *Router) Headers(pairs ...string) *Route { 194 | return r.NewRoute().Headers(pairs...) 195 | } 196 | 197 | // Host registers a new route with a matcher for the URL host. 198 | // See Route.Host(). 199 | func (r *Router) Host(tpl string) *Route { 200 | return r.NewRoute().Host(tpl) 201 | } 202 | 203 | // MatcherFunc registers a new route with a custom matcher function. 204 | // See Route.MatcherFunc(). 205 | func (r *Router) MatcherFunc(f MatcherFunc) *Route { 206 | return r.NewRoute().MatcherFunc(f) 207 | } 208 | 209 | // Methods registers a new route with a matcher for HTTP methods. 210 | // See Route.Methods(). 211 | func (r *Router) Methods(methods ...string) *Route { 212 | return r.NewRoute().Methods(methods...) 213 | } 214 | 215 | // Path registers a new route with a matcher for the URL path. 216 | // See Route.Path(). 217 | func (r *Router) Path(tpl string) *Route { 218 | return r.NewRoute().Path(tpl) 219 | } 220 | 221 | // PathPrefix registers a new route with a matcher for the URL path prefix. 222 | // See Route.PathPrefix(). 223 | func (r *Router) PathPrefix(tpl string) *Route { 224 | return r.NewRoute().PathPrefix(tpl) 225 | } 226 | 227 | // Queries registers a new route with a matcher for URL query values. 228 | // See Route.Queries(). 229 | func (r *Router) Queries(pairs ...string) *Route { 230 | return r.NewRoute().Queries(pairs...) 231 | } 232 | 233 | // Schemes registers a new route with a matcher for URL schemes. 234 | // See Route.Schemes(). 235 | func (r *Router) Schemes(schemes ...string) *Route { 236 | return r.NewRoute().Schemes(schemes...) 237 | } 238 | 239 | // BuildVarsFunc registers a new route with a custom function for modifying 240 | // route variables before building a URL. 241 | func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { 242 | return r.NewRoute().BuildVarsFunc(f) 243 | } 244 | 245 | // Walk walks the router and all its sub-routers, calling walkFn for each route 246 | // in the tree. The routes are walked in the order they were added. Sub-routers 247 | // are explored depth-first. 248 | func (r *Router) Walk(walkFn WalkFunc) error { 249 | return r.walk(walkFn, []*Route{}) 250 | } 251 | 252 | // SkipRouter is used as a return value from WalkFuncs to indicate that the 253 | // router that walk is about to descend down to should be skipped. 254 | var SkipRouter = errors.New("skip this router") 255 | 256 | // WalkFunc is the type of the function called for each route visited by Walk. 257 | // At every invocation, it is given the current route, and the current router, 258 | // and a list of ancestor routes that lead to the current route. 259 | type WalkFunc func(route *Route, router *Router, ancestors []*Route) error 260 | 261 | func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { 262 | for _, t := range r.routes { 263 | if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" { 264 | continue 265 | } 266 | 267 | err := walkFn(t, r, ancestors) 268 | if err == SkipRouter { 269 | continue 270 | } 271 | for _, sr := range t.matchers { 272 | if h, ok := sr.(*Router); ok { 273 | err := h.walk(walkFn, ancestors) 274 | if err != nil { 275 | return err 276 | } 277 | } 278 | } 279 | if h, ok := t.handler.(*Router); ok { 280 | ancestors = append(ancestors, t) 281 | err := h.walk(walkFn, ancestors) 282 | if err != nil { 283 | return err 284 | } 285 | ancestors = ancestors[:len(ancestors)-1] 286 | } 287 | } 288 | return nil 289 | } 290 | 291 | // ---------------------------------------------------------------------------- 292 | // Context 293 | // ---------------------------------------------------------------------------- 294 | 295 | // RouteMatch stores information about a matched route. 296 | type RouteMatch struct { 297 | Route *Route 298 | Handler http.Handler 299 | Vars map[string]string 300 | } 301 | 302 | type contextKey int 303 | 304 | const ( 305 | varsKey contextKey = iota 306 | routeKey 307 | ) 308 | 309 | // Vars returns the route variables for the current request, if any. 310 | func Vars(r *http.Request) map[string]string { 311 | if rv := context.Get(r, varsKey); rv != nil { 312 | return rv.(map[string]string) 313 | } 314 | return nil 315 | } 316 | 317 | // CurrentRoute returns the matched route for the current request, if any. 318 | // This only works when called inside the handler of the matched route 319 | // because the matched route is stored in the request context which is cleared 320 | // after the handler returns, unless the KeepContext option is set on the 321 | // Router. 322 | func CurrentRoute(r *http.Request) *Route { 323 | if rv := context.Get(r, routeKey); rv != nil { 324 | return rv.(*Route) 325 | } 326 | return nil 327 | } 328 | 329 | func setVars(r *http.Request, val interface{}) { 330 | if val != nil { 331 | context.Set(r, varsKey, val) 332 | } 333 | } 334 | 335 | func setCurrentRoute(r *http.Request, val interface{}) { 336 | if val != nil { 337 | context.Set(r, routeKey, val) 338 | } 339 | } 340 | 341 | // ---------------------------------------------------------------------------- 342 | // Helpers 343 | // ---------------------------------------------------------------------------- 344 | 345 | // cleanPath returns the canonical path for p, eliminating . and .. elements. 346 | // Borrowed from the net/http package. 347 | func cleanPath(p string) string { 348 | if p == "" { 349 | return "/" 350 | } 351 | if p[0] != '/' { 352 | p = "/" + p 353 | } 354 | np := path.Clean(p) 355 | // path.Clean removes trailing slash except for root; 356 | // put the trailing slash back if necessary. 357 | if p[len(p)-1] == '/' && np != "/" { 358 | np += "/" 359 | } 360 | return np 361 | } 362 | 363 | // uniqueVars returns an error if two slices contain duplicated strings. 364 | func uniqueVars(s1, s2 []string) error { 365 | for _, v1 := range s1 { 366 | for _, v2 := range s2 { 367 | if v1 == v2 { 368 | return fmt.Errorf("mux: duplicated route variable %q", v2) 369 | } 370 | } 371 | } 372 | return nil 373 | } 374 | 375 | // checkPairs returns the count of strings passed in, and an error if 376 | // the count is not an even number. 377 | func checkPairs(pairs ...string) (int, error) { 378 | length := len(pairs) 379 | if length%2 != 0 { 380 | return length, fmt.Errorf( 381 | "mux: number of parameters must be multiple of 2, got %v", pairs) 382 | } 383 | return length, nil 384 | } 385 | 386 | // mapFromPairsToString converts variadic string parameters to a 387 | // string to string map. 388 | func mapFromPairsToString(pairs ...string) (map[string]string, error) { 389 | length, err := checkPairs(pairs...) 390 | if err != nil { 391 | return nil, err 392 | } 393 | m := make(map[string]string, length/2) 394 | for i := 0; i < length; i += 2 { 395 | m[pairs[i]] = pairs[i+1] 396 | } 397 | return m, nil 398 | } 399 | 400 | // mapFromPairsToRegex converts variadic string paramers to a 401 | // string to regex map. 402 | func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { 403 | length, err := checkPairs(pairs...) 404 | if err != nil { 405 | return nil, err 406 | } 407 | m := make(map[string]*regexp.Regexp, length/2) 408 | for i := 0; i < length; i += 2 { 409 | regex, err := regexp.Compile(pairs[i+1]) 410 | if err != nil { 411 | return nil, err 412 | } 413 | m[pairs[i]] = regex 414 | } 415 | return m, nil 416 | } 417 | 418 | // matchInArray returns true if the given string value is in the array. 419 | func matchInArray(arr []string, value string) bool { 420 | for _, v := range arr { 421 | if v == value { 422 | return true 423 | } 424 | } 425 | return false 426 | } 427 | 428 | // matchMapWithString returns true if the given key/value pairs exist in a given map. 429 | func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { 430 | for k, v := range toCheck { 431 | // Check if key exists. 432 | if canonicalKey { 433 | k = http.CanonicalHeaderKey(k) 434 | } 435 | if values := toMatch[k]; values == nil { 436 | return false 437 | } else if v != "" { 438 | // If value was defined as an empty string we only check that the 439 | // key exists. Otherwise we also check for equality. 440 | valueExists := false 441 | for _, value := range values { 442 | if v == value { 443 | valueExists = true 444 | break 445 | } 446 | } 447 | if !valueExists { 448 | return false 449 | } 450 | } 451 | } 452 | return true 453 | } 454 | 455 | // matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against 456 | // the given regex 457 | func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { 458 | for k, v := range toCheck { 459 | // Check if key exists. 460 | if canonicalKey { 461 | k = http.CanonicalHeaderKey(k) 462 | } 463 | if values := toMatch[k]; values == nil { 464 | return false 465 | } else if v != nil { 466 | // If value was defined as an empty string we only check that the 467 | // key exists. Otherwise we also check for equality. 468 | valueExists := false 469 | for _, value := range values { 470 | if v.MatchString(value) { 471 | valueExists = true 472 | break 473 | } 474 | } 475 | if !valueExists { 476 | return false 477 | } 478 | } 479 | } 480 | return true 481 | } 482 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/regexp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | // newRouteRegexp parses a route template and returns a routeRegexp, 18 | // used to match a host, a path or a query string. 19 | // 20 | // It will extract named variables, assemble a regexp to be matched, create 21 | // a "reverse" template to build URLs and compile regexps to validate variable 22 | // values used in URL building. 23 | // 24 | // Previously we accepted only Python-like identifiers for variable 25 | // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that 26 | // name and pattern can't be empty, and names can't contain a colon. 27 | func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) { 28 | // Check if it is well-formed. 29 | idxs, errBraces := braceIndices(tpl) 30 | if errBraces != nil { 31 | return nil, errBraces 32 | } 33 | // Backup the original. 34 | template := tpl 35 | // Now let's parse it. 36 | defaultPattern := "[^/]+" 37 | if matchQuery { 38 | defaultPattern = "[^?&]*" 39 | } else if matchHost { 40 | defaultPattern = "[^.]+" 41 | matchPrefix = false 42 | } 43 | // Only match strict slash if not matching 44 | if matchPrefix || matchHost || matchQuery { 45 | strictSlash = false 46 | } 47 | // Set a flag for strictSlash. 48 | endSlash := false 49 | if strictSlash && strings.HasSuffix(tpl, "/") { 50 | tpl = tpl[:len(tpl)-1] 51 | endSlash = true 52 | } 53 | varsN := make([]string, len(idxs)/2) 54 | varsR := make([]*regexp.Regexp, len(idxs)/2) 55 | pattern := bytes.NewBufferString("") 56 | pattern.WriteByte('^') 57 | reverse := bytes.NewBufferString("") 58 | var end int 59 | var err error 60 | for i := 0; i < len(idxs); i += 2 { 61 | // Set all values we are interested in. 62 | raw := tpl[end:idxs[i]] 63 | end = idxs[i+1] 64 | parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) 65 | name := parts[0] 66 | patt := defaultPattern 67 | if len(parts) == 2 { 68 | patt = parts[1] 69 | } 70 | // Name or pattern can't be empty. 71 | if name == "" || patt == "" { 72 | return nil, fmt.Errorf("mux: missing name or pattern in %q", 73 | tpl[idxs[i]:end]) 74 | } 75 | // Build the regexp pattern. 76 | fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) 77 | 78 | // Build the reverse template. 79 | fmt.Fprintf(reverse, "%s%%s", raw) 80 | 81 | // Append variable name and compiled pattern. 82 | varsN[i/2] = name 83 | varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | } 88 | // Add the remaining. 89 | raw := tpl[end:] 90 | pattern.WriteString(regexp.QuoteMeta(raw)) 91 | if strictSlash { 92 | pattern.WriteString("[/]?") 93 | } 94 | if matchQuery { 95 | // Add the default pattern if the query value is empty 96 | if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { 97 | pattern.WriteString(defaultPattern) 98 | } 99 | } 100 | if !matchPrefix { 101 | pattern.WriteByte('$') 102 | } 103 | reverse.WriteString(raw) 104 | if endSlash { 105 | reverse.WriteByte('/') 106 | } 107 | // Compile full regexp. 108 | reg, errCompile := regexp.Compile(pattern.String()) 109 | if errCompile != nil { 110 | return nil, errCompile 111 | } 112 | // Done! 113 | return &routeRegexp{ 114 | template: template, 115 | matchHost: matchHost, 116 | matchQuery: matchQuery, 117 | strictSlash: strictSlash, 118 | regexp: reg, 119 | reverse: reverse.String(), 120 | varsN: varsN, 121 | varsR: varsR, 122 | }, nil 123 | } 124 | 125 | // routeRegexp stores a regexp to match a host or path and information to 126 | // collect and validate route variables. 127 | type routeRegexp struct { 128 | // The unmodified template. 129 | template string 130 | // True for host match, false for path or query string match. 131 | matchHost bool 132 | // True for query string match, false for path and host match. 133 | matchQuery bool 134 | // The strictSlash value defined on the route, but disabled if PathPrefix was used. 135 | strictSlash bool 136 | // Expanded regexp. 137 | regexp *regexp.Regexp 138 | // Reverse template. 139 | reverse string 140 | // Variable names. 141 | varsN []string 142 | // Variable regexps (validators). 143 | varsR []*regexp.Regexp 144 | } 145 | 146 | // Match matches the regexp against the URL host or path. 147 | func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { 148 | if !r.matchHost { 149 | if r.matchQuery { 150 | return r.matchQueryString(req) 151 | } 152 | 153 | return r.regexp.MatchString(req.URL.Path) 154 | } 155 | 156 | return r.regexp.MatchString(getHost(req)) 157 | } 158 | 159 | // url builds a URL part using the given values. 160 | func (r *routeRegexp) url(values map[string]string) (string, error) { 161 | urlValues := make([]interface{}, len(r.varsN)) 162 | for k, v := range r.varsN { 163 | value, ok := values[v] 164 | if !ok { 165 | return "", fmt.Errorf("mux: missing route variable %q", v) 166 | } 167 | urlValues[k] = value 168 | } 169 | rv := fmt.Sprintf(r.reverse, urlValues...) 170 | if !r.regexp.MatchString(rv) { 171 | // The URL is checked against the full regexp, instead of checking 172 | // individual variables. This is faster but to provide a good error 173 | // message, we check individual regexps if the URL doesn't match. 174 | for k, v := range r.varsN { 175 | if !r.varsR[k].MatchString(values[v]) { 176 | return "", fmt.Errorf( 177 | "mux: variable %q doesn't match, expected %q", values[v], 178 | r.varsR[k].String()) 179 | } 180 | } 181 | } 182 | return rv, nil 183 | } 184 | 185 | // getURLQuery returns a single query parameter from a request URL. 186 | // For a URL with foo=bar&baz=ding, we return only the relevant key 187 | // value pair for the routeRegexp. 188 | func (r *routeRegexp) getURLQuery(req *http.Request) string { 189 | if !r.matchQuery { 190 | return "" 191 | } 192 | templateKey := strings.SplitN(r.template, "=", 2)[0] 193 | for key, vals := range req.URL.Query() { 194 | if key == templateKey && len(vals) > 0 { 195 | return key + "=" + vals[0] 196 | } 197 | } 198 | return "" 199 | } 200 | 201 | func (r *routeRegexp) matchQueryString(req *http.Request) bool { 202 | return r.regexp.MatchString(r.getURLQuery(req)) 203 | } 204 | 205 | // braceIndices returns the first level curly brace indices from a string. 206 | // It returns an error in case of unbalanced braces. 207 | func braceIndices(s string) ([]int, error) { 208 | var level, idx int 209 | var idxs []int 210 | for i := 0; i < len(s); i++ { 211 | switch s[i] { 212 | case '{': 213 | if level++; level == 1 { 214 | idx = i 215 | } 216 | case '}': 217 | if level--; level == 0 { 218 | idxs = append(idxs, idx, i+1) 219 | } else if level < 0 { 220 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 221 | } 222 | } 223 | } 224 | if level != 0 { 225 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 226 | } 227 | return idxs, nil 228 | } 229 | 230 | // varGroupName builds a capturing group name for the indexed variable. 231 | func varGroupName(idx int) string { 232 | return "v" + strconv.Itoa(idx) 233 | } 234 | 235 | // ---------------------------------------------------------------------------- 236 | // routeRegexpGroup 237 | // ---------------------------------------------------------------------------- 238 | 239 | // routeRegexpGroup groups the route matchers that carry variables. 240 | type routeRegexpGroup struct { 241 | host *routeRegexp 242 | path *routeRegexp 243 | queries []*routeRegexp 244 | } 245 | 246 | // setMatch extracts the variables from the URL once a route matches. 247 | func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { 248 | // Store host variables. 249 | if v.host != nil { 250 | host := getHost(req) 251 | matches := v.host.regexp.FindStringSubmatchIndex(host) 252 | if len(matches) > 0 { 253 | extractVars(host, matches, v.host.varsN, m.Vars) 254 | } 255 | } 256 | // Store path variables. 257 | if v.path != nil { 258 | matches := v.path.regexp.FindStringSubmatchIndex(req.URL.Path) 259 | if len(matches) > 0 { 260 | extractVars(req.URL.Path, matches, v.path.varsN, m.Vars) 261 | // Check if we should redirect. 262 | if v.path.strictSlash { 263 | p1 := strings.HasSuffix(req.URL.Path, "/") 264 | p2 := strings.HasSuffix(v.path.template, "/") 265 | if p1 != p2 { 266 | u, _ := url.Parse(req.URL.String()) 267 | if p1 { 268 | u.Path = u.Path[:len(u.Path)-1] 269 | } else { 270 | u.Path += "/" 271 | } 272 | m.Handler = http.RedirectHandler(u.String(), 301) 273 | } 274 | } 275 | } 276 | } 277 | // Store query string variables. 278 | for _, q := range v.queries { 279 | queryURL := q.getURLQuery(req) 280 | matches := q.regexp.FindStringSubmatchIndex(queryURL) 281 | if len(matches) > 0 { 282 | extractVars(queryURL, matches, q.varsN, m.Vars) 283 | } 284 | } 285 | } 286 | 287 | // getHost tries its best to return the request host. 288 | func getHost(r *http.Request) string { 289 | if r.URL.IsAbs() { 290 | return r.URL.Host 291 | } 292 | host := r.Host 293 | // Slice off any port information. 294 | if i := strings.Index(host, ":"); i != -1 { 295 | host = host[:i] 296 | } 297 | return host 298 | 299 | } 300 | 301 | func extractVars(input string, matches []int, names []string, output map[string]string) { 302 | matchesCount := 0 303 | prevEnd := -1 304 | for i := 2; i < len(matches) && matchesCount < len(names); i += 2 { 305 | if prevEnd < matches[i+1] { 306 | value := input[matches[i]:matches[i+1]] 307 | output[names[matchesCount]] = value 308 | prevEnd = matches[i+1] 309 | matchesCount++ 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /likes/vendor/github.com/gorilla/mux/route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // Route stores information to match a request and build URLs. 17 | type Route struct { 18 | // Parent where the route was registered (a Router). 19 | parent parentRoute 20 | // Request handler for the route. 21 | handler http.Handler 22 | // List of matchers. 23 | matchers []matcher 24 | // Manager for the variables from host and path. 25 | regexp *routeRegexpGroup 26 | // If true, when the path pattern is "/path/", accessing "/path" will 27 | // redirect to the former and vice versa. 28 | strictSlash bool 29 | // If true, this route never matches: it is only used to build URLs. 30 | buildOnly bool 31 | // The name used to build URLs. 32 | name string 33 | // Error resulted from building a route. 34 | err error 35 | 36 | buildVarsFunc BuildVarsFunc 37 | } 38 | 39 | // Match matches the route against the request. 40 | func (r *Route) Match(req *http.Request, match *RouteMatch) bool { 41 | if r.buildOnly || r.err != nil { 42 | return false 43 | } 44 | // Match everything. 45 | for _, m := range r.matchers { 46 | if matched := m.Match(req, match); !matched { 47 | return false 48 | } 49 | } 50 | // Yay, we have a match. Let's collect some info about it. 51 | if match.Route == nil { 52 | match.Route = r 53 | } 54 | if match.Handler == nil { 55 | match.Handler = r.handler 56 | } 57 | if match.Vars == nil { 58 | match.Vars = make(map[string]string) 59 | } 60 | // Set variables. 61 | if r.regexp != nil { 62 | r.regexp.setMatch(req, match, r) 63 | } 64 | return true 65 | } 66 | 67 | // ---------------------------------------------------------------------------- 68 | // Route attributes 69 | // ---------------------------------------------------------------------------- 70 | 71 | // GetError returns an error resulted from building the route, if any. 72 | func (r *Route) GetError() error { 73 | return r.err 74 | } 75 | 76 | // BuildOnly sets the route to never match: it is only used to build URLs. 77 | func (r *Route) BuildOnly() *Route { 78 | r.buildOnly = true 79 | return r 80 | } 81 | 82 | // Handler -------------------------------------------------------------------- 83 | 84 | // Handler sets a handler for the route. 85 | func (r *Route) Handler(handler http.Handler) *Route { 86 | if r.err == nil { 87 | r.handler = handler 88 | } 89 | return r 90 | } 91 | 92 | // HandlerFunc sets a handler function for the route. 93 | func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { 94 | return r.Handler(http.HandlerFunc(f)) 95 | } 96 | 97 | // GetHandler returns the handler for the route, if any. 98 | func (r *Route) GetHandler() http.Handler { 99 | return r.handler 100 | } 101 | 102 | // Name ----------------------------------------------------------------------- 103 | 104 | // Name sets the name for the route, used to build URLs. 105 | // If the name was registered already it will be overwritten. 106 | func (r *Route) Name(name string) *Route { 107 | if r.name != "" { 108 | r.err = fmt.Errorf("mux: route already has name %q, can't set %q", 109 | r.name, name) 110 | } 111 | if r.err == nil { 112 | r.name = name 113 | r.getNamedRoutes()[name] = r 114 | } 115 | return r 116 | } 117 | 118 | // GetName returns the name for the route, if any. 119 | func (r *Route) GetName() string { 120 | return r.name 121 | } 122 | 123 | // ---------------------------------------------------------------------------- 124 | // Matchers 125 | // ---------------------------------------------------------------------------- 126 | 127 | // matcher types try to match a request. 128 | type matcher interface { 129 | Match(*http.Request, *RouteMatch) bool 130 | } 131 | 132 | // addMatcher adds a matcher to the route. 133 | func (r *Route) addMatcher(m matcher) *Route { 134 | if r.err == nil { 135 | r.matchers = append(r.matchers, m) 136 | } 137 | return r 138 | } 139 | 140 | // addRegexpMatcher adds a host or path matcher and builder to a route. 141 | func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { 142 | if r.err != nil { 143 | return r.err 144 | } 145 | r.regexp = r.getRegexpGroup() 146 | if !matchHost && !matchQuery { 147 | if len(tpl) == 0 || tpl[0] != '/' { 148 | return fmt.Errorf("mux: path must start with a slash, got %q", tpl) 149 | } 150 | if r.regexp.path != nil { 151 | tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl 152 | } 153 | } 154 | rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash) 155 | if err != nil { 156 | return err 157 | } 158 | for _, q := range r.regexp.queries { 159 | if err = uniqueVars(rr.varsN, q.varsN); err != nil { 160 | return err 161 | } 162 | } 163 | if matchHost { 164 | if r.regexp.path != nil { 165 | if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { 166 | return err 167 | } 168 | } 169 | r.regexp.host = rr 170 | } else { 171 | if r.regexp.host != nil { 172 | if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { 173 | return err 174 | } 175 | } 176 | if matchQuery { 177 | r.regexp.queries = append(r.regexp.queries, rr) 178 | } else { 179 | r.regexp.path = rr 180 | } 181 | } 182 | r.addMatcher(rr) 183 | return nil 184 | } 185 | 186 | // Headers -------------------------------------------------------------------- 187 | 188 | // headerMatcher matches the request against header values. 189 | type headerMatcher map[string]string 190 | 191 | func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { 192 | return matchMapWithString(m, r.Header, true) 193 | } 194 | 195 | // Headers adds a matcher for request header values. 196 | // It accepts a sequence of key/value pairs to be matched. For example: 197 | // 198 | // r := mux.NewRouter() 199 | // r.Headers("Content-Type", "application/json", 200 | // "X-Requested-With", "XMLHttpRequest") 201 | // 202 | // The above route will only match if both request header values match. 203 | // If the value is an empty string, it will match any value if the key is set. 204 | func (r *Route) Headers(pairs ...string) *Route { 205 | if r.err == nil { 206 | var headers map[string]string 207 | headers, r.err = mapFromPairsToString(pairs...) 208 | return r.addMatcher(headerMatcher(headers)) 209 | } 210 | return r 211 | } 212 | 213 | // headerRegexMatcher matches the request against the route given a regex for the header 214 | type headerRegexMatcher map[string]*regexp.Regexp 215 | 216 | func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { 217 | return matchMapWithRegex(m, r.Header, true) 218 | } 219 | 220 | // HeadersRegexp accepts a sequence of key/value pairs, where the value has regex 221 | // support. For example: 222 | // 223 | // r := mux.NewRouter() 224 | // r.HeadersRegexp("Content-Type", "application/(text|json)", 225 | // "X-Requested-With", "XMLHttpRequest") 226 | // 227 | // The above route will only match if both the request header matches both regular expressions. 228 | // It the value is an empty string, it will match any value if the key is set. 229 | func (r *Route) HeadersRegexp(pairs ...string) *Route { 230 | if r.err == nil { 231 | var headers map[string]*regexp.Regexp 232 | headers, r.err = mapFromPairsToRegex(pairs...) 233 | return r.addMatcher(headerRegexMatcher(headers)) 234 | } 235 | return r 236 | } 237 | 238 | // Host ----------------------------------------------------------------------- 239 | 240 | // Host adds a matcher for the URL host. 241 | // It accepts a template with zero or more URL variables enclosed by {}. 242 | // Variables can define an optional regexp pattern to be matched: 243 | // 244 | // - {name} matches anything until the next dot. 245 | // 246 | // - {name:pattern} matches the given regexp pattern. 247 | // 248 | // For example: 249 | // 250 | // r := mux.NewRouter() 251 | // r.Host("www.example.com") 252 | // r.Host("{subdomain}.domain.com") 253 | // r.Host("{subdomain:[a-z]+}.domain.com") 254 | // 255 | // Variable names must be unique in a given route. They can be retrieved 256 | // calling mux.Vars(request). 257 | func (r *Route) Host(tpl string) *Route { 258 | r.err = r.addRegexpMatcher(tpl, true, false, false) 259 | return r 260 | } 261 | 262 | // MatcherFunc ---------------------------------------------------------------- 263 | 264 | // MatcherFunc is the function signature used by custom matchers. 265 | type MatcherFunc func(*http.Request, *RouteMatch) bool 266 | 267 | // Match returns the match for a given request. 268 | func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { 269 | return m(r, match) 270 | } 271 | 272 | // MatcherFunc adds a custom function to be used as request matcher. 273 | func (r *Route) MatcherFunc(f MatcherFunc) *Route { 274 | return r.addMatcher(f) 275 | } 276 | 277 | // Methods -------------------------------------------------------------------- 278 | 279 | // methodMatcher matches the request against HTTP methods. 280 | type methodMatcher []string 281 | 282 | func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { 283 | return matchInArray(m, r.Method) 284 | } 285 | 286 | // Methods adds a matcher for HTTP methods. 287 | // It accepts a sequence of one or more methods to be matched, e.g.: 288 | // "GET", "POST", "PUT". 289 | func (r *Route) Methods(methods ...string) *Route { 290 | for k, v := range methods { 291 | methods[k] = strings.ToUpper(v) 292 | } 293 | return r.addMatcher(methodMatcher(methods)) 294 | } 295 | 296 | // Path ----------------------------------------------------------------------- 297 | 298 | // Path adds a matcher for the URL path. 299 | // It accepts a template with zero or more URL variables enclosed by {}. The 300 | // template must start with a "/". 301 | // Variables can define an optional regexp pattern to be matched: 302 | // 303 | // - {name} matches anything until the next slash. 304 | // 305 | // - {name:pattern} matches the given regexp pattern. 306 | // 307 | // For example: 308 | // 309 | // r := mux.NewRouter() 310 | // r.Path("/products/").Handler(ProductsHandler) 311 | // r.Path("/products/{key}").Handler(ProductsHandler) 312 | // r.Path("/articles/{category}/{id:[0-9]+}"). 313 | // Handler(ArticleHandler) 314 | // 315 | // Variable names must be unique in a given route. They can be retrieved 316 | // calling mux.Vars(request). 317 | func (r *Route) Path(tpl string) *Route { 318 | r.err = r.addRegexpMatcher(tpl, false, false, false) 319 | return r 320 | } 321 | 322 | // PathPrefix ----------------------------------------------------------------- 323 | 324 | // PathPrefix adds a matcher for the URL path prefix. This matches if the given 325 | // template is a prefix of the full URL path. See Route.Path() for details on 326 | // the tpl argument. 327 | // 328 | // Note that it does not treat slashes specially ("/foobar/" will be matched by 329 | // the prefix "/foo") so you may want to use a trailing slash here. 330 | // 331 | // Also note that the setting of Router.StrictSlash() has no effect on routes 332 | // with a PathPrefix matcher. 333 | func (r *Route) PathPrefix(tpl string) *Route { 334 | r.err = r.addRegexpMatcher(tpl, false, true, false) 335 | return r 336 | } 337 | 338 | // Query ---------------------------------------------------------------------- 339 | 340 | // Queries adds a matcher for URL query values. 341 | // It accepts a sequence of key/value pairs. Values may define variables. 342 | // For example: 343 | // 344 | // r := mux.NewRouter() 345 | // r.Queries("foo", "bar", "id", "{id:[0-9]+}") 346 | // 347 | // The above route will only match if the URL contains the defined queries 348 | // values, e.g.: ?foo=bar&id=42. 349 | // 350 | // It the value is an empty string, it will match any value if the key is set. 351 | // 352 | // Variables can define an optional regexp pattern to be matched: 353 | // 354 | // - {name} matches anything until the next slash. 355 | // 356 | // - {name:pattern} matches the given regexp pattern. 357 | func (r *Route) Queries(pairs ...string) *Route { 358 | length := len(pairs) 359 | if length%2 != 0 { 360 | r.err = fmt.Errorf( 361 | "mux: number of parameters must be multiple of 2, got %v", pairs) 362 | return nil 363 | } 364 | for i := 0; i < length; i += 2 { 365 | if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { 366 | return r 367 | } 368 | } 369 | 370 | return r 371 | } 372 | 373 | // Schemes -------------------------------------------------------------------- 374 | 375 | // schemeMatcher matches the request against URL schemes. 376 | type schemeMatcher []string 377 | 378 | func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { 379 | return matchInArray(m, r.URL.Scheme) 380 | } 381 | 382 | // Schemes adds a matcher for URL schemes. 383 | // It accepts a sequence of schemes to be matched, e.g.: "http", "https". 384 | func (r *Route) Schemes(schemes ...string) *Route { 385 | for k, v := range schemes { 386 | schemes[k] = strings.ToLower(v) 387 | } 388 | return r.addMatcher(schemeMatcher(schemes)) 389 | } 390 | 391 | // BuildVarsFunc -------------------------------------------------------------- 392 | 393 | // BuildVarsFunc is the function signature used by custom build variable 394 | // functions (which can modify route variables before a route's URL is built). 395 | type BuildVarsFunc func(map[string]string) map[string]string 396 | 397 | // BuildVarsFunc adds a custom function to be used to modify build variables 398 | // before a route's URL is built. 399 | func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { 400 | r.buildVarsFunc = f 401 | return r 402 | } 403 | 404 | // Subrouter ------------------------------------------------------------------ 405 | 406 | // Subrouter creates a subrouter for the route. 407 | // 408 | // It will test the inner routes only if the parent route matched. For example: 409 | // 410 | // r := mux.NewRouter() 411 | // s := r.Host("www.example.com").Subrouter() 412 | // s.HandleFunc("/products/", ProductsHandler) 413 | // s.HandleFunc("/products/{key}", ProductHandler) 414 | // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 415 | // 416 | // Here, the routes registered in the subrouter won't be tested if the host 417 | // doesn't match. 418 | func (r *Route) Subrouter() *Router { 419 | router := &Router{parent: r, strictSlash: r.strictSlash} 420 | r.addMatcher(router) 421 | return router 422 | } 423 | 424 | // ---------------------------------------------------------------------------- 425 | // URL building 426 | // ---------------------------------------------------------------------------- 427 | 428 | // URL builds a URL for the route. 429 | // 430 | // It accepts a sequence of key/value pairs for the route variables. For 431 | // example, given this route: 432 | // 433 | // r := mux.NewRouter() 434 | // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 435 | // Name("article") 436 | // 437 | // ...a URL for it can be built using: 438 | // 439 | // url, err := r.Get("article").URL("category", "technology", "id", "42") 440 | // 441 | // ...which will return an url.URL with the following path: 442 | // 443 | // "/articles/technology/42" 444 | // 445 | // This also works for host variables: 446 | // 447 | // r := mux.NewRouter() 448 | // r.Host("{subdomain}.domain.com"). 449 | // HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 450 | // Name("article") 451 | // 452 | // // url.String() will be "http://news.domain.com/articles/technology/42" 453 | // url, err := r.Get("article").URL("subdomain", "news", 454 | // "category", "technology", 455 | // "id", "42") 456 | // 457 | // All variables defined in the route are required, and their values must 458 | // conform to the corresponding patterns. 459 | func (r *Route) URL(pairs ...string) (*url.URL, error) { 460 | if r.err != nil { 461 | return nil, r.err 462 | } 463 | if r.regexp == nil { 464 | return nil, errors.New("mux: route doesn't have a host or path") 465 | } 466 | values, err := r.prepareVars(pairs...) 467 | if err != nil { 468 | return nil, err 469 | } 470 | var scheme, host, path string 471 | if r.regexp.host != nil { 472 | // Set a default scheme. 473 | scheme = "http" 474 | if host, err = r.regexp.host.url(values); err != nil { 475 | return nil, err 476 | } 477 | } 478 | if r.regexp.path != nil { 479 | if path, err = r.regexp.path.url(values); err != nil { 480 | return nil, err 481 | } 482 | } 483 | return &url.URL{ 484 | Scheme: scheme, 485 | Host: host, 486 | Path: path, 487 | }, nil 488 | } 489 | 490 | // URLHost builds the host part of the URL for a route. See Route.URL(). 491 | // 492 | // The route must have a host defined. 493 | func (r *Route) URLHost(pairs ...string) (*url.URL, error) { 494 | if r.err != nil { 495 | return nil, r.err 496 | } 497 | if r.regexp == nil || r.regexp.host == nil { 498 | return nil, errors.New("mux: route doesn't have a host") 499 | } 500 | values, err := r.prepareVars(pairs...) 501 | if err != nil { 502 | return nil, err 503 | } 504 | host, err := r.regexp.host.url(values) 505 | if err != nil { 506 | return nil, err 507 | } 508 | return &url.URL{ 509 | Scheme: "http", 510 | Host: host, 511 | }, nil 512 | } 513 | 514 | // URLPath builds the path part of the URL for a route. See Route.URL(). 515 | // 516 | // The route must have a path defined. 517 | func (r *Route) URLPath(pairs ...string) (*url.URL, error) { 518 | if r.err != nil { 519 | return nil, r.err 520 | } 521 | if r.regexp == nil || r.regexp.path == nil { 522 | return nil, errors.New("mux: route doesn't have a path") 523 | } 524 | values, err := r.prepareVars(pairs...) 525 | if err != nil { 526 | return nil, err 527 | } 528 | path, err := r.regexp.path.url(values) 529 | if err != nil { 530 | return nil, err 531 | } 532 | return &url.URL{ 533 | Path: path, 534 | }, nil 535 | } 536 | 537 | // GetPathTemplate returns the template used to build the 538 | // route match. 539 | // This is useful for building simple REST API documentation and for instrumentation 540 | // against third-party services. 541 | // An error will be returned if the route does not define a path. 542 | func (r *Route) GetPathTemplate() (string, error) { 543 | if r.err != nil { 544 | return "", r.err 545 | } 546 | if r.regexp == nil || r.regexp.path == nil { 547 | return "", errors.New("mux: route doesn't have a path") 548 | } 549 | return r.regexp.path.template, nil 550 | } 551 | 552 | // GetHostTemplate returns the template used to build the 553 | // route match. 554 | // This is useful for building simple REST API documentation and for instrumentation 555 | // against third-party services. 556 | // An error will be returned if the route does not define a host. 557 | func (r *Route) GetHostTemplate() (string, error) { 558 | if r.err != nil { 559 | return "", r.err 560 | } 561 | if r.regexp == nil || r.regexp.host == nil { 562 | return "", errors.New("mux: route doesn't have a host") 563 | } 564 | return r.regexp.host.template, nil 565 | } 566 | 567 | // prepareVars converts the route variable pairs into a map. If the route has a 568 | // BuildVarsFunc, it is invoked. 569 | func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { 570 | m, err := mapFromPairsToString(pairs...) 571 | if err != nil { 572 | return nil, err 573 | } 574 | return r.buildVars(m), nil 575 | } 576 | 577 | func (r *Route) buildVars(m map[string]string) map[string]string { 578 | if r.parent != nil { 579 | m = r.parent.buildVars(m) 580 | } 581 | if r.buildVarsFunc != nil { 582 | m = r.buildVarsFunc(m) 583 | } 584 | return m 585 | } 586 | 587 | // ---------------------------------------------------------------------------- 588 | // parentRoute 589 | // ---------------------------------------------------------------------------- 590 | 591 | // parentRoute allows routes to know about parent host and path definitions. 592 | type parentRoute interface { 593 | getNamedRoutes() map[string]*Route 594 | getRegexpGroup() *routeRegexpGroup 595 | buildVars(map[string]string) map[string]string 596 | } 597 | 598 | // getNamedRoutes returns the map where named routes are registered. 599 | func (r *Route) getNamedRoutes() map[string]*Route { 600 | if r.parent == nil { 601 | // During tests router is not always set. 602 | r.parent = NewRouter() 603 | } 604 | return r.parent.getNamedRoutes() 605 | } 606 | 607 | // getRegexpGroup returns regexp definitions from this route. 608 | func (r *Route) getRegexpGroup() *routeRegexpGroup { 609 | if r.regexp == nil { 610 | if r.parent == nil { 611 | // During tests router is not always set. 612 | r.parent = NewRouter() 613 | } 614 | regexp := r.parent.getRegexpGroup() 615 | if regexp == nil { 616 | r.regexp = new(routeRegexpGroup) 617 | } else { 618 | // Copy. 619 | r.regexp = &routeRegexpGroup{ 620 | host: regexp.host, 621 | path: regexp.path, 622 | queries: regexp.queries, 623 | } 624 | } 625 | } 626 | return r.regexp 627 | } 628 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | docker-compose build 6 | docker-compose push 7 | docker save -o images.tar \ 8 | dockerdemos/lab-web \ 9 | dockerdemos/lab-words-dispatcher \ 10 | dockerdemos/lab-words-java \ 11 | mongo-express:0.31.0 \ 12 | mongo:3.3.15 13 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.11.4-alpine 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | COPY default.conf /etc/nginx/conf.d/default.conf 5 | COPY static /usr/share/nginx/html 6 | -------------------------------------------------------------------------------- /web/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | } 9 | 10 | location ~ /words/(.*) { 11 | resolver 127.0.0.11 ipv6=off valid=1s; 12 | set $upstream words; 13 | 14 | proxy_pass http://$upstream:8080/$1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log stderr debug; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /dev/stdout; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | include /etc/nginx/conf.d/*.conf; 25 | } -------------------------------------------------------------------------------- /web/static/IndieFlower.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/lab-docker/7a8e12822aadc28aa91e3c4c59e6cb044f2229d9/web/static/IndieFlower.ttf -------------------------------------------------------------------------------- /web/static/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var lab = angular.module('lab', []); 4 | 5 | lab.controller('LabCtrl', function ($scope, $http) { 6 | $scope.noun1 = ""; 7 | $scope.noun2 = ""; 8 | $scope.adjective1 = ""; 9 | $scope.adjective2 = ""; 10 | $scope.verb = ""; 11 | 12 | $http.get('/words/noun1').success(function(data) { 13 | $scope.noun1 = data; 14 | }); 15 | $http.get('/words/noun2').success(function(data) { 16 | data.word = data.word + "." 17 | $scope.noun2 = data; 18 | }); 19 | $http.get('/words/adjective1').success(function(data) { 20 | data.word = data.word.charAt(0).toUpperCase() + data.word.substr(1) 21 | $scope.adjective1 = data; 22 | }); 23 | $http.get('/words/adjective2').success(function(data) { 24 | $scope.adjective2 = data; 25 | }); 26 | $http.get('/words/verb').success(function(data) { 27 | $scope.verb = data; 28 | }); 29 | 30 | $scope.share = function() { 31 | var sentence = $scope.noun1 + " " + $scope.adjective1 + " " + $scope.verb + " " + $scope.noun2 + " " + $scope.adjective2 32 | $http.post('http://docker.local:9000/likes',{'name':sentence}) 33 | }; 34 | }); 35 | -------------------------------------------------------------------------------- /web/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Docker Lab - Cadavres exquis 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /web/static/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/lab-docker/7a8e12822aadc28aa91e3c4c59e6cb044f2229d9/web/static/share.png -------------------------------------------------------------------------------- /web/static/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Indie Flower'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Indie Flower'), local('IndieFlower'), url('IndieFlower.ttf') format('woff2'); 6 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 7 | } 8 | 9 | body { 10 | text-align: center; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | .sentence { 16 | margin: 100px auto 0 auto; 17 | } 18 | 19 | .result { 20 | display: inline-block; 21 | position: relative; 22 | display: inline-block; 23 | border: 2px solid; 24 | padding: 10px 20px 30px 20px; 25 | margin: 0 5px 10px 5px; 26 | color: white; 27 | box-shadow: -1px 2px 5px 1px rgba(0, 0, 0, 0.3); 28 | height: 42px; 29 | min-width: 30px; 30 | } 31 | 32 | .result .word { 33 | font-family: 'Indie Flower', cursive; 34 | font-size: 32px; 35 | color: white; 36 | } 37 | 38 | .result .hostname { 39 | position: absolute; 40 | width: 100%; 41 | left: 0; 42 | bottom: 0; 43 | font-size: 0.8em; 44 | height: 14px; 45 | } 46 | 47 | .noun { 48 | background-color: dodgerblue; 49 | border-color: #1e83f1; 50 | transform: rotate(1deg); 51 | } 52 | 53 | .verb { 54 | background-color: orangered; 55 | border-color: #e84000; 56 | transform: rotate(1deg); 57 | } 58 | 59 | .adjective { 60 | background-color: forestgreen; 61 | border-color: #1c721c; 62 | transform: rotate(-1deg); 63 | } 64 | -------------------------------------------------------------------------------- /words-dispatcher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.1-alpine 2 | 3 | EXPOSE 8080 4 | CMD ["./dispatcher"] 5 | 6 | COPY dispatcher.go . 7 | RUN go build dispatcher.go 8 | -------------------------------------------------------------------------------- /words-dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math/rand" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | rand.Seed(time.Now().UnixNano()) 15 | http.HandleFunc("/", forward("words-java", 8080)) 16 | http.ListenAndServe(":8080", nil) 17 | } 18 | 19 | func forward(host string, port int) func(w http.ResponseWriter, r *http.Request) { 20 | return func(w http.ResponseWriter, r *http.Request) { 21 | 22 | addrs, err := net.LookupHost(host) 23 | if err != nil { 24 | log.Println("Error", err) 25 | http.Error(w, err.Error(), 500) 26 | return 27 | } 28 | 29 | log.Printf("%s %d available ips: %v", r.RequestURI, len(addrs), addrs) 30 | ip := addrs[rand.Intn(len(addrs))] 31 | log.Printf("%s I choose %s", r.RequestURI, ip) 32 | 33 | url := fmt.Sprintf("http://%s:%d%s", ip, port, r.RequestURI) 34 | log.Printf("%s Calling %s", r.RequestURI, url) 35 | 36 | resp, err := http.Get(url) 37 | if err != nil { 38 | log.Println("Error", err) 39 | http.Error(w, err.Error(), 500) 40 | return 41 | } 42 | 43 | buf, err := ioutil.ReadAll(resp.Body) 44 | if err != nil { 45 | log.Println("Error", err) 46 | http.Error(w, err.Error(), 500) 47 | return 48 | } 49 | 50 | _, err = w.Write(buf) 51 | if err != nil { 52 | log.Println("Error", err) 53 | http.Error(w, err.Error(), 500) 54 | return 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /words-java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u92-jdk-alpine 2 | 3 | RUN MAVEN_VERSION=3.3.9 \ 4 | && cd /usr/share \ 5 | && wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz -O - | tar xzf - \ 6 | && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \ 7 | && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn 8 | 9 | WORKDIR /home/lab 10 | ENTRYPOINT ["java", "-DPROD_MODE=true", "-Xmx32m", "-jar", "target/words.jar"] 11 | EXPOSE 8080 12 | 13 | COPY pom.xml . 14 | RUN mvn verify -DskipTests --fail-never 15 | 16 | COPY src ./src 17 | RUN mvn verify 18 | -------------------------------------------------------------------------------- /words-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | codestory 8 | words 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 3.0.4 13 | 14 | 15 | 16 | UTF-8 17 | 1.8 18 | 1.8 19 | 20 | 21 | 22 | words 23 | 24 | 25 | 26 | 27 | maven-clean-plugin 28 | 2.6.1 29 | 30 | 31 | maven-compiler-plugin 32 | 3.3 33 | 34 | 35 | maven-deploy-plugin 36 | 2.8.2 37 | 38 | 39 | maven-install-plugin 40 | 2.5.2 41 | 42 | 43 | maven-jar-plugin 44 | 2.6 45 | 46 | 47 | maven-resources-plugin 48 | 2.7 49 | 50 | 51 | maven-site-plugin 52 | 3.4 53 | 54 | 55 | maven-source-plugin 56 | 2.4 57 | 58 | 59 | maven-release-plugin 60 | 2.5.2 61 | 62 | 63 | maven-surefire-plugin 64 | 2.18.1 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-dependency-plugin 73 | 2.10 74 | 75 | 76 | copy-dependencies 77 | package 78 | 79 | copy-dependencies 80 | 81 | 82 | 83 | 84 | 85 | maven-jar-plugin 86 | 2.5 87 | 88 | 89 | 90 | true 91 | dependency 92 | Main 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | net.code-story 103 | http 104 | 2.104 105 | 106 | 107 | net.code-story 108 | fluent 109 | 1.4 110 | 111 | 112 | org.jongo 113 | jongo 114 | 1.3.0 115 | 116 | 117 | org.mongodb 118 | mongo-java-driver 119 | 3.2.2 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /words-java/src/main/java/AddHostName.java: -------------------------------------------------------------------------------- 1 | import net.codestory.http.Context; 2 | import net.codestory.http.filters.Filter; 3 | import net.codestory.http.filters.PayloadSupplier; 4 | import net.codestory.http.payload.Payload; 5 | 6 | import java.net.InetAddress; 7 | import java.net.UnknownHostException; 8 | 9 | public class AddHostName implements Filter { 10 | private final String hostname; 11 | 12 | public AddHostName() throws UnknownHostException { 13 | hostname = InetAddress.getLocalHost().getHostAddress(); 14 | } 15 | 16 | @Override 17 | public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws Exception { 18 | Payload payload = nextFilter.get(); 19 | 20 | if (payload.rawContent() instanceof WordResponse) { 21 | WordResponse response = (WordResponse) payload.rawContent(); 22 | response.hostname = hostname; 23 | } 24 | 25 | return payload; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /words-java/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import com.google.common.base.Supplier; 2 | import com.google.common.base.Suppliers; 3 | import com.mongodb.DB; 4 | import com.mongodb.MongoClient; 5 | import com.mongodb.MongoClientOptions; 6 | import net.codestory.http.WebServer; 7 | import net.codestory.http.filters.log.LogRequestFilter; 8 | import org.jongo.Jongo; 9 | 10 | import java.net.UnknownHostException; 11 | import java.util.List; 12 | import java.util.Random; 13 | 14 | @SuppressWarnings("ALL") 15 | public class Main { 16 | public static void main(String[] args) throws UnknownHostException { 17 | Supplier jongo = Suppliers.memoize(() -> createJongo()); 18 | 19 | Supplier noun1 = Suppliers.memoize(() -> randomWord("nouns", jongo)); 20 | Supplier noun2 = Suppliers.memoize(() -> randomWord("nouns", jongo)); 21 | Supplier adjective1 = Suppliers.memoize(() -> randomWord("adjectives", jongo)); 22 | Supplier adjective2 = Suppliers.memoize(() -> randomWord("adjectives", jongo)); 23 | Supplier verb = Suppliers.memoize(() -> randomWord("verbs", jongo)); 24 | 25 | new WebServer().configure(routes -> routes 26 | .filter(LogRequestFilter.class) 27 | .filter(AddHostName.class) 28 | .get("/noun1", () -> noun1.get()) 29 | .get("/adjective1", () -> adjective1.get()) 30 | .get("/verb", () -> verb.get()) 31 | .get("/noun2", () -> noun2.get()) 32 | .get("/adjective2", () -> adjective2.get()) 33 | ).start(); 34 | } 35 | 36 | private static WordResponse randomWord(String set, Supplier jongo) { 37 | Words words = new Words(jongo.get(), set); 38 | 39 | switch (set) { 40 | case "nouns": 41 | words.addIfEmpty("dead body", "elephant", "go language", "laptop", "container", "micro-service", "turtle", "whale"); 42 | break; 43 | case "verbs": 44 | words.addIfEmpty("will drink", "smashed", "smokes", "eats", "walks towards", "loves", "helps", "pushes", "debugs"); 45 | break; 46 | case "adjectives": 47 | words.addIfEmpty("the exquisite", "a pink", "the rotten", "a red", "the floating", "a broken", "a shiny", "the pretty", "the impressive", "an awesome"); 48 | break; 49 | } 50 | 51 | Random random = new Random(); 52 | List all = words.all(); 53 | String word = all.get(random.nextInt(all.size())); 54 | 55 | return new WordResponse(word, null); 56 | } 57 | 58 | private static Jongo createJongo() { 59 | DB db = new MongoClient("mongo:27017", new MongoClientOptions.Builder() 60 | .serverSelectionTimeout(2000) 61 | .build()).getDB("lab-docker"); 62 | 63 | return new Jongo(db); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /words-java/src/main/java/WordResponse.java: -------------------------------------------------------------------------------- 1 | public class WordResponse { 2 | String word; 3 | String hostname; 4 | 5 | WordResponse(String word, String hostname) { 6 | this.word = word; 7 | this.hostname = hostname; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /words-java/src/main/java/Words.java: -------------------------------------------------------------------------------- 1 | import org.bson.types.ObjectId; 2 | import org.jongo.Jongo; 3 | import org.jongo.MongoCollection; 4 | 5 | import java.util.List; 6 | 7 | import static net.codestory.Fluent.*; 8 | 9 | class Words { 10 | private final MongoCollection collection; 11 | 12 | Words(Jongo jongo, String name) { 13 | collection = jongo.getCollection(name); 14 | collection.ensureIndex("{name:1}"); 15 | } 16 | 17 | void addIfEmpty(String... names) { 18 | if (collection.count() == 0) { 19 | collection.insert(of(names).map(Word::new).toArray(Word[]::new)); 20 | } 21 | } 22 | 23 | List all() { 24 | return of(collection.find().as(Word.class).iterator()).map(w -> w.name).toList(); 25 | } 26 | 27 | static class Word { 28 | ObjectId _id; 29 | String name; 30 | 31 | Word() { 32 | } 33 | 34 | Word(String name) { 35 | this.name = name; 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------