├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.png ├── docker-compose.yml ├── fts-hotels-index.json ├── mix-and-match.yml ├── pom.xml ├── src └── main │ ├── java │ └── trycb │ │ ├── Application.java │ │ ├── config │ │ ├── Database.java │ │ └── Request.java │ │ ├── model │ │ ├── Error.java │ │ ├── IValue.java │ │ └── Result.java │ │ ├── service │ │ ├── Airport.java │ │ ├── FlightPath.java │ │ ├── Hotel.java │ │ ├── Index.java │ │ ├── TenantUser.java │ │ └── TokenService.java │ │ ├── util │ │ └── CorsFilter.java │ │ └── web │ │ ├── AirportController.java │ │ ├── FlightPathController.java │ │ ├── HotelController.java │ │ ├── IndexController.java │ │ └── TenantUserController.java │ └── resources │ ├── application.properties │ └── static │ └── swagger.json ├── swagger.json ├── update-swagger.sh └── wait-for-couchbase.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | .DS_Store* 4 | .idea/ 5 | Thumbs.db 6 | ~* 7 | *.out 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u292-slim-buster 2 | 3 | LABEL maintainer="Couchbase" 4 | 5 | WORKDIR /app 6 | 7 | RUN mkdir -p /usr/share/man/man1 8 | RUN apt-get update && apt-get install -y \ 9 | maven \ 10 | jq curl 11 | 12 | ADD . /app 13 | 14 | # Install project dependencies and generate jar file 15 | RUN mvn clean install 16 | 17 | # Expose ports 18 | EXPOSE 8080 19 | 20 | # Set the entrypoint 21 | ENTRYPOINT ["./wait-for-couchbase.sh", "java", "-jar", "target/try-cb-java.jar"] 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2021 Couchbase, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Couchbase Java Travel-Sample Application 2 | 3 | This is a sample application for getting started with [Couchbase Server] and the [Java SDK]. 4 | The application runs a single page web UI for demonstrating SQL for Documents (N1QL), Sub-document requests and Full Text Search (FTS) querying capabilities. 5 | It uses Couchbase Server together with the [Spring Boot] web framework for [Java], [Swagger] for API documentation, [Vue] and [Bootstrap]. 6 | 7 | The application is a flight planner that allows the user to search for and select a flight route (including the return flight) based on airports and dates. 8 | Airport selection is done dynamically using an autocomplete box bound to N1QL queries on the server side. After selecting a date, it then searches 9 | for applicable air flight routes from a previously populated database. An additional page allows users to search for Hotels using less structured keywords. 10 | 11 | ![Application](app.png) 12 | 13 | 14 | ## Prerequisites 15 | 16 | To download the application you can either download [the archive](https://github.com/couchbaselabs/try-cb-java/archive/master.zip) or clone the repository: 17 | 18 | git clone https://github.com/couchbaselabs/try-cb-java.git 19 | 20 | 22 | 23 | We recommend running the application with Docker, which starts up all components for you, 24 | but you can also run it in a Mix-and-Match style, which we'll decribe below. 25 | 26 | ## Running the application with Docker 27 | 28 | You will need [Docker](https://docs.docker.com/get-docker/) installed on your machine in order to run this application as we have defined a [_Dockerfile_](Dockerfile) and a [_docker-compose.yml_](docker-compose.yml) to run Couchbase Server 7.0.0, the front-end [Vue app](https://github.com/couchbaselabs/try-cb-frontend-v2.git) and the Java REST API. 29 | 30 | To launch the full application, simply run this command from a terminal: 31 | 32 | docker-compose up 33 | 34 | > **_NOTE:_** You may need more than the default RAM to run the images. 35 | We have tested the travel-sample apps with 4.5 GB RAM configured in Docker's Preferences... -> Resources -> Memory. 36 | When you run the application for the first time, it will pull/build the relevant docker images, so it might take a bit of time. 37 | 38 | This will start the Java backend, Couchbase Server 7.0.0 and the Vue frontend app. 39 | 40 | ``` 41 | ❯ docker-compose up 42 | ... 43 | Creating couchbase-sandbox-7.0.0 ... done 44 | Creating try-cb-api ... done 45 | Creating try-cb-fe ... done 46 | Attaching to couchbase-sandbox-7.0.0, try-cb-api, try-cb-fe 47 | couchbase-sandbox-7.0.0 | Starting Couchbase Server -- Web UI available at http://:8091 48 | couchbase-sandbox-7.0.0 | and logs available in /opt/couchbase/var/lib/couchbase/logs 49 | couchbase-sandbox-7.0.0 | Configuring Couchbase Server. Please wait (~60 sec)... 50 | try-cb-api | wait-for-couchbase: checking http://db:8091/pools/default/buckets/travel-sample/ 51 | try-cb-api | wait-for-couchbase: polling for '.scopes | map(.name) | contains(["inventory", " 52 | try-cb-fe | wait-for-it: waiting 400 seconds for backend:8080 53 | try-cb-api | wait-for-couchbase: ... 54 | couchbase-sandbox-7.0.0 | Configuration completed! 55 | couchbase-sandbox-7.0.0 | Couchbase Admin UI: http://localhost:8091 56 | couchbase-sandbox-7.0.0 | Login credentials: Administrator / password 57 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/cfg 58 | try-cb-api | wait-for-couchbase: polling for '.status == "ok"' 59 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/index/hotels-index 60 | try-cb-api | wait-for-couchbase: polling for '.status == "ok"' 61 | try-cb-api | wait-for-couchbase: Failure 62 | try-cb-api | wait-for-couchbase: Creating hotels-index... 63 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/index/hotels-index/count 64 | try-cb-api | wait-for-couchbase: polling for '.count >= 917' 65 | try-cb-api | wait-for-couchbase: ... 66 | try-cb-api | wait-for-couchbase: ... 67 | try-cb-api | wait-for-couchbase: checking http://db:9102/api/v1/stats 68 | try-cb-api | wait-for-couchbase: polling for '.indexer.indexer_state == "Active"' 69 | try-cb-api | wait-for-couchbase: polling for '. | keys | contains(["travel-sample:def_airport 70 | try-cb-api | wait-for-couchbase: polling for '. | del(.indexer) | del(.["travel-sample:def_na 71 | try-cb-api | wait-for-couchbase: value is currently: 72 | try-cb-api | false 73 | try-cb-api | wait-for-couchbase: ... 74 | try-cb-api | wait-for-couchbase: polling for '. | del(.indexer) | map(.num_pending_requests = 75 | try-cb-api | 76 | try-cb-api | . ____ _ __ _ _ 77 | try-cb-api | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 78 | try-cb-api | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 79 | try-cb-api | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 80 | try-cb-api | ' |____| .__|_| |_|_| |_\__, | / / / / 81 | try-cb-api | =========|_|==============|___/=/_/_/_/ 82 | try-cb-api | :: Spring Boot :: (v2.5.0) 83 | try-cb-api | 84 | try-cb-api | 2021-06-04 14:47:12.896 INFO 1 --- [ main] trycb.Application : Starting Application v2.3.0 using Java 1.8.0_292 on e7a5966cfaad with PID 1 (/app/target/try-cb-java.jar started by root in /app) 85 | try-cb-api | 2021-06-04 14:47:12.908 INFO 1 --- [ main] trycb.Application : No active profile set, falling back to default profiles: default 86 | try-cb-api | 2021-06-04 14:47:17.271 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 87 | try-cb-api | 2021-06-04 14:47:17.335 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 88 | try-cb-api | 2021-06-04 14:47:17.335 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.46] 89 | try-cb-api | 2021-06-04 14:47:17.531 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 90 | try-cb-api | 2021-06-04 14:47:17.532 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4339 ms 91 | try-cb-api | 2021-06-04 14:47:17.787 DEBUG 1 --- [ main] o.s.w.f.CommonsRequestLoggingFilter : Filter 'logFilter' configured for use 92 | try-cb-api | 2021-06-04 14:47:19.460 INFO 1 --- [ cb-events] com.couchbase.core : [com.couchbase.core][DnsSrvLookupFailedEvent][75ms] DNS SRV lookup failed (name not found), trying to bootstrap from given hostname directly. 93 | try-cb-api | 2021-06-04 14:47:22.039 INFO 1 --- [ cb-events] com.couchbase.core : [com.couchbase.core][BucketOpenedEvent][1184ms] Opened bucket "travel-sample" {"coreId":"0x8503f8fb00000001"} 94 | try-cb-api | 2021-06-04 14:47:23.953 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 95 | try-cb-api | 2021-06-04 14:47:24.012 INFO 1 --- [ main] trycb.Application : Started Application in 12.758 seconds (JVM running for 14.829) 96 | try-cb-api | 2021-06-04 14:47:24.019 INFO 1 --- [ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state LivenessState changed to CORRECT 97 | try-cb-api | 2021-06-04 14:47:24.025 INFO 1 --- [ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC 98 | try-cb-fe | wait-for-it: backend:8080 is available after 88 seconds 99 | try-cb-fe | 100 | try-cb-fe | > try-cb-frontend-v2@0.1.0 serve 101 | try-cb-fe | > vue-cli-service serve --port 8081 102 | try-cb-fe | 103 | try-cb-fe | INFO Starting development server... 104 | try-cb-fe | DONE Compiled successfully in 7785ms2:47:36 PM 105 | try-cb-fe | 106 | try-cb-fe | 107 | try-cb-fe | App running at: 108 | try-cb-fe | - Local: http://localhost:8081/ 109 | try-cb-fe | 110 | try-cb-fe | It seems you are running Vue CLI inside a container. 111 | try-cb-fe | Access the dev server via http://localhost:/ 112 | try-cb-fe | 113 | try-cb-fe | Note that the development build is not optimized. 114 | try-cb-fe | To create a production build, run npm run build. 115 | try-cb-fe | 116 | ``` 117 | 118 | You should then be able to browse the UI, search for US airports and get flight 119 | route information. 120 | 121 | To end the application press Control+C in the terminal 122 | and wait for docker-compose to gracefully stop your containers. 123 | 124 | ## Mix and match services 125 | 126 | Instead of running all services, you can start any combination of `backend`, 127 | `frontend`, `db` via docker, and take responsibility for starting the other 128 | services yourself. 129 | 130 | As the provided `docker-compose.yml` sets up dependencies between the services, 131 | to make startup as smooth and automatic as possible, we also provide an 132 | alternative `mix-and-match.yml`. We'll look at a few useful scenarios here. 133 | 134 | ### Bring your own database 135 | 136 | If you wish to run this application against your own configuration of Couchbase 137 | Server, you will need version 7.0.0 or later with the `travel-sample` 138 | bucket setup. 139 | 140 | > **_NOTE:_** If you are not using Docker to start up the API server, or the 141 | > provided wrapper `wait-for-couchbase.sh`, you will need to create a full text 142 | > search index on travel-sample bucket called 'hotels-index'. You can do this 143 | > via the following command: 144 | 145 | curl --fail -s -u : -X PUT \ 146 | http://:8094/api/index/hotels-index \ 147 | -H 'cache-control: no-cache' \ 148 | -H 'content-type: application/json' \ 149 | -d @fts-hotels-index.json 150 | 151 | With a running Couchbase Server, you can pass the database details in: 152 | 153 | CB_HOST=10.144.211.101 CB_USER=Administrator CB_PSWD=password docker-compose -f mix-and-match.yml up backend frontend 154 | 155 | The Docker image will run the same checks as usual, and also create the 156 | `hotels-index` if it does not already exist. 157 | 158 | ### Running the Java API application manually 159 | 160 | You may want to run the Java application yourself, to make rapid changes to it, 161 | and try out the features of the Couchbase API, without having to re-build the Docker 162 | image. You may still use Docker to run the Database and Frontend components if desired. 163 | 164 | Please ensure that you have the following before proceeding. 165 | 166 | * Java 8 or later (Java 11 recommended) 167 | * Maven 3 or later 168 | 169 | Install the dependencies: 170 | 171 | mvn clean install 172 | 173 | The first time you run against a new database image, you may want to use the provided 174 | `wait-for-couchbase.sh` wrapper to ensure that all indexes are created. 175 | For example, using the Docker image provided: 176 | 177 | docker-compose -f mix-and-match.yml up db 178 | 179 | export CB_HOST=localhost CB_USER=Administrator CB_PSWD=password 180 | ./wait-for-couchbase.sh echo "Couchbase is ready!" 181 | 182 | mvn spring-boot:run -Dspring-boot.run.arguments="--storage.host=$CB_HOST storage.username=$CB_USER storage.password=$CB_PSWD" 183 | 184 | If you already have an existing Couchbase server running and correctly configured, you might run: 185 | 186 | mvn spring-boot:run -Dspring-boot.run.arguments="--storage.host=localhost storage.username=Administrator storage.password=password" 187 | 188 | Finally, if you want to see how the sample frontend Vue application works with your changes, 189 | run it with: 190 | 191 | docker-compose -f mix-and-match.yml up frontend 192 | 193 | ### Running the front-end manually 194 | 195 | To run the frontend components manually without Docker, follow the guide 196 | [here](https://github.com/couchbaselabs/try-cb-frontend-v2) 197 | 198 | ## REST API reference, and tests. 199 | 200 | All the travel-sample apps conform to the same interface, which means that they can all be used with the same database configuration and Vue.js frontend. 201 | 202 | We've integrated Swagger/OpenApi version 3 documentation which can be accessed on the backend at `http://localhost:8080/apidocs` once you have started the app. 203 | 204 | (You can also view a read-only version at https://docs.couchbase.com/java-sdk/current/hello-world/sample-application.html#) 205 | 206 | To further ensure that every app conforms to the API, we have a [test suite][try-cb-test], which you can simply run with the command: 207 | 208 | ``` 209 | docker-compose --profile test up test 210 | ``` 211 | 212 | If you are running locally though, with a view to extending or modifying the travel-sample app, you will likely want to be able to make changes to both the code and the tests in parallel. 213 | 214 | * Start the backend server locally, for example using "Running the Java API application manually" above. 215 | * Check out the [test suite][try-cb-test] repo in a separate working directory, and run the tests manually, as per the instructions. 216 | 217 | Check the test repo for details on how to run locally. 218 | 219 | [Couchbase Server]: https://www.couchbase.com/ 220 | [Java SDK]: https://docs.couchbase.com/java-sdk/current/hello-world/overview.html 221 | [Spring Boot]: https://spring.io/projects/spring-boot 222 | [Java]: https://www.java.com/en/ 223 | [Swagger]: https://swagger.io/resources/open-api/ 224 | [Vue]: https://vuejs.org/ 225 | [Bootstrap]: https://getbootstrap.com/ 226 | [try-cb-test]: https://github.com/couchbaselabs/try-cb-test/ 227 | -------------------------------------------------------------------------------- /app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-java/c80b85ee1eee34b3c29c4b44e61840459fb8f42e/app.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | backend: 5 | build: . 6 | depends_on: 7 | - db 8 | ports: 9 | - 8080:8080 10 | container_name: try-cb-api 11 | 12 | frontend: 13 | build: "https://github.com/couchbaselabs/try-cb-frontend-v2.git#7.0" 14 | depends_on: 15 | - backend 16 | ports: 17 | - 8081:8081 18 | container_name: try-cb-fe 19 | entrypoint: ["wait-for-it", "backend:8080", "--timeout=400", "--strict", "--", "npm", "run", "serve"] 20 | 21 | db: 22 | image: couchbase/server-sandbox:7.0.0 23 | ports: 24 | - "8091-8095:8091-8095" 25 | - "11210:11210" 26 | expose: # expose ports 8091 & 8094 to other containers (mainly for backend) 27 | - "8091" 28 | - "8094" 29 | container_name: couchbase-sandbox-7.0.0 30 | 31 | test: 32 | build: "https://github.com/couchbaselabs/try-cb-test.git#main" 33 | depends_on: 34 | - backend 35 | environment: 36 | BACKEND_BASE_URL: http://backend:8080 37 | entrypoint: ["wait-for-it", "backend:8080", "--timeout=400", "--strict", "--", "bats", "travel-sample-backend.bats"] 38 | profiles: 39 | - test 40 | -------------------------------------------------------------------------------- /fts-hotels-index.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotels-index", 3 | "type": "fulltext-index", 4 | "params": { 5 | "doc_config": { 6 | "docid_prefix_delim": "", 7 | "docid_regexp": "", 8 | "mode": "scope.collection.type_field", 9 | "type_field": "type" 10 | }, 11 | "mapping": { 12 | "default_analyzer": "standard", 13 | "default_datetime_parser": "dateTimeOptional", 14 | "default_field": "_all", 15 | "default_mapping": { 16 | "dynamic": true, 17 | "enabled": false 18 | }, 19 | "default_type": "_default", 20 | "docvalues_dynamic": false, 21 | "index_dynamic": true, 22 | "store_dynamic": false, 23 | "type_field": "_type", 24 | "types": { 25 | "inventory.hotel": { 26 | "dynamic": true, 27 | "enabled": true 28 | } 29 | } 30 | }, 31 | "store": { 32 | "indexType": "scorch", 33 | "segmentVersion": 15 34 | } 35 | }, 36 | "sourceType": "couchbase", 37 | "sourceName": "travel-sample", 38 | "sourceParams": {}, 39 | "planParams": { 40 | "maxPartitionsPerPIndex": 1024, 41 | "indexPartitions": 1, 42 | "numReplicas": 0 43 | }, 44 | "uuid": "" 45 | } 46 | -------------------------------------------------------------------------------- /mix-and-match.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | backend: 5 | build: . 6 | ports: 7 | - 8080:8080 8 | environment: 9 | - CB_HOST 10 | - CB_USER 11 | - CB_PSWD 12 | command: --storage.host=${CB_HOST} --storage.username=${CB_USER} --storage.password=${CB_PSWD} 13 | container_name: try-cb-api-mm 14 | 15 | frontend: 16 | build: "https://github.com/couchbaselabs/try-cb-frontend-v2.git#7.0" 17 | ports: 18 | - 8081:8081 19 | container_name: try-cb-fe-mm 20 | 21 | db: 22 | image: couchbase/server-sandbox:7.0.0 23 | ports: 24 | - "8091-8095:8091-8095" 25 | - "9102:9102" 26 | - "11210:11210" 27 | expose: 28 | - "8091" 29 | - "8094" 30 | container_name: couchbase-sandbox-7.0.0-mm 31 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.couchbase.example 6 | try-cb-java 7 | 2.3.0 8 | 9 | 10 | org.springframework.boot 11 | spring-boot-starter-parent 12 | 2.6.6 13 | 14 | 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | 23 | 24 | com.couchbase.client 25 | java-client 26 | 3.1.5 27 | 28 | 29 | 30 | 31 | javax.xml.bind 32 | jaxb-api 33 | 2.2.11 34 | 35 | 36 | 37 | 38 | org.springframework 39 | spring-tx 40 | 41 | 42 | 43 | 44 | org.springframework.security 45 | spring-security-core 46 | 47 | 48 | 49 | 50 | io.jsonwebtoken 51 | jjwt 52 | 0.6.0 53 | 54 | 55 | 56 | 57 | org.springdoc 58 | springdoc-openapi-ui 59 | 1.5.9 60 | 61 | 62 | 63 | 64 | 65 | 66 | couchbase 67 | Couchbase Preview Repository 68 | http://files.couchbase.com/maven2 69 | 70 | 71 | 72 | 73 | 74 | try-cb-java 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | com.spotify 82 | docker-maven-plugin 83 | 0.4.11 84 | 85 | trycb/java 86 | src/main/docker 87 | 88 | ${project.build.finalName}.jar 89 | 90 | 91 | 92 | / 93 | ${project.build.directory} 94 | ${project.build.finalName}.jar 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/main/java/trycb/Application.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Couchbase, Inc. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING 20 | * IN THE SOFTWARE. 21 | */ 22 | package trycb; 23 | 24 | import org.springframework.boot.SpringApplication; 25 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 26 | import org.springframework.boot.autoconfigure.SpringBootApplication; 27 | import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; 28 | 29 | @SpringBootApplication 30 | @EnableAutoConfiguration(exclude = {CouchbaseAutoConfiguration.class}) 31 | public class Application { 32 | 33 | public static void main(String[] args) { 34 | SpringApplication.run(Application.class, args); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/trycb/config/Database.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Couchbase, Inc. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING 20 | * IN THE SOFTWARE. 21 | */ 22 | package trycb.config; 23 | 24 | import com.couchbase.client.java.Bucket; 25 | import com.couchbase.client.java.Cluster; 26 | 27 | import org.springframework.beans.factory.annotation.Value; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | 31 | @Configuration 32 | public class Database { 33 | 34 | @Value("${storage.host}") 35 | private String host; 36 | 37 | @Value("${storage.bucket}") 38 | private String bucket; 39 | 40 | @Value("${storage.username}") 41 | private String username; 42 | 43 | @Value("${storage.password}") 44 | private String password; 45 | 46 | public @Bean Cluster loginCluster() { 47 | return Cluster.connect(host, username, password); 48 | } 49 | 50 | public @Bean Bucket loginBucket() { 51 | return loginCluster().bucket(bucket); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/trycb/config/Request.java: -------------------------------------------------------------------------------- 1 | package trycb.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.filter.CommonsRequestLoggingFilter; 6 | 7 | /** 8 | * Trace incoming HTTP requests using Spring Boot CommonsRequestLoggingFilter. 9 | * This allows us to see incoming request logs when running the application. 10 | */ 11 | @Configuration 12 | public class Request { 13 | 14 | public @Bean CommonsRequestLoggingFilter logFilter() { 15 | CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); 16 | filter.setIncludeQueryString(true); 17 | filter.setIncludePayload(true); 18 | filter.setMaxPayloadLength(10000); 19 | filter.setIncludeHeaders(false); 20 | filter.setAfterMessagePrefix("REQUEST: "); 21 | return filter; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/trycb/model/Error.java: -------------------------------------------------------------------------------- 1 | package trycb.model; 2 | 3 | /** 4 | * A standardized error format for failing responses, that the frontend 5 | * application can interpret for all endpoints. 6 | */ 7 | public class Error implements IValue { 8 | 9 | private final String message; 10 | 11 | public Error(String message) { 12 | this.message = message; 13 | } 14 | 15 | public String getMessage() { 16 | return message; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/trycb/model/IValue.java: -------------------------------------------------------------------------------- 1 | package trycb.model; 2 | 3 | /** 4 | * Marker interface for standardized values returned to the frontend. 5 | */ 6 | public interface IValue { } 7 | -------------------------------------------------------------------------------- /src/main/java/trycb/model/Result.java: -------------------------------------------------------------------------------- 1 | package trycb.model; 2 | 3 | /** 4 | * A standardized result format for successful responses, that the frontend 5 | * application can interpret for all endpoints. Allows to contain user-facing 6 | * data and an array of context strings, eg. N1QL queries, to be displayed in a 7 | * "learn more" or console kind of UI element on the front end. 8 | * 9 | */ 10 | public class Result implements IValue { 11 | 12 | private final T data; 13 | private final String[] context; 14 | 15 | private Result(T data, String... contexts) { 16 | this.data = data; 17 | this.context = contexts; 18 | } 19 | 20 | public static Result of(T data, String... contexts) { 21 | return new Result(data, contexts); 22 | } 23 | 24 | public T getData() { 25 | return data; 26 | } 27 | 28 | public String[] getContext() { 29 | return context; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/Airport.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import com.couchbase.client.core.error.QueryException; 8 | import com.couchbase.client.java.Cluster; 9 | import com.couchbase.client.java.json.JsonObject; 10 | import com.couchbase.client.java.query.QueryOptions; 11 | import com.couchbase.client.java.query.QueryResult; 12 | 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.dao.DataRetrievalFailureException; 16 | import org.springframework.stereotype.Service; 17 | 18 | import trycb.model.Result; 19 | 20 | @Service 21 | public class Airport { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(Airport.class); 24 | 25 | /** 26 | * Find all airports. 27 | */ 28 | public static Result>> findAll(final Cluster cluster, final String bucket, String params) { 29 | StringBuilder builder = new StringBuilder(); 30 | builder.append("SELECT airportname FROM `").append(bucket).append("`.inventory.airport WHERE "); 31 | boolean sameCase = (params.equals(params.toUpperCase()) || params.equals(params.toLowerCase())); 32 | if (params.length() == 3 && sameCase) { 33 | builder.append("faa = $val"); 34 | params = params.toUpperCase(); 35 | } else if (params.length() == 4 && sameCase) { 36 | builder.append("icao = $val"); 37 | params = params.toUpperCase(); 38 | } else { 39 | // The airport name should start with the parameter value. 40 | builder.append("POSITION(LOWER(airportname), $val) = 0"); 41 | params = params.toLowerCase(); 42 | } 43 | String query = builder.toString(); 44 | 45 | logQuery(query); 46 | QueryResult result = null; 47 | try { 48 | result = cluster.query(query, QueryOptions.queryOptions().raw("$val", params)); 49 | } catch (QueryException e) { 50 | LOGGER.warn("Query failed with exception: " + e); 51 | throw new DataRetrievalFailureException("Query error", e); 52 | } 53 | 54 | List resultObjects = result.rowsAsObject(); 55 | List> data = new LinkedList>(); 56 | for (JsonObject obj : resultObjects) { 57 | data.add(obj.toMap()); 58 | } 59 | 60 | String querytype = "N1QL query - scoped to inventory: "; 61 | return Result.of(data, querytype, query); 62 | } 63 | 64 | /** 65 | * Helper method to log the executing query. 66 | */ 67 | private static void logQuery(String query) { 68 | LOGGER.info("Executing Query: {}", query); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/FlightPath.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | import java.util.Calendar; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Random; 8 | 9 | import com.couchbase.client.core.error.QueryException; 10 | import com.couchbase.client.java.Cluster; 11 | import com.couchbase.client.java.json.JsonArray; 12 | import com.couchbase.client.java.json.JsonObject; 13 | import com.couchbase.client.java.query.QueryOptions; 14 | import com.couchbase.client.java.query.QueryResult; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.dao.DataRetrievalFailureException; 19 | import org.springframework.stereotype.Service; 20 | 21 | import trycb.model.Result; 22 | 23 | @Service 24 | public class FlightPath { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(FlightPath.class); 27 | 28 | /** 29 | * Find all flight paths. 30 | */ 31 | public static Result>> findAll(final Cluster cluster, final String bucket, String from, 32 | String to, Calendar leave) { 33 | StringBuilder builder = new StringBuilder(); 34 | builder.append("SELECT faa as fromAirport "); 35 | builder.append("FROM `").append(bucket).append("`.inventory.airport "); 36 | builder.append("WHERE airportname = $from "); 37 | builder.append("UNION "); 38 | builder.append("SELECT faa as toAirport "); 39 | builder.append("FROM `").append(bucket).append("`.inventory.airport "); 40 | builder.append("WHERE airportname = $to"); 41 | String unionQuery = builder.toString(); 42 | 43 | logQuery(unionQuery); 44 | QueryResult result = null; 45 | try { 46 | result = cluster.query(unionQuery, QueryOptions.queryOptions().raw("$from", from).raw("$to", to)); 47 | } catch (QueryException e) { 48 | LOGGER.warn("Query failed with exception: " + e); 49 | throw new DataRetrievalFailureException("Query error: " + result); 50 | } 51 | 52 | List rows = result.rowsAsObject(); 53 | String fromAirport = null; 54 | String toAirport = null; 55 | for (JsonObject obj : rows) { 56 | if (obj.containsKey("fromAirport")) { 57 | fromAirport = obj.getString("fromAirport"); 58 | } 59 | if (obj.containsKey("toAirport")) { 60 | toAirport = obj.getString("toAirport"); 61 | } 62 | } 63 | 64 | StringBuilder joinBuilder = new StringBuilder(); 65 | joinBuilder.append("SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment "); 66 | joinBuilder.append("FROM `").append(bucket).append("`.inventory.route AS r "); 67 | joinBuilder.append("UNNEST r.schedule AS s "); 68 | joinBuilder.append("JOIN `").append(bucket).append("`.inventory.airline AS a ON KEYS r.airlineid "); 69 | joinBuilder.append("WHERE r.sourceairport = ? and r.destinationairport = ? "); 70 | joinBuilder.append("AND s.day = ? "); 71 | joinBuilder.append("ORDER BY a.name ASC"); 72 | String joinQuery = joinBuilder.toString(); 73 | 74 | JsonArray params = JsonArray.create(); 75 | params.add(fromAirport); 76 | params.add(toAirport); 77 | params.add(leave.get(Calendar.DAY_OF_WEEK)); 78 | 79 | logQuery(joinQuery); 80 | QueryResult otherResult = null; 81 | try { 82 | otherResult = cluster.query(joinQuery, QueryOptions.queryOptions().parameters(params)); 83 | } catch (QueryException e) { 84 | LOGGER.warn("Query failed with exception: " + e); 85 | throw new DataRetrievalFailureException("Query error: " + otherResult); 86 | } 87 | 88 | List resultRows = otherResult.rowsAsObject(); 89 | Random rand = new Random(); 90 | List> data = new LinkedList>(); 91 | for (JsonObject row : resultRows) { 92 | row.put("flighttime", rand.nextInt(8000)); 93 | row.put("price", Math.ceil(row.getDouble("flighttime") / 8 * 100) / 100); 94 | data.add(row.toMap()); 95 | } 96 | 97 | String querytype = "N1QL query - scoped to inventory: "; 98 | return Result.of(data, querytype, unionQuery, joinQuery); 99 | } 100 | 101 | /** 102 | * Helper method to log the executing query. 103 | */ 104 | private static void logQuery(String query) { 105 | LOGGER.info("Executing Query: {}", query); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/Hotel.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | import static com.couchbase.client.java.kv.LookupInSpec.get; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import com.couchbase.client.core.error.DocumentNotFoundException; 12 | import com.couchbase.client.java.Bucket; 13 | import com.couchbase.client.java.Cluster; 14 | import com.couchbase.client.java.Collection; 15 | import com.couchbase.client.java.Scope; 16 | import com.couchbase.client.java.kv.LookupInResult; 17 | import com.couchbase.client.java.search.SearchOptions; 18 | import com.couchbase.client.java.search.SearchQuery; 19 | import com.couchbase.client.java.search.queries.ConjunctionQuery; 20 | import com.couchbase.client.java.search.result.SearchResult; 21 | import com.couchbase.client.java.search.result.SearchRow; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.dao.DataRetrievalFailureException; 27 | import org.springframework.stereotype.Service; 28 | 29 | import trycb.model.Result; 30 | 31 | @Service 32 | public class Hotel { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(Hotel.class); 35 | 36 | private Bucket bucket; 37 | 38 | @Autowired 39 | public Hotel(Bucket bucket) { 40 | this.bucket = bucket; 41 | } 42 | 43 | /** 44 | * Search for a hotel in a particular location. 45 | */ 46 | public Result>> findHotels(final Cluster cluster, final String location, 47 | final String description) { 48 | ConjunctionQuery fts = SearchQuery.conjuncts(SearchQuery.term("hotel").field("type")); 49 | 50 | if (location != null && !location.isEmpty() && !"*".equals(location)) { 51 | fts.and(SearchQuery.disjuncts( 52 | SearchQuery.matchPhrase(location).field("country"), 53 | SearchQuery.matchPhrase(location).field("city"), 54 | SearchQuery.matchPhrase(location).field("state"), 55 | SearchQuery.matchPhrase(location).field("address") 56 | )); 57 | } 58 | 59 | if (description != null && !description.isEmpty() && !"*".equals(description)) { 60 | fts.and(SearchQuery.disjuncts( 61 | SearchQuery.matchPhrase(description).field("description"), 62 | SearchQuery.matchPhrase(description).field("name") 63 | )); 64 | } 65 | 66 | logQuery(fts.export().toString()); 67 | SearchOptions opts = SearchOptions.searchOptions().limit(100); 68 | SearchResult result = cluster.searchQuery("hotels-index", fts, opts); 69 | 70 | String queryType = "FTS search - scoped to: inventory.hotel within fields country, city, state, address, name, description"; 71 | return Result.of(extractResultOrThrow(result), queryType); 72 | } 73 | 74 | /** 75 | * Search for an hotel. 76 | */ 77 | public Result>> findHotels(final Cluster cluster, final String description) { 78 | return findHotels(cluster, "*", description); 79 | } 80 | 81 | /** 82 | * Find all hotels. 83 | */ 84 | public Result>> findAllHotels(final Cluster cluster) { 85 | return findHotels(cluster, "*", "*"); 86 | } 87 | 88 | /** 89 | * Extract a FTS result or throw if there is an issue. 90 | */ 91 | private List> extractResultOrThrow(SearchResult result) { 92 | if (result.metaData().metrics().errorPartitionCount() > 0) { 93 | LOGGER.warn("Query returned with errors: " + result.metaData().errors()); 94 | throw new DataRetrievalFailureException("Query error: " + result.metaData().errors()); 95 | } 96 | 97 | List> content = new ArrayList>(); 98 | for (SearchRow row : result.rows()) { 99 | 100 | LookupInResult res; 101 | try { 102 | Scope scope = bucket.scope("inventory"); 103 | Collection collection = scope.collection("hotel"); 104 | res = collection.lookupIn(row.id(), 105 | Arrays.asList(get("country"), get("city"), get("state"), 106 | get("address"), get("name"), get("description"))); 107 | } catch (DocumentNotFoundException ex) { 108 | continue; 109 | } 110 | 111 | Map map = new HashMap(); 112 | 113 | String country = res.contentAs(0, String.class); 114 | String city = res.contentAs(1, String.class); 115 | String state = res.contentAs(2, String.class); 116 | String address = res.contentAs(3, String.class); 117 | 118 | StringBuilder fullAddr = new StringBuilder(); 119 | if (address != null) 120 | fullAddr.append(address).append(", "); 121 | if (city != null) 122 | fullAddr.append(city).append(", "); 123 | if (state != null) 124 | fullAddr.append(state).append(", "); 125 | if (country != null) 126 | fullAddr.append(country); 127 | 128 | if (fullAddr.length() > 2 && fullAddr.charAt(fullAddr.length() - 2) == ',') 129 | fullAddr.delete(fullAddr.length() - 2, fullAddr.length() - 1); 130 | 131 | map.put("name", res.contentAs(4, String.class)); 132 | map.put("description", res.contentAs(5, String.class)); 133 | map.put("address", fullAddr.toString()); 134 | 135 | content.add(map); 136 | } 137 | return content; 138 | } 139 | 140 | /** 141 | * Helper method to log the executing query. 142 | */ 143 | private static void logQuery(String query) { 144 | LOGGER.info("Executing FTS Query: {}", query); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/Index.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | public class Index { 4 | /** 5 | * Returns the index page. 6 | */ 7 | public static String getInfo() { 8 | return "

Java Travel Sample API

" 9 | + "A sample API for getting started with Couchbase Server and the Java SDK." + ""; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/TenantUser.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | import static com.couchbase.client.java.kv.InsertOptions.insertOptions; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | 11 | import com.couchbase.client.core.error.DocumentNotFoundException; 12 | import com.couchbase.client.core.msg.kv.DurabilityLevel; 13 | import com.couchbase.client.java.Bucket; 14 | import com.couchbase.client.java.Collection; 15 | import com.couchbase.client.java.Scope; 16 | import com.couchbase.client.java.json.JsonArray; 17 | import com.couchbase.client.java.json.JsonObject; 18 | import com.couchbase.client.java.kv.GetResult; 19 | import com.couchbase.client.java.kv.InsertOptions; 20 | 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; 23 | import org.springframework.security.authentication.AuthenticationServiceException; 24 | import org.springframework.security.crypto.bcrypt.BCrypt; 25 | import org.springframework.stereotype.Service; 26 | 27 | import trycb.model.Result; 28 | 29 | @Service 30 | public class TenantUser { 31 | 32 | private final TokenService jwtService; 33 | 34 | @Autowired 35 | public TenantUser(TokenService jwtService) { 36 | this.jwtService = jwtService; 37 | } 38 | 39 | static final String USERS_COLLECTION_NAME = "users"; 40 | static final String BOOKINGS_COLLECTION_NAME = "bookings"; 41 | 42 | /** 43 | * Try to log the given tenant user in. 44 | */ 45 | public Result> login(final Bucket bucket, final String tenant, final String username, 46 | final String password) { 47 | Scope scope = bucket.scope(tenant); 48 | Collection collection = scope.collection(USERS_COLLECTION_NAME); 49 | String queryType = String.format("KV get - scoped to %s.users: for password field in document %s", scope.name(), 50 | username); 51 | 52 | GetResult doc; 53 | try { 54 | doc = collection.get(username); 55 | } catch (DocumentNotFoundException ex) { 56 | throw new AuthenticationCredentialsNotFoundException("Bad Username or Password"); 57 | } 58 | JsonObject res = doc.contentAsObject(); 59 | if (BCrypt.checkpw(password, res.getString("password"))) { 60 | Map data = JsonObject.create() 61 | .put("token", jwtService.buildToken(username)) 62 | .toMap(); 63 | return Result.of(data, queryType); 64 | } else { 65 | throw new AuthenticationCredentialsNotFoundException("Bad Username or Password"); 66 | } 67 | } 68 | 69 | /** 70 | * Create a tenant user. 71 | */ 72 | public Result> createLogin(final Bucket bucket, final String tenant, final String username, 73 | final String password, DurabilityLevel expiry) { 74 | String passHash = BCrypt.hashpw(password, BCrypt.gensalt()); 75 | JsonObject doc = JsonObject.create() 76 | .put("type", "user") 77 | .put("name", username) 78 | .put("password", passHash); 79 | InsertOptions options = insertOptions(); 80 | if (expiry.ordinal() > 0) { 81 | options.durability(expiry); 82 | } 83 | 84 | Scope scope = bucket.scope(tenant); 85 | Collection collection = scope.collection(USERS_COLLECTION_NAME); 86 | String queryType = String.format("KV insert - scoped to %s.users: document %s", scope.name(), username); 87 | try { 88 | collection.insert(username, doc, options); 89 | Map data = JsonObject.create().put("token", jwtService.buildToken(username)).toMap(); 90 | return Result.of(data, queryType); 91 | } catch (Exception e) { 92 | e.printStackTrace(); 93 | throw new AuthenticationServiceException("There was an error creating account"); 94 | } 95 | } 96 | 97 | /* 98 | * Register a flight (or flights) for the given tenant user. 99 | */ 100 | public Result> registerFlightForUser(final Bucket bucket, final String tenant, 101 | final String username, final JsonArray newFlights) { 102 | String userId = username; 103 | GetResult userDataFetch; 104 | Scope scope = bucket.scope(tenant); 105 | Collection usersCollection = scope.collection(USERS_COLLECTION_NAME); 106 | Collection bookingsCollection = scope.collection(BOOKINGS_COLLECTION_NAME); 107 | 108 | try { 109 | userDataFetch = usersCollection.get(userId); 110 | } catch (DocumentNotFoundException ex) { 111 | throw new IllegalStateException(); 112 | } 113 | JsonObject userData = userDataFetch.contentAsObject(); 114 | 115 | if (newFlights == null) { 116 | throw new IllegalArgumentException("No flights in payload"); 117 | } 118 | 119 | JsonArray added = JsonArray.create(); 120 | JsonArray allBookedFlights = userData.getArray("flights"); 121 | if (allBookedFlights == null) { 122 | allBookedFlights = JsonArray.create(); 123 | } 124 | 125 | for (Object newFlight : newFlights) { 126 | checkFlight(newFlight); 127 | JsonObject t = ((JsonObject) newFlight); 128 | t.put("bookedon", "try-cb-java"); 129 | String flightId = UUID.randomUUID().toString(); 130 | bookingsCollection.insert(flightId, t); 131 | allBookedFlights.add(flightId); 132 | added.add(t); 133 | } 134 | 135 | userData.put("flights", allBookedFlights); 136 | usersCollection.upsert(userId, userData); 137 | 138 | JsonObject responseData = JsonObject.create().put("added", added); 139 | 140 | String queryType = String.format("KV update - scoped to %s.users: for bookings field in document %s", 141 | scope.name(), username); 142 | return Result.of(responseData.toMap(), queryType); 143 | } 144 | 145 | private static void checkFlight(Object f) { 146 | if (f == null || !(f instanceof JsonObject)) { 147 | throw new IllegalArgumentException("Each flight must be a non-null object"); 148 | } 149 | JsonObject flight = (JsonObject) f; 150 | if (!flight.containsKey("name") || !flight.containsKey("date") || !flight.containsKey("sourceairport") 151 | || !flight.containsKey("destinationairport")) { 152 | throw new IllegalArgumentException("Malformed flight inside flights payload" + flight.toString()); 153 | } 154 | } 155 | 156 | public Result>> getFlightsForUser(final Bucket bucket, final String tenant, 157 | final String username) { 158 | GetResult userDoc; 159 | Scope scope = bucket.scope(tenant); 160 | Collection usersCollection = scope.collection(USERS_COLLECTION_NAME); 161 | Collection bookingsCollection = scope.collection(BOOKINGS_COLLECTION_NAME); 162 | 163 | try { 164 | userDoc = usersCollection.get(username); 165 | } catch (DocumentNotFoundException ex) { 166 | return Result.of(Collections.emptyList()); 167 | } 168 | JsonObject userData = userDoc.contentAsObject(); 169 | JsonArray flights = userData.getArray("flights"); 170 | if (flights == null) { 171 | return Result.of(Collections.emptyList()); 172 | } 173 | 174 | // The "flights" array contains flight ids. Convert them to actual objects. 175 | List> results = new ArrayList>(); 176 | for (int i = 0; i < flights.size(); i++) { 177 | String flightId = flights.getString(i); 178 | GetResult res; 179 | try { 180 | res = bookingsCollection.get(flightId); 181 | } catch (DocumentNotFoundException ex) { 182 | throw new RuntimeException("Unable to retrieve flight id " + flightId); 183 | } 184 | Map flight = res.contentAsObject().toMap(); 185 | results.add(flight); 186 | } 187 | 188 | String queryType = String.format("KV get - scoped to %s.users: for %d bookings in document %s", scope.name(), 189 | results.size(), username); 190 | return Result.of(results, queryType); 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/trycb/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package trycb.service; 2 | 3 | import com.couchbase.client.core.deps.io.netty.util.CharsetUtil; 4 | import com.couchbase.client.java.json.JsonObject; 5 | import io.jsonwebtoken.JwtException; 6 | import io.jsonwebtoken.Jwts; 7 | import io.jsonwebtoken.SignatureAlgorithm; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.Base64Utils; 11 | 12 | @Service 13 | public class TokenService { 14 | 15 | 16 | @Value("${jwt.secret}") 17 | private String secret; 18 | 19 | @Value("${jwt.enabled}") 20 | private boolean useJwt; 21 | 22 | /** 23 | * @throws IllegalStateException when the Authorization header couldn't be verified or didn't match the expected 24 | * username. 25 | */ 26 | public void verifyAuthenticationHeader(String authorization, String expectedUsername) { 27 | String token = authorization.replaceFirst("Bearer ", ""); 28 | String tokenName; 29 | if (useJwt) { 30 | tokenName = verifyJwt(token); 31 | } else { 32 | tokenName = verifySimple(token); 33 | } 34 | if (!expectedUsername.equals(tokenName)) { 35 | throw new IllegalStateException("Token and username don't match"); 36 | } 37 | } 38 | 39 | private String verifyJwt(String token) { 40 | try { 41 | String username = Jwts.parser() 42 | .setSigningKey(secret) 43 | .parseClaimsJws(token) 44 | .getBody() 45 | .get("user", String.class); 46 | return username; 47 | } catch (JwtException e) { 48 | throw new IllegalStateException("Could not verify JWT token", e); 49 | } 50 | } 51 | 52 | private String verifySimple(String token) { 53 | try { 54 | return new String(Base64Utils.decodeFromString(token)); 55 | } catch (Exception e) { 56 | throw new IllegalStateException("Could not verify simple token", e); 57 | } 58 | } 59 | 60 | public String buildToken(String username) { 61 | if (useJwt) { 62 | return buildJwtToken(username); 63 | } else { 64 | return buildSimpleToken(username); 65 | } 66 | } 67 | 68 | private String buildJwtToken(String username) { 69 | String token = Jwts.builder().signWith(SignatureAlgorithm.HS512, secret) 70 | .setPayload(JsonObject.create() 71 | .put("user", username) 72 | .toString()) 73 | .compact(); 74 | return token; 75 | } 76 | 77 | private String buildSimpleToken(String username) { 78 | return Base64Utils.encodeToString(username.getBytes(CharsetUtil.UTF_8)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/trycb/util/CorsFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Couchbase, Inc. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING 20 | * IN THE SOFTWARE. 21 | */ 22 | package trycb.util; 23 | 24 | import java.io.IOException; 25 | 26 | import javax.servlet.Filter; 27 | import javax.servlet.FilterChain; 28 | import javax.servlet.FilterConfig; 29 | import javax.servlet.ServletException; 30 | import javax.servlet.ServletRequest; 31 | import javax.servlet.ServletResponse; 32 | import javax.servlet.http.HttpServletResponse; 33 | 34 | import org.springframework.stereotype.Component; 35 | 36 | @Component 37 | public class CorsFilter implements Filter { 38 | 39 | @Override 40 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 41 | throws IOException, ServletException { 42 | HttpServletResponse response = (HttpServletResponse) res; 43 | 44 | response.setHeader("Access-Control-Allow-Origin", "*"); 45 | response.setHeader("Access-Control-Allow-Credentials", "true"); 46 | response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE"); 47 | response.setHeader("Access-Control-Allow-Headers", 48 | "Origin, X-Requested-With, Content-Type, Accept, Authorization"); 49 | 50 | chain.doFilter(req, res); 51 | } 52 | 53 | @Override 54 | public void init(FilterConfig filterConfig) throws ServletException {} 55 | 56 | @Override 57 | public void destroy() {} 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/trycb/web/AirportController.java: -------------------------------------------------------------------------------- 1 | package trycb.web; 2 | 3 | import com.couchbase.client.java.Bucket; 4 | import com.couchbase.client.java.Cluster; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import trycb.model.Error; 16 | import trycb.model.IValue; 17 | import trycb.service.Airport; 18 | 19 | @RestController 20 | @RequestMapping("/api/airports") 21 | public class AirportController { 22 | 23 | private final Cluster cluster; 24 | private final Bucket bucket; 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(AirportController.class); 27 | 28 | @Autowired 29 | public AirportController(Cluster cluster, Bucket bucket) { 30 | this.cluster = cluster; 31 | this.bucket = bucket; 32 | } 33 | 34 | @RequestMapping 35 | public ResponseEntity airports(@RequestParam("search") String search) { 36 | try { 37 | return ResponseEntity.ok(Airport.findAll(cluster, bucket.name(), search)); 38 | } catch (Exception e) { 39 | LOGGER.error("Failed with exception blah", e); 40 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new Error(e.getMessage())); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/trycb/web/FlightPathController.java: -------------------------------------------------------------------------------- 1 | package trycb.web; 2 | 3 | import java.text.DateFormat; 4 | import java.util.Calendar; 5 | import java.util.Locale; 6 | 7 | import com.couchbase.client.java.Bucket; 8 | import com.couchbase.client.java.Cluster; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import trycb.model.Error; 21 | import trycb.model.IValue; 22 | import trycb.service.FlightPath; 23 | 24 | @RestController 25 | @RequestMapping("/api/flightPaths") 26 | public class FlightPathController { 27 | 28 | private final Cluster cluster; 29 | private final Bucket bucket; 30 | 31 | private static final Logger LOGGER = LoggerFactory.getLogger(FlightPathController.class); 32 | 33 | @Autowired 34 | public FlightPathController(Cluster cluster, Bucket bucket) { 35 | this.cluster = cluster; 36 | this.bucket = bucket; 37 | } 38 | 39 | @RequestMapping("/{from}/{to}") 40 | public ResponseEntity all(@PathVariable("from") String from, @PathVariable("to") String to, 41 | @RequestParam String leave) { 42 | try { 43 | Calendar calendar = Calendar.getInstance(Locale.US); 44 | calendar.setTime(DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).parse(leave)); 45 | return ResponseEntity.ok(FlightPath.findAll(cluster, bucket.name(), from, to, calendar)); 46 | } catch (Exception e) { 47 | LOGGER.error("Failed with exception", e); 48 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new Error(e.getMessage())); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/trycb/web/HotelController.java: -------------------------------------------------------------------------------- 1 | package trycb.web; 2 | 3 | import com.couchbase.client.java.Cluster; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import trycb.model.Error; 16 | import trycb.model.IValue; 17 | import trycb.service.Hotel; 18 | 19 | @RestController 20 | @RequestMapping("/api/hotels") 21 | public class HotelController { 22 | 23 | private final Cluster cluster; 24 | private final Hotel hotelService; 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(HotelController.class); 27 | private static final String LOG_FAILURE_MESSAGE = "Failed with exception"; 28 | 29 | @Autowired 30 | public HotelController(Cluster cluster, Hotel hotelService) { 31 | this.cluster = cluster; 32 | this.hotelService = hotelService; 33 | } 34 | 35 | @RequestMapping(value = "/{description}/{location}/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 36 | public ResponseEntity findHotelsByDescriptionAndLocation( 37 | @PathVariable("location") String location, @PathVariable("description") String desc) { 38 | try { 39 | return ResponseEntity.ok(hotelService.findHotels(cluster, location, desc)); 40 | } catch (Exception e) { 41 | LOGGER.error(LOG_FAILURE_MESSAGE, e); 42 | return ResponseEntity.badRequest().body(new Error(e.getMessage())); 43 | } 44 | } 45 | 46 | @RequestMapping(value = "/{description}/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 47 | public ResponseEntity findHotelsByDescription(@PathVariable("description") String desc) { 48 | try { 49 | return ResponseEntity.ok(hotelService.findHotels(cluster, desc)); 50 | } catch (Exception e) { 51 | LOGGER.error(LOG_FAILURE_MESSAGE, e); 52 | return ResponseEntity.badRequest().body(new Error(e.getMessage())); 53 | } 54 | } 55 | 56 | @RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 57 | public ResponseEntity findAllHotels() { 58 | try { 59 | return ResponseEntity.ok(hotelService.findAllHotels(cluster)); 60 | } catch (Exception e) { 61 | LOGGER.error(LOG_FAILURE_MESSAGE, e); 62 | return ResponseEntity.badRequest().body(new Error(e.getMessage())); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/trycb/web/IndexController.java: -------------------------------------------------------------------------------- 1 | package trycb.web; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestMethod; 6 | import org.springframework.web.bind.annotation.ResponseBody; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import trycb.service.Index; 10 | 11 | @RestController 12 | public class IndexController { 13 | 14 | @RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE) 15 | @ResponseBody 16 | public String index() { 17 | return Index.getInfo(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/trycb/web/TenantUserController.java: -------------------------------------------------------------------------------- 1 | package trycb.web; 2 | 3 | import java.util.Map; 4 | 5 | import com.couchbase.client.core.msg.kv.DurabilityLevel; 6 | import com.couchbase.client.java.Bucket; 7 | import com.couchbase.client.java.json.JsonObject; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.security.authentication.AuthenticationServiceException; 16 | import org.springframework.security.core.AuthenticationException; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestBody; 19 | import org.springframework.web.bind.annotation.RequestHeader; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import trycb.model.Error; 25 | import trycb.model.IValue; 26 | import trycb.model.Result; 27 | import trycb.service.TenantUser; 28 | import trycb.service.TokenService; 29 | 30 | @RestController 31 | @RequestMapping("/api/tenants") 32 | public class TenantUserController { 33 | 34 | private final Bucket bucket; 35 | private final TenantUser tenantUserService; 36 | private final TokenService jwtService; 37 | 38 | private static final Logger LOGGER = LoggerFactory.getLogger(TenantUserController.class); 39 | 40 | @Value("${storage.expiry:0}") 41 | private int expiry; 42 | 43 | @Autowired 44 | public TenantUserController(Bucket bucket, TenantUser tenantUserService, TokenService jwtService) { 45 | this.bucket = bucket; 46 | this.tenantUserService = tenantUserService; 47 | this.jwtService = jwtService; 48 | } 49 | 50 | @RequestMapping(value = "/{tenant}/user/login", method = RequestMethod.POST) 51 | public ResponseEntity login(@PathVariable("tenant") String tenant, 52 | @RequestBody Map loginInfo) { 53 | String user = loginInfo.get("user"); 54 | String password = loginInfo.get("password"); 55 | if (user == null || password == null) { 56 | return ResponseEntity.badRequest().body(new Error("User or password missing, or malformed request")); 57 | } 58 | 59 | try { 60 | return ResponseEntity.ok(tenantUserService.login(bucket, tenant, user, password)); 61 | } catch (AuthenticationException e) { 62 | LOGGER.error("Authentication failed with exception", e); 63 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new Error(e.getMessage())); 64 | } catch (Exception e) { 65 | LOGGER.error("Failed with exception", e); 66 | return ResponseEntity.status(500).body(new Error(e.getMessage())); 67 | } 68 | } 69 | 70 | @RequestMapping(value = "/{tenant}/user/signup", method = RequestMethod.POST) 71 | public ResponseEntity createLogin(@PathVariable("tenant") String tenant, 72 | @RequestBody String json) { 73 | JsonObject jsonData = JsonObject.fromJson(json); 74 | try { 75 | Result> result = tenantUserService.createLogin(bucket, tenant, 76 | jsonData.getString("user"), jsonData.getString("password"), DurabilityLevel.values()[expiry]); 77 | return ResponseEntity.status(HttpStatus.CREATED).body(result); 78 | } catch (AuthenticationServiceException e) { 79 | LOGGER.error("Authentication failed with exception", e); 80 | return ResponseEntity.status(HttpStatus.CONFLICT).body(new Error(e.getMessage())); 81 | } catch (Exception e) { 82 | LOGGER.error("Failed with exception", e); 83 | return ResponseEntity.status(500).body(new Error(e.getMessage())); 84 | } 85 | } 86 | 87 | @RequestMapping(value = "/{tenant}/user/{username}/flights", method = RequestMethod.PUT) 88 | public ResponseEntity book(@PathVariable("tenant") String tenant, 89 | @PathVariable("username") String username, @RequestBody String json, 90 | @RequestHeader("Authorization") String authentication) { 91 | if (authentication == null || !authentication.startsWith("Bearer ")) { 92 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new Error("Bearer Authentication must be used")); 93 | } 94 | JsonObject jsonData = JsonObject.fromJson(json); 95 | try { 96 | jwtService.verifyAuthenticationHeader(authentication, username); 97 | Result> result = tenantUserService.registerFlightForUser(bucket, tenant, username, 98 | jsonData.getArray("flights")); 99 | return ResponseEntity.ok().body(result); 100 | } catch (IllegalStateException e) { 101 | LOGGER.error("Failed with invalid state exception", e); 102 | return ResponseEntity.status(HttpStatus.FORBIDDEN) 103 | .body(new Error("Forbidden, you can't book for this user")); 104 | } catch (IllegalArgumentException e) { 105 | LOGGER.error("Failed with invalid argument exception", e); 106 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new Error(e.getMessage())); 107 | } 108 | } 109 | 110 | @RequestMapping(value = "/{tenant}/user/{username}/flights", method = RequestMethod.GET) 111 | public Object booked(@PathVariable("tenant") String tenant, @PathVariable("username") String username, 112 | @RequestHeader("Authorization") String authentication) { 113 | if (authentication == null || !authentication.startsWith("Bearer ")) { 114 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new Error("Bearer Authentication must be used")); 115 | } 116 | 117 | try { 118 | jwtService.verifyAuthenticationHeader(authentication, username); 119 | return ResponseEntity.ok(tenantUserService.getFlightsForUser(bucket, tenant, username)); 120 | } catch (IllegalStateException e) { 121 | LOGGER.error("Failed with invalid state exception", e); 122 | return ResponseEntity.status(HttpStatus.FORBIDDEN) 123 | .body(new Error("Forbidden, you don't have access to this cart")); 124 | } catch (IllegalArgumentException e) { 125 | LOGGER.error("Failed with invalid argument exception", e); 126 | return ResponseEntity.status(HttpStatus.FORBIDDEN) 127 | .body(new Error("Forbidden, you don't have access to this cart")); 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | jwt.secret=UNSECURE_SECRET_TOKEN 2 | jwt.enabled=true 3 | storage.host=db 4 | storage.bucket=travel-sample 5 | storage.username=Administrator 6 | storage.password=password 7 | #in seconds, set to 0 to disable 8 | storage.expiry=0 9 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 10 | springdoc.swagger-ui.url=/swagger.json 11 | springdoc.swagger-ui.path=/apidocs 12 | -------------------------------------------------------------------------------- /src/main/resources/static/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "schemas": { 4 | "Context": { 5 | "items": { 6 | "type": "string" 7 | }, 8 | "type": "array" 9 | }, 10 | "Error": { 11 | "properties": { 12 | "message": { 13 | "example": "An error message", 14 | "type": "string" 15 | } 16 | }, 17 | "type": "object" 18 | }, 19 | "ResultList": { 20 | "properties": { 21 | "context": { 22 | "$ref": "#/components/schemas/Context" 23 | }, 24 | "data": { 25 | "items": { 26 | "type": "object" 27 | }, 28 | "type": "array" 29 | } 30 | }, 31 | "type": "object" 32 | }, 33 | "ResultSingleton": { 34 | "properties": { 35 | "context": { 36 | "$ref": "#/components/schemas/Context" 37 | }, 38 | "data": { 39 | "type": "object" 40 | } 41 | }, 42 | "type": "object" 43 | } 44 | }, 45 | "securitySchemes": { 46 | "bearer": { 47 | "bearerFormat": "JWT", 48 | "description": "JWT Authorization header using the Bearer scheme.", 49 | "scheme": "bearer", 50 | "type": "http" 51 | } 52 | } 53 | }, 54 | "definitions": {}, 55 | "info": { 56 | "description": "A sample API for getting started with Couchbase Server and the SDK.", 57 | "termsOfService": "", 58 | "title": "Travel Sample API", 59 | "version": "1.0" 60 | }, 61 | "openapi": "3.0.3", 62 | "paths": { 63 | "/": { 64 | "get": { 65 | "responses": { 66 | "200": { 67 | "content": { 68 | "text/html": { 69 | "example": "

Travel Sample API

" 70 | } 71 | }, 72 | "description": "Returns the API index page" 73 | } 74 | }, 75 | "summary": "Returns the index page" 76 | } 77 | }, 78 | "/api/airports": { 79 | "get": { 80 | "parameters": [ 81 | { 82 | "description": "The airport name/code to search for", 83 | "example": "SFO", 84 | "in": "query", 85 | "name": "search", 86 | "required": true, 87 | "schema": { 88 | "type": "string" 89 | } 90 | } 91 | ], 92 | "responses": { 93 | "200": { 94 | "content": { 95 | "application/json": { 96 | "example": { 97 | "context": [ 98 | "A description of a N1QL operation" 99 | ], 100 | "data": [ 101 | { 102 | "airportname": "San Francisco Intl" 103 | } 104 | ] 105 | }, 106 | "schema": { 107 | "$ref": "#/components/schemas/ResultList" 108 | } 109 | } 110 | }, 111 | "description": "Returns airport data and query context information" 112 | } 113 | }, 114 | "summary": "Returns list of matching airports and the source query", 115 | "tags": [ 116 | "airports" 117 | ] 118 | } 119 | }, 120 | "/api/flightPaths/{fromloc}/{toloc}": { 121 | "get": { 122 | "parameters": [ 123 | { 124 | "description": "Airport name for beginning route", 125 | "example": "San Francisco Intl", 126 | "in": "path", 127 | "name": "fromloc", 128 | "required": true, 129 | "schema": { 130 | "type": "string" 131 | } 132 | }, 133 | { 134 | "description": "Airport name for end route", 135 | "example": "Los Angeles Intl", 136 | "in": "path", 137 | "name": "toloc", 138 | "required": true, 139 | "schema": { 140 | "type": "string" 141 | } 142 | }, 143 | { 144 | "description": "Date of flight departure in `mm/dd/yyyy` format", 145 | "example": "05/24/2021", 146 | "in": "query", 147 | "name": "leave", 148 | "required": true, 149 | "schema": { 150 | "format": "date", 151 | "type": "string" 152 | } 153 | } 154 | ], 155 | "responses": { 156 | "200": { 157 | "content": { 158 | "application/json": { 159 | "example": { 160 | "context": [ 161 | "N1QL query - scoped to inventory: SELECT faa as fromAirport FROM `travel-sample`.inventory.airport WHERE airportname = $1 UNION SELECT faa as toAirport FROM `travel-sample`.inventory.airport WHERE airportname = $2" 162 | ], 163 | "data": [ 164 | { 165 | "destinationairport": "LAX", 166 | "equipment": "738", 167 | "flight": "AA331", 168 | "flighttime": 1220, 169 | "name": "American Airlines", 170 | "price": 152.5, 171 | "sourceairport": "SFO", 172 | "utc": "16:37:00" 173 | } 174 | ] 175 | }, 176 | "schema": { 177 | "$ref": "#/components/schemas/ResultList" 178 | } 179 | } 180 | }, 181 | "description": "Returns flight data and query context information" 182 | } 183 | }, 184 | "summary": "Return flights information, cost and more for a given flight time and date", 185 | "tags": [ 186 | "flightPaths" 187 | ] 188 | } 189 | }, 190 | "/api/hotels/{description}/{location}/": { 191 | "get": { 192 | "parameters": [ 193 | { 194 | "description": "Hotel description keywords", 195 | "example": "pool", 196 | "in": "path", 197 | "name": "description", 198 | "required": false, 199 | "schema": { 200 | "type": "string" 201 | } 202 | }, 203 | { 204 | "description": "Hotel location", 205 | "example": "San Francisco", 206 | "in": "path", 207 | "name": "location", 208 | "required": false, 209 | "schema": { 210 | "type": "string" 211 | } 212 | } 213 | ], 214 | "responses": { 215 | "200": { 216 | "content": { 217 | "application/json": { 218 | "example": { 219 | "context": [ 220 | "FTS search - scoped to: inventory.hotel within fields address,city,state,country,name,description" 221 | ], 222 | "data": [ 223 | { 224 | "address": "250 Beach St, San Francisco, California, United States", 225 | "description": "Nice hotel, centrally located (only two blocks from Pier 39). Heated outdoor swimming pool.", 226 | "name": "Radisson Hotel Fisherman's Wharf" 227 | }, 228 | { 229 | "address": "121 7th St, San Francisco, California, United States", 230 | "description": "Chain motel with a few more amenities than the typical Best Western; outdoor swimming pool, internet access, cafe on-site, pet friendly.", 231 | "name": "Best Western Americania" 232 | } 233 | ] 234 | }, 235 | "schema": { 236 | "$ref": "#/components/schemas/ResultList" 237 | } 238 | } 239 | }, 240 | "description": "Returns hotel data and query context information" 241 | } 242 | }, 243 | "summary": "Find hotels using full text search", 244 | "tags": [ 245 | "hotels" 246 | ] 247 | } 248 | }, 249 | "/api/tenants/{tenant}/user/login": { 250 | "post": { 251 | "parameters": [ 252 | { 253 | "description": "Tenant agent name", 254 | "example": "tenant_agent_00", 255 | "in": "path", 256 | "name": "tenant", 257 | "required": true, 258 | "schema": { 259 | "type": "string" 260 | } 261 | } 262 | ], 263 | "requestBody": { 264 | "content": { 265 | "application/json": { 266 | "schema": { 267 | "properties": { 268 | "password": { 269 | "example": "password1", 270 | "type": "string" 271 | }, 272 | "user": { 273 | "example": "user1", 274 | "type": "string" 275 | } 276 | }, 277 | "required": [ 278 | "user", 279 | "password" 280 | ], 281 | "type": "object" 282 | } 283 | } 284 | } 285 | }, 286 | "responses": { 287 | "200": { 288 | "content": { 289 | "application/json": { 290 | "example": { 291 | "context": [ 292 | "KV get - scoped to tenant_agent_00.users: for password field in document user1" 293 | ], 294 | "data": { 295 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 296 | } 297 | }, 298 | "schema": { 299 | "$ref": "#/components/schemas/ResultSingleton" 300 | } 301 | } 302 | }, 303 | "description": "Returns login data and query context information" 304 | }, 305 | "401": { 306 | "content": { 307 | "application/json": { 308 | "schema": { 309 | "$ref": "#/components/schemas/Error" 310 | } 311 | } 312 | }, 313 | "description": "Returns an authentication error" 314 | } 315 | }, 316 | "summary": "Login an existing user for a given tenant agent", 317 | "tags": [ 318 | "tenants" 319 | ] 320 | } 321 | }, 322 | "/api/tenants/{tenant}/user/signup": { 323 | "post": { 324 | "parameters": [ 325 | { 326 | "description": "Tenant agent name", 327 | "example": "tenant_agent_00", 328 | "in": "path", 329 | "name": "tenant", 330 | "required": true, 331 | "schema": { 332 | "type": "string" 333 | } 334 | } 335 | ], 336 | "requestBody": { 337 | "content": { 338 | "application/json": { 339 | "schema": { 340 | "properties": { 341 | "password": { 342 | "example": "password1", 343 | "type": "string" 344 | }, 345 | "user": { 346 | "example": "user1", 347 | "type": "string" 348 | } 349 | }, 350 | "required": [ 351 | "user", 352 | "password" 353 | ], 354 | "type": "object" 355 | } 356 | } 357 | } 358 | }, 359 | "responses": { 360 | "201": { 361 | "content": { 362 | "application/json": { 363 | "example": { 364 | "context": [ 365 | "KV insert - scoped to tenant_agent_00.users: document user1" 366 | ], 367 | "data": { 368 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 369 | } 370 | }, 371 | "schema": { 372 | "$ref": "#/components/schemas/ResultSingleton" 373 | } 374 | } 375 | }, 376 | "description": "Returns login data and query context information" 377 | }, 378 | "409": { 379 | "content": { 380 | "application/json": { 381 | "schema": { 382 | "$ref": "#/components/schemas/Error" 383 | } 384 | } 385 | }, 386 | "description": "Returns a conflict error" 387 | } 388 | }, 389 | "summary": "Signup a new user", 390 | "tags": [ 391 | "tenants" 392 | ] 393 | } 394 | }, 395 | "/api/tenants/{tenant}/user/{username}/flights": { 396 | "get": { 397 | "parameters": [ 398 | { 399 | "description": "Tenant agent name", 400 | "example": "tenant_agent_00", 401 | "in": "path", 402 | "name": "tenant", 403 | "required": true, 404 | "schema": { 405 | "type": "string" 406 | } 407 | }, 408 | { 409 | "description": "Username", 410 | "example": "user1", 411 | "in": "path", 412 | "name": "username", 413 | "required": true, 414 | "schema": { 415 | "type": "string" 416 | } 417 | } 418 | ], 419 | "responses": { 420 | "200": { 421 | "content": { 422 | "application/json": { 423 | "example": { 424 | "context": [ 425 | "KV get - scoped to tenant_agent_00.user: for 2 bookings in document user1" 426 | ], 427 | "data": [ 428 | { 429 | "date": "05/24/2021", 430 | "destinationairport": "LAX", 431 | "equipment": "738", 432 | "flight": "AA655", 433 | "flighttime": 5383, 434 | "name": "American Airlines", 435 | "price": 672.88, 436 | "sourceairport": "SFO", 437 | "utc": "11:42:00" 438 | }, 439 | { 440 | "date": "05/28/2021", 441 | "destinationairport": "SFO", 442 | "equipment": "738", 443 | "flight": "AA344", 444 | "flighttime": 6081, 445 | "name": "American Airlines", 446 | "price": 760.13, 447 | "sourceairport": "LAX", 448 | "utc": "20:47:00" 449 | } 450 | ] 451 | }, 452 | "schema": { 453 | "$ref": "#/components/schemas/ResultList" 454 | } 455 | } 456 | }, 457 | "description": "Returns flight data and query context information" 458 | }, 459 | "401": { 460 | "content": { 461 | "application/json": { 462 | "schema": { 463 | "$ref": "#/components/schemas/Error" 464 | } 465 | } 466 | }, 467 | "description": "Returns an authentication error" 468 | } 469 | }, 470 | "security": [ 471 | { 472 | "bearer": [] 473 | } 474 | ], 475 | "summary": "List the flights that have been reserved by a user", 476 | "tags": [ 477 | "tenants" 478 | ] 479 | }, 480 | "put": { 481 | "parameters": [ 482 | { 483 | "description": "Tenant agent name", 484 | "example": "tenant_agent_00", 485 | "in": "path", 486 | "name": "tenant", 487 | "required": true, 488 | "schema": { 489 | "type": "string" 490 | } 491 | }, 492 | { 493 | "description": "Username", 494 | "example": "user1", 495 | "in": "path", 496 | "name": "username", 497 | "required": true, 498 | "schema": { 499 | "type": "string" 500 | } 501 | } 502 | ], 503 | "requestBody": { 504 | "content": { 505 | "application/json": { 506 | "schema": { 507 | "properties": { 508 | "flights": { 509 | "example": [ 510 | { 511 | "date": "12/12/2020", 512 | "destinationairport": "Leonardo Da Vinci International Airport", 513 | "flight": "12RF", 514 | "name": "boeing", 515 | "price": 50.0, 516 | "sourceairport": "London (Gatwick)" 517 | } 518 | ], 519 | "format": "string", 520 | "type": "array" 521 | } 522 | }, 523 | "type": "object" 524 | } 525 | } 526 | } 527 | }, 528 | "responses": { 529 | "200": { 530 | "content": { 531 | "application/json": { 532 | "example": { 533 | "context": [ 534 | "KV update - scoped to tenant_agent_00.user: for bookings field in document user1" 535 | ], 536 | "data": { 537 | "added": [ 538 | { 539 | "date": "12/12/2020", 540 | "destinationairport": "Leonardo Da Vinci International Airport", 541 | "flight": "12RF", 542 | "name": "boeing", 543 | "price": 50.0, 544 | "sourceairport": "London (Gatwick)" 545 | } 546 | ] 547 | } 548 | }, 549 | "schema": { 550 | "$ref": "#/components/schemas/ResultSingleton" 551 | } 552 | } 553 | }, 554 | "description": "Returns flight data and query context information" 555 | }, 556 | "401": { 557 | "content": { 558 | "application/json": { 559 | "schema": { 560 | "$ref": "#/components/schemas/Error" 561 | } 562 | } 563 | }, 564 | "description": "Returns an authentication error" 565 | } 566 | }, 567 | "security": [ 568 | { 569 | "bearer": [] 570 | } 571 | ], 572 | "summary": "Book a new flight for a user", 573 | "tags": [ 574 | "tenants" 575 | ] 576 | } 577 | } 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "schemas": { 4 | "Context": { 5 | "items": { 6 | "type": "string" 7 | }, 8 | "type": "array" 9 | }, 10 | "Error": { 11 | "properties": { 12 | "message": { 13 | "example": "An error message", 14 | "type": "string" 15 | } 16 | }, 17 | "type": "object" 18 | }, 19 | "ResultList": { 20 | "properties": { 21 | "context": { 22 | "$ref": "#/components/schemas/Context" 23 | }, 24 | "data": { 25 | "items": { 26 | "type": "object" 27 | }, 28 | "type": "array" 29 | } 30 | }, 31 | "type": "object" 32 | }, 33 | "ResultSingleton": { 34 | "properties": { 35 | "context": { 36 | "$ref": "#/components/schemas/Context" 37 | }, 38 | "data": { 39 | "type": "object" 40 | } 41 | }, 42 | "type": "object" 43 | } 44 | }, 45 | "securitySchemes": { 46 | "bearer": { 47 | "bearerFormat": "JWT", 48 | "description": "JWT Authorization header using the Bearer scheme.", 49 | "scheme": "bearer", 50 | "type": "http" 51 | } 52 | } 53 | }, 54 | "definitions": {}, 55 | "info": { 56 | "description": "A sample API for getting started with Couchbase Server and the SDK.", 57 | "termsOfService": "", 58 | "title": "Travel Sample API", 59 | "version": "1.0" 60 | }, 61 | "openapi": "3.0.3", 62 | "paths": { 63 | "/": { 64 | "get": { 65 | "responses": { 66 | "200": { 67 | "content": { 68 | "text/html": { 69 | "example": "

Travel Sample API

" 70 | } 71 | }, 72 | "description": "Returns the API index page" 73 | } 74 | }, 75 | "summary": "Returns the index page" 76 | } 77 | }, 78 | "/api/airports": { 79 | "get": { 80 | "parameters": [ 81 | { 82 | "description": "The airport name/code to search for", 83 | "example": "SFO", 84 | "in": "query", 85 | "name": "search", 86 | "required": true, 87 | "schema": { 88 | "type": "string" 89 | } 90 | } 91 | ], 92 | "responses": { 93 | "200": { 94 | "content": { 95 | "application/json": { 96 | "example": { 97 | "context": [ 98 | "A description of a N1QL operation" 99 | ], 100 | "data": [ 101 | { 102 | "airportname": "San Francisco Intl" 103 | } 104 | ] 105 | }, 106 | "schema": { 107 | "$ref": "#/components/schemas/ResultList" 108 | } 109 | } 110 | }, 111 | "description": "Returns airport data and query context information" 112 | } 113 | }, 114 | "summary": "Returns list of matching airports and the source query", 115 | "tags": [ 116 | "airports" 117 | ] 118 | } 119 | }, 120 | "/api/flightPaths/{fromloc}/{toloc}": { 121 | "get": { 122 | "parameters": [ 123 | { 124 | "description": "Airport name for beginning route", 125 | "example": "San Francisco Intl", 126 | "in": "path", 127 | "name": "fromloc", 128 | "required": true, 129 | "schema": { 130 | "type": "string" 131 | } 132 | }, 133 | { 134 | "description": "Airport name for end route", 135 | "example": "Los Angeles Intl", 136 | "in": "path", 137 | "name": "toloc", 138 | "required": true, 139 | "schema": { 140 | "type": "string" 141 | } 142 | }, 143 | { 144 | "description": "Date of flight departure in `mm/dd/yyyy` format", 145 | "example": "05/24/2021", 146 | "in": "query", 147 | "name": "leave", 148 | "required": true, 149 | "schema": { 150 | "format": "date", 151 | "type": "string" 152 | } 153 | } 154 | ], 155 | "responses": { 156 | "200": { 157 | "content": { 158 | "application/json": { 159 | "example": { 160 | "context": [ 161 | "N1QL query - scoped to inventory: SELECT faa as fromAirport FROM `travel-sample`.inventory.airport WHERE airportname = $1 UNION SELECT faa as toAirport FROM `travel-sample`.inventory.airport WHERE airportname = $2" 162 | ], 163 | "data": [ 164 | { 165 | "destinationairport": "LAX", 166 | "equipment": "738", 167 | "flight": "AA331", 168 | "flighttime": 1220, 169 | "name": "American Airlines", 170 | "price": 152.5, 171 | "sourceairport": "SFO", 172 | "utc": "16:37:00" 173 | } 174 | ] 175 | }, 176 | "schema": { 177 | "$ref": "#/components/schemas/ResultList" 178 | } 179 | } 180 | }, 181 | "description": "Returns flight data and query context information" 182 | } 183 | }, 184 | "summary": "Return flights information, cost and more for a given flight time and date", 185 | "tags": [ 186 | "flightPaths" 187 | ] 188 | } 189 | }, 190 | "/api/hotels/{description}/{location}/": { 191 | "get": { 192 | "parameters": [ 193 | { 194 | "description": "Hotel description keywords", 195 | "example": "pool", 196 | "in": "path", 197 | "name": "description", 198 | "required": false, 199 | "schema": { 200 | "type": "string" 201 | } 202 | }, 203 | { 204 | "description": "Hotel location", 205 | "example": "San Francisco", 206 | "in": "path", 207 | "name": "location", 208 | "required": false, 209 | "schema": { 210 | "type": "string" 211 | } 212 | } 213 | ], 214 | "responses": { 215 | "200": { 216 | "content": { 217 | "application/json": { 218 | "example": { 219 | "context": [ 220 | "FTS search - scoped to: inventory.hotel within fields address,city,state,country,name,description" 221 | ], 222 | "data": [ 223 | { 224 | "address": "250 Beach St, San Francisco, California, United States", 225 | "description": "Nice hotel, centrally located (only two blocks from Pier 39). Heated outdoor swimming pool.", 226 | "name": "Radisson Hotel Fisherman's Wharf" 227 | }, 228 | { 229 | "address": "121 7th St, San Francisco, California, United States", 230 | "description": "Chain motel with a few more amenities than the typical Best Western; outdoor swimming pool, internet access, cafe on-site, pet friendly.", 231 | "name": "Best Western Americania" 232 | } 233 | ] 234 | }, 235 | "schema": { 236 | "$ref": "#/components/schemas/ResultList" 237 | } 238 | } 239 | }, 240 | "description": "Returns hotel data and query context information" 241 | } 242 | }, 243 | "summary": "Find hotels using full text search", 244 | "tags": [ 245 | "hotels" 246 | ] 247 | } 248 | }, 249 | "/api/tenants/{tenant}/user/login": { 250 | "post": { 251 | "parameters": [ 252 | { 253 | "description": "Tenant agent name", 254 | "example": "tenant_agent_00", 255 | "in": "path", 256 | "name": "tenant", 257 | "required": true, 258 | "schema": { 259 | "type": "string" 260 | } 261 | } 262 | ], 263 | "requestBody": { 264 | "content": { 265 | "application/json": { 266 | "schema": { 267 | "properties": { 268 | "password": { 269 | "example": "password1", 270 | "type": "string" 271 | }, 272 | "user": { 273 | "example": "user1", 274 | "type": "string" 275 | } 276 | }, 277 | "required": [ 278 | "user", 279 | "password" 280 | ], 281 | "type": "object" 282 | } 283 | } 284 | } 285 | }, 286 | "responses": { 287 | "200": { 288 | "content": { 289 | "application/json": { 290 | "example": { 291 | "context": [ 292 | "KV get - scoped to tenant_agent_00.users: for password field in document user1" 293 | ], 294 | "data": { 295 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 296 | } 297 | }, 298 | "schema": { 299 | "$ref": "#/components/schemas/ResultSingleton" 300 | } 301 | } 302 | }, 303 | "description": "Returns login data and query context information" 304 | }, 305 | "401": { 306 | "content": { 307 | "application/json": { 308 | "schema": { 309 | "$ref": "#/components/schemas/Error" 310 | } 311 | } 312 | }, 313 | "description": "Returns an authentication error" 314 | } 315 | }, 316 | "summary": "Login an existing user for a given tenant agent", 317 | "tags": [ 318 | "tenants" 319 | ] 320 | } 321 | }, 322 | "/api/tenants/{tenant}/user/signup": { 323 | "post": { 324 | "parameters": [ 325 | { 326 | "description": "Tenant agent name", 327 | "example": "tenant_agent_00", 328 | "in": "path", 329 | "name": "tenant", 330 | "required": true, 331 | "schema": { 332 | "type": "string" 333 | } 334 | } 335 | ], 336 | "requestBody": { 337 | "content": { 338 | "application/json": { 339 | "schema": { 340 | "properties": { 341 | "password": { 342 | "example": "password1", 343 | "type": "string" 344 | }, 345 | "user": { 346 | "example": "user1", 347 | "type": "string" 348 | } 349 | }, 350 | "required": [ 351 | "user", 352 | "password" 353 | ], 354 | "type": "object" 355 | } 356 | } 357 | } 358 | }, 359 | "responses": { 360 | "201": { 361 | "content": { 362 | "application/json": { 363 | "example": { 364 | "context": [ 365 | "KV insert - scoped to tenant_agent_00.users: document user1" 366 | ], 367 | "data": { 368 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 369 | } 370 | }, 371 | "schema": { 372 | "$ref": "#/components/schemas/ResultSingleton" 373 | } 374 | } 375 | }, 376 | "description": "Returns login data and query context information" 377 | }, 378 | "409": { 379 | "content": { 380 | "application/json": { 381 | "schema": { 382 | "$ref": "#/components/schemas/Error" 383 | } 384 | } 385 | }, 386 | "description": "Returns a conflict error" 387 | } 388 | }, 389 | "summary": "Signup a new user", 390 | "tags": [ 391 | "tenants" 392 | ] 393 | } 394 | }, 395 | "/api/tenants/{tenant}/user/{username}/flights": { 396 | "get": { 397 | "parameters": [ 398 | { 399 | "description": "Tenant agent name", 400 | "example": "tenant_agent_00", 401 | "in": "path", 402 | "name": "tenant", 403 | "required": true, 404 | "schema": { 405 | "type": "string" 406 | } 407 | }, 408 | { 409 | "description": "Username", 410 | "example": "user1", 411 | "in": "path", 412 | "name": "username", 413 | "required": true, 414 | "schema": { 415 | "type": "string" 416 | } 417 | } 418 | ], 419 | "responses": { 420 | "200": { 421 | "content": { 422 | "application/json": { 423 | "example": { 424 | "context": [ 425 | "KV get - scoped to tenant_agent_00.user: for 2 bookings in document user1" 426 | ], 427 | "data": [ 428 | { 429 | "date": "05/24/2021", 430 | "destinationairport": "LAX", 431 | "equipment": "738", 432 | "flight": "AA655", 433 | "flighttime": 5383, 434 | "name": "American Airlines", 435 | "price": 672.88, 436 | "sourceairport": "SFO", 437 | "utc": "11:42:00" 438 | }, 439 | { 440 | "date": "05/28/2021", 441 | "destinationairport": "SFO", 442 | "equipment": "738", 443 | "flight": "AA344", 444 | "flighttime": 6081, 445 | "name": "American Airlines", 446 | "price": 760.13, 447 | "sourceairport": "LAX", 448 | "utc": "20:47:00" 449 | } 450 | ] 451 | }, 452 | "schema": { 453 | "$ref": "#/components/schemas/ResultList" 454 | } 455 | } 456 | }, 457 | "description": "Returns flight data and query context information" 458 | }, 459 | "401": { 460 | "content": { 461 | "application/json": { 462 | "schema": { 463 | "$ref": "#/components/schemas/Error" 464 | } 465 | } 466 | }, 467 | "description": "Returns an authentication error" 468 | } 469 | }, 470 | "security": [ 471 | { 472 | "bearer": [] 473 | } 474 | ], 475 | "summary": "List the flights that have been reserved by a user", 476 | "tags": [ 477 | "tenants" 478 | ] 479 | }, 480 | "put": { 481 | "parameters": [ 482 | { 483 | "description": "Tenant agent name", 484 | "example": "tenant_agent_00", 485 | "in": "path", 486 | "name": "tenant", 487 | "required": true, 488 | "schema": { 489 | "type": "string" 490 | } 491 | }, 492 | { 493 | "description": "Username", 494 | "example": "user1", 495 | "in": "path", 496 | "name": "username", 497 | "required": true, 498 | "schema": { 499 | "type": "string" 500 | } 501 | } 502 | ], 503 | "requestBody": { 504 | "content": { 505 | "application/json": { 506 | "schema": { 507 | "properties": { 508 | "flights": { 509 | "example": [ 510 | { 511 | "date": "12/12/2020", 512 | "destinationairport": "Leonardo Da Vinci International Airport", 513 | "flight": "12RF", 514 | "name": "boeing", 515 | "price": 50.0, 516 | "sourceairport": "London (Gatwick)" 517 | } 518 | ], 519 | "format": "string", 520 | "type": "array" 521 | } 522 | }, 523 | "type": "object" 524 | } 525 | } 526 | } 527 | }, 528 | "responses": { 529 | "200": { 530 | "content": { 531 | "application/json": { 532 | "example": { 533 | "context": [ 534 | "KV update - scoped to tenant_agent_00.user: for bookings field in document user1" 535 | ], 536 | "data": { 537 | "added": [ 538 | { 539 | "date": "12/12/2020", 540 | "destinationairport": "Leonardo Da Vinci International Airport", 541 | "flight": "12RF", 542 | "name": "boeing", 543 | "price": 50.0, 544 | "sourceairport": "London (Gatwick)" 545 | } 546 | ] 547 | } 548 | }, 549 | "schema": { 550 | "$ref": "#/components/schemas/ResultSingleton" 551 | } 552 | } 553 | }, 554 | "description": "Returns flight data and query context information" 555 | }, 556 | "401": { 557 | "content": { 558 | "application/json": { 559 | "schema": { 560 | "$ref": "#/components/schemas/Error" 561 | } 562 | } 563 | }, 564 | "description": "Returns an authentication error" 565 | } 566 | }, 567 | "security": [ 568 | { 569 | "bearer": [] 570 | } 571 | ], 572 | "summary": "Book a new flight for a user", 573 | "tags": [ 574 | "tenants" 575 | ] 576 | } 577 | } 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /update-swagger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GH_USER=${GH_USER:-couchbaselabs} 4 | GH_PROJECT=${GH_PROJECT:-try-cb-python} 5 | GH_BRANCH=${GH_BRANCH:-7.0} 6 | 7 | URL=https://raw.githubusercontent.com/${GH_USER}/${GH_PROJECT}/${GH_BRANCH}/swagger.json 8 | echo "Getting $URL ..." 9 | curl -S --fail -O $URL 10 | 11 | if [ $? -eq 0 ]; then 12 | echo swagger.json retrieved: 13 | git diff --exit-code ./src/main/resources/static/swagger.json && echo "No changes!" 14 | else 15 | echo "couldn't retrieve swagger.json" 16 | fi 17 | -------------------------------------------------------------------------------- /wait-for-couchbase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # wait-for-couchbase.sh 3 | 4 | set -e 5 | 6 | CB_HOST="${CB_HOST:-db}" 7 | CB_USER="${CB_USER:-Administrator}" 8 | CB_PSWD="${CB_PSWD:-password}" 9 | 10 | #### Utility Functions #### 11 | # (see bottom of file for the calling script) 12 | 13 | log() { 14 | echo "wait-for-couchbase: $@" | cut -c -${COLUMNS:-80} 15 | } 16 | 17 | wait-for-one() { 18 | local ATTEMPTS=$1 19 | local URL=$2 20 | local QUERY=$3 21 | local EXPECTED=true 22 | local OUT=wait-for-couchbase.out 23 | 24 | for attempt in $(seq 1 $ATTEMPTS); do 25 | status=$(curl -s -w "%{http_code}" -o $OUT -u "${CB_USER}:${CB_PSWD}" $URL) 26 | if [ $attempt -eq 1 ]; then 27 | log "polling for '$QUERY'" 28 | if [ $DEBUG ]; then jq . $OUT; fi 29 | elif (($attempt % 5 == 0)); then 30 | log "..." 31 | fi 32 | if [ "x$status" == "x200" ]; then 33 | result=$(jq "$QUERY" <$OUT) 34 | if [ "x$result" == "x$EXPECTED" ]; then 35 | return # success 36 | fi 37 | if [ $attempt -eq 1 ]; then 38 | log "value is currently:" 39 | jq . <<<"$result" 40 | fi 41 | fi 42 | sleep 2 43 | done 44 | return 1 # failure 45 | } 46 | 47 | wait-for() { 48 | local ATTEMPTS=$1 49 | local URL="http://${CB_HOST}${2}" 50 | shift 51 | shift 52 | 53 | log "checking $URL" 54 | 55 | for QUERY in "$@"; do 56 | wait-for-one $ATTEMPTS $URL "$QUERY" || ( 57 | log "Failure" 58 | exit 1 59 | ) 60 | done 61 | return # success 62 | } 63 | 64 | function createHotelsIndex() { 65 | log "Creating hotels-index..." 66 | http_code=$(curl -o hotel-index.out -w '%{http_code}' -s -u ${CB_USER}:${CB_PSWD} -X PUT \ 67 | http://${CB_HOST}:8094/api/index/hotels-index \ 68 | -H 'cache-control: no-cache' \ 69 | -H 'content-type: application/json' \ 70 | -d @fts-hotels-index.json) 71 | if [[ $http_code -ne 200 ]]; then 72 | log Hotel index creation failed 73 | cat hotel-index.out 74 | exit 1 75 | fi 76 | } 77 | 78 | ##### Script starts here ##### 79 | ATTEMPTS=150 80 | 81 | wait-for $ATTEMPTS \ 82 | ":8091/pools/default/buckets/travel-sample/scopes/" \ 83 | '.scopes | map(.name) | contains(["inventory", "tenant_agent_00", "tenant_agent_01"])' 84 | 85 | wait-for $ATTEMPTS \ 86 | ":8094/api/cfg" \ 87 | '.status == "ok"' 88 | 89 | if (wait-for 1 ":8094/api/index/hotels-index" '.status == "ok"'); then 90 | log "index already exists" 91 | else 92 | createHotelsIndex 93 | wait-for $ATTEMPTS \ 94 | ":8094/api/index/hotels-index/count" \ 95 | '.count >= 917' 96 | fi 97 | 98 | # now check that the indexes have had enough time to come up... 99 | wait-for $ATTEMPTS \ 100 | ":9102/api/v1/stats" \ 101 | '.indexer.indexer_state == "Active"' \ 102 | '. | keys | contains(["travel-sample:def_airportname", "travel-sample:def_city", "travel-sample:def_faa", "travel-sample:def_icao", "travel-sample:def_name_type", "travel-sample:def_primary", "travel-sample:def_route_src_dst_day", "travel-sample:def_schedule_utc", "travel-sample:def_sourceairport", "travel-sample:def_type", "travel-sample:inventory:airline:def_inventory_airline_primary", "travel-sample:inventory:airport:def_inventory_airport_airportname", "travel-sample:inventory:airport:def_inventory_airport_city", "travel-sample:inventory:airport:def_inventory_airport_faa", "travel-sample:inventory:airport:def_inventory_airport_primary", "travel-sample:inventory:hotel:def_inventory_hotel_city", "travel-sample:inventory:hotel:def_inventory_hotel_primary", "travel-sample:inventory:landmark:def_inventory_landmark_city", "travel-sample:inventory:landmark:def_inventory_landmark_primary", "travel-sample:inventory:route:def_inventory_route_primary", "travel-sample:inventory:route:def_inventory_route_route_src_dst_day", "travel-sample:inventory:route:def_inventory_route_schedule_utc", "travel-sample:inventory:route:def_inventory_route_sourceairport"])' \ 103 | '. | del(.indexer) | del(.["travel-sample:def_name_type"]) | map(.items_count > 0) | all' \ 104 | '. | del(.indexer) | map(.num_pending_requests == 0) | all' 105 | 106 | exec $@ 107 | --------------------------------------------------------------------------------