├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── 1-bug_report.md │ └── 2-feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GettingStarted.md ├── LICENSE ├── README.md ├── SampleApp.md ├── api ├── .env ├── .idea │ ├── compiler.xml │ ├── encodings.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── Dockerfile ├── Dockerfile-development ├── README.md ├── SpringDAL.iml ├── azure-pipelines.yml ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── microsoft │ │ │ │ └── cse │ │ │ │ └── reference │ │ │ │ └── spring │ │ │ │ └── dal │ │ │ │ ├── SpringDAL.java │ │ │ │ ├── config │ │ │ │ ├── CORSConfig.java │ │ │ │ ├── Constants.java │ │ │ │ ├── DatabaseInformation.java │ │ │ │ ├── DevelopmentConfig.java │ │ │ │ ├── DevelopmentEmbeddedData.java │ │ │ │ ├── IApplicationConfig.java │ │ │ │ ├── MongoConfig.java │ │ │ │ ├── OauthConfig.java │ │ │ │ ├── ProductionConfig.java │ │ │ │ └── RepositoryConfig.java │ │ │ │ ├── controllers │ │ │ │ ├── CustomEndpointController.java │ │ │ │ ├── HealthEndpointController.java │ │ │ │ ├── PersonRepository.java │ │ │ │ ├── PrincipalRepository.java │ │ │ │ └── TitleRepository.java │ │ │ │ ├── converters │ │ │ │ ├── BooleanToInteger.java │ │ │ │ ├── EmptyStringToNull.java │ │ │ │ ├── IntegerToBoolean.java │ │ │ │ ├── JsonArrayToStringList.java │ │ │ │ └── NullToEmptyString.java │ │ │ │ └── models │ │ │ │ ├── Person.java │ │ │ │ ├── Principal.java │ │ │ │ ├── PrincipalWithName.java │ │ │ │ └── Title.java │ │ └── resources │ │ │ ├── ApplicationInsights.xml │ │ │ └── testdata │ │ │ ├── names.testdata.json │ │ │ ├── principals_mapping.testdata.json │ │ │ └── titles.testdata.json │ └── test │ │ └── java │ │ └── com │ │ └── microsoft │ │ └── cse │ │ └── reference │ │ └── spring │ │ └── dal │ │ ├── integration │ │ ├── BasicExclusionTests.java │ │ ├── BasicRouteTests.java │ │ ├── CustomRouteTests.java │ │ ├── Helpers.java │ │ └── PropertyMockingApplicationContextInitializer.java │ │ └── unit │ │ ├── PersonDataTests.java │ │ ├── PrincipalDataTests.java │ │ └── TitleDataTests.java └── swagger.yml ├── data ├── .gitignore ├── Dockerfile ├── README.md ├── cosmos-import.sh ├── getdata.sh ├── importdata.sh ├── load_env.sh └── sampledata │ ├── name.basics.txt │ ├── title.basics.txt │ ├── title.ratings.txt │ └── titlle.principals.txt ├── docs ├── azureActiveDirectory.md ├── buildAndReleasePipelines.md └── images │ └── high_level_architecture.png ├── infrastructure ├── README.md ├── azure-pipelines.yml ├── azuredeploy.json ├── azuredeploy_application.json ├── deploy.sh ├── global-resources │ ├── README.md │ ├── azure-pipelines.yml │ ├── azuredeploy.json │ └── endpoint_deploy.json └── images │ ├── perftest1.png │ ├── perftest2.png │ └── perftest3.png ├── integration-test-tool ├── .gitignore ├── README.md ├── azure-pipelines.yml ├── config.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ └── integration.test.ts ├── tsconfig.json └── tslint.json └── ui ├── .gitignore ├── Dockerfile ├── Dockerfile-developer ├── README.md ├── azure-pipelines.yml ├── conf └── conf.d │ └── default.conf ├── images └── uiScreenshot.png ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── App.tsx │ ├── DefaultComponent.tsx │ ├── Home.tsx │ ├── Navbar.tsx │ ├── PageForm.tsx │ ├── Person.tsx │ ├── Title.tsx │ ├── __snapshots__ │ │ ├── App.tsx.snap │ │ ├── DefaultComponent.tsx.snap │ │ ├── Home.tsx.snap │ │ ├── Navbar.tsx.snap │ │ ├── PageForm.tsx.snap │ │ ├── Person.tsx.snap │ │ └── Title.tsx.snap │ └── setup.ts ├── components │ ├── App.tsx │ ├── AuthButton.tsx │ ├── AuthContext.tsx │ ├── AuthResponseBar.tsx │ ├── DefaultComponent.tsx │ ├── Home.tsx │ ├── Navbar.tsx │ ├── PageForm.tsx │ ├── Person.tsx │ ├── PrivateRoute.tsx │ └── Title.tsx ├── index.tsx ├── public │ ├── index.html │ └── mountains.svg └── styles │ ├── App.css │ └── Navbar.css ├── tsconfig.json ├── tslint.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | ### Have you identified a reproducible problem in this project? 9 | 10 | Complete the following items then submit your issue: 11 | - [ ] System Information (Operating System) 12 | - [ ] Software Versions (whichever software is relevant to your issue i.e. Java, Node.js, etc.) 13 | - [ ] Clear description of issue 14 | - [ ] Detailed steps how to reproduce the issue 15 | - [ ] Attempted solutions you have tried and/or relevant links to potential solutions 16 | - [ ] Do you plan on fixing this yourself? 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature Request' 3 | about: Suggest an idea for this project 4 | labels: feature 5 | --- 6 | 7 | ### Do you have a feature request? 8 | 9 | Complete the following items then submit your issue: 10 | - [ ] Detailed description of feature 11 | - [ ] What value will this bring to this project? 12 | - [ ] Whether you plan on adding this feature yourself or not -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom entries ### 2 | *.tsv 3 | *.env 4 | *.prefs 5 | *.java2 6 | *.class 7 | *.classpath 8 | *.properties 9 | *.lst 10 | *.jar 11 | *.original 12 | *.project 13 | 14 | # Created by https://www.gitignore.io/api/maven 15 | 16 | ### Maven ### 17 | target/ 18 | pom.xml.tag 19 | pom.xml.releaseBackup 20 | pom.xml.versionsBackup 21 | pom.xml.next 22 | release.properties 23 | dependency-reduced-pom.xml 24 | buildNumber.properties 25 | .mvn/timing.properties 26 | .mvn/wrapper/maven-wrapper.jar 27 | 28 | 29 | # End of https://www.gitignore.io/api/maven 30 | 31 | 32 | 33 | # Created by https://www.gitignore.io/api/intellij 34 | 35 | ### Intellij ### 36 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 37 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 38 | 39 | # User-specific stuff 40 | .idea/**/workspace.xml 41 | .idea/**/tasks.xml 42 | .idea/**/usage.statistics.xml 43 | .idea/**/dictionaries 44 | .idea/**/shelf 45 | 46 | # Generated files 47 | .idea/**/contentModel.xml 48 | 49 | # Sensitive or high-churn files 50 | .idea/**/dataSources/ 51 | .idea/**/dataSources.ids 52 | .idea/**/dataSources.local.xml 53 | .idea/**/sqlDataSources.xml 54 | .idea/**/dynamic.xml 55 | .idea/**/uiDesigner.xml 56 | .idea/**/dbnavigator.xml 57 | 58 | # Gradle 59 | .idea/**/gradle.xml 60 | .idea/**/libraries 61 | 62 | # Gradle and Maven with auto-import 63 | # When using Gradle or Maven with auto-import, you should exclude module files, 64 | # since they will be recreated, and may cause churn. Uncomment if using 65 | # auto-import. 66 | # .idea/modules.xml 67 | # .idea/*.iml 68 | # .idea/modules 69 | 70 | # CMake 71 | cmake-build-*/ 72 | 73 | # Mongo Explorer plugin 74 | .idea/**/mongoSettings.xml 75 | 76 | # File-based project format 77 | *.iws 78 | 79 | # IntelliJ 80 | out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Crashlytics plugin (for Android Studio and IntelliJ) 92 | com_crashlytics_export_strings.xml 93 | crashlytics.properties 94 | crashlytics-build.properties 95 | fabric.properties 96 | 97 | # Editor-based Rest Client 98 | .idea/httpRequests 99 | 100 | # Android studio 3.1+ serialized cache file 101 | .idea/caches/build_file_checksums.ser 102 | 103 | ### Intellij Patch ### 104 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 105 | 106 | # *.iml 107 | # modules.xml 108 | # .idea/misc.xml 109 | # *.ipr 110 | 111 | # Sonarlint plugin 112 | .idea/sonarlint 113 | 114 | .idea/misc.xml 115 | # End of https://www.gitignore.io/api/intellij 116 | 117 | .DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to containers-rest-cosmos-appservice-java 2 | 3 | Welcome, and thank you for your interest in contributing! 4 | 5 | There are many ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved. 6 | 7 | Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 8 | 9 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | ## Opening Issues 12 | Have you identified a reproducible problem in this project? Submit a new issue and select the `Bug Report` template. 13 | 14 | Do you have a feature request? Submit a new issue and select the `Feature Request` template. 15 | 16 | ## Opening a PR 17 | 18 | In order to quickly review and accept your pull requests: 19 | - Create one pull request per issue and link the issue in the pull request 20 | - Never merge multiple requests in one unless they have the same root cause 21 | - Avoid pure formatting changes to code that have not been modified otherwise 22 | - Pull requests should contain tests whenever possible 23 | -------------------------------------------------------------------------------- /GettingStarted.md: -------------------------------------------------------------------------------- 1 | ## Quickstart 2 | 3 | This project is composed of many different pieces - This section is designed to get you up and running as quickly as possible. 4 | 5 | * The largest component of this service is the Java Backend - see [the Backend Readme](./api/README.md) 6 | * Our UI component is a separate service that's built using React and Webpack - see [the UI Readme](./ui/README.md) 7 | * To scale our service on Azure, we leverage ARM templates - see [the Infrastructure Readme](./infrastructure/README.md) 8 | * To run Integration Tests against production services - see [The Integration Test Tool Readme](./integration-test-tool/README.md) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SampleApp.md: -------------------------------------------------------------------------------- 1 | # Sample Application 2 | 3 | This project includes a Java sample application, built on the Sprint Boot Data REST framework, that exposes multiple REST services to read and write data stored in Azure Cosmos DB. 4 | 5 | The REST services are hosted in containers running in Azure App Service for Containers. 6 | 7 | HA/DR is provided by hosting the services in multiple regions, as well as Cosmos DB's native geo-redundancy. 8 | 9 | Traffic Manager is used to route traffic based on geo-proximity, and Application Gateway provides path-based routing, service authentication and DDoS protection. 10 | 11 | Cosmos DB is configured to use the NoSQL MongoDB API. *(Note: We are currently working to add a sample that uses the Cosmos SQL API.)* 12 | 13 | In order to demonstrate Cosmos DB performance with large amounts of data, the project imports historical movie data from [IMDb](https://www.imdb.com/interfaces/). See (https://datasets.imdbws.com/). The datasets include 8.9 million people, 5.3 million movies and 30 million relationships between them. 14 | 15 | ## REST API 16 | 17 | We're using three kinds of models: `Person`, `Title`, and `Principal`. The `Person` model represents a person who participates in media, either in front of the camera or behind the scenes. The `Title` represents what it sounds like - the title of the piece of media, be it a movie, a TV series, or some other kind of similar media. Finally, the `Principal` model and its derivative child class `PrincipalWithName` represent the intersection of Person and Title, ie. what a particular person does or plays in a specific title. 18 | 19 | To meaningfully access this IMDb dataset and these models, there are a few routes one can access on the API. 20 | 21 | + `/people` 22 | + `POST` - Creates a person, and returns information and ID of new person 23 | + `GET` - Returns a small number of people entries 24 | + `/people/{nconst}` > nconst is the unique identifier 25 | + `GET` - Gets the person associated with ID, and returns information about the person 26 | + `PUT` - Updates a person for a given ID, and returns information about updated person 27 | + `DELETE` - Deletes a person with a given ID, and returns the success/failure code 28 | + `/people/{nconst}/titles` > nconst is the unique identifier 29 | + `GET` - Gets the titles in the dataset associated with the person with specified ID and returns them in an array 30 | + `/titles` 31 | + `POST` - Creates a title, and returns the information and ID of the new titles 32 | + `GET` - Returns a small number of title entries 33 | + `/titles/{tconst}` > tconst is the unique identifier 34 | + `GET` - Gets the title of piece given the ID, and returns information about that title 35 | + `PUT` - Updates the title of a piece given the ID, and returns that updated information based on ID 36 | + `DELETE` - Deletes the piece of media given the ID, and returns the success/failure code 37 | + `/titles/{tconst}/people` > tconst is the unique identifier 38 | + `GET` - Gets the people in the dataset associated with the given title, and returns that list 39 | + `/titles/{tconst}/cast` > tconst is the unique identifier 40 | + `GET` - Gets the people in the dataset associated with the given title who act, and returns that list 41 | + `/titles/{tconst}/crew` > tconst is the unique identifier 42 | + `GET` - Gets the people in the dataset associated with the given title who participate behind the scenes, and returns that list 43 | 44 | For more details, check out the [Swagger documentation](./api/swagger.yml). 45 | -------------------------------------------------------------------------------- /api/.env: -------------------------------------------------------------------------------- 1 | # These are the enviromental variabes availabe for the api. 2 | # Use this file for internal development or when launching the docker container locally. 3 | # For example docker run --env-file .env -p 4 | 5 | # Applications Configuration 6 | # SPRING_PROFILES_ACTIVE=development 7 | 8 | # Authorization Configuration 9 | # OAUTH_KEYSET_URI= 10 | # OAUTH_RES_ID= 11 | 12 | # Database Configuration 13 | # DB_CONNSTR= 14 | # DB_NAME= 15 | # EXCLUDE_FILTER= 16 | 17 | # Logging Configuration 18 | # LOGGING_LEVEL_ROOT 19 | # LOGGING_LEVEL_COM_MICROSOFT_CSE_* 20 | # LOGGING_LEVEL_ORG_ZALANDO_LOGBOOK 21 | # LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_DATA_MONGODB_CORE_MONGOTEMPLATE 22 | 23 | # Telemetry Configuratin 24 | # APPLICATION_INSIGHTS_IKEY 25 | 26 | # CORS Configuration 27 | # ALLOWED_ORIGIN=* 28 | -------------------------------------------------------------------------------- /api/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /api/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /api/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /api/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /api/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the ubuntu:bionic in order to run the tests 2 | FROM ubuntu:bionic AS build 3 | 4 | # Update packages 5 | RUN apt-get update 6 | RUN apt-get install -y openjdk-8-jdk maven 7 | 8 | # Set the correct Java version 9 | RUN update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 10 | 11 | # Copy POM and source into the /tmp for building. 12 | COPY pom.xml /tmp/ 13 | COPY src /tmp/src/ 14 | 15 | # Set Working Directory 16 | WORKDIR /tmp/ 17 | RUN mvn test package 18 | 19 | # Start with a base image containing Java runtime 20 | FROM openjdk:8-jdk-alpine 21 | 22 | # Add a volume pointing to /tmp 23 | VOLUME /tmp 24 | 25 | # Make port 8080 available to the world outside this container 26 | EXPOSE 8080 27 | 28 | # Copy package from build container into the final container 29 | COPY --from=build /tmp/target/spring-dal-0.0.1-SNAPSHOT.jar spring-dal.jar 30 | 31 | # Run the jar file 32 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/spring-dal.jar"] -------------------------------------------------------------------------------- /api/Dockerfile-development: -------------------------------------------------------------------------------- 1 | # Use this dockerfile for develpment and testing of the backend api 2 | # To build: docker build -f Dockerfile-development . 3 | # To run: docker run --env-file .env -p 8080:8080 4 | # Open the browser and point it to: http://locahost:8080 5 | 6 | # Use the ubuntu:bionic in order to run the tests 7 | FROM ubuntu:bionic AS build 8 | 9 | # Update packages 10 | RUN apt-get update 11 | RUN apt-get install -y openjdk-8-jdk maven 12 | 13 | # Set the correct Java version 14 | RUN update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 15 | 16 | # Copy POM and source into the /tmp for building. 17 | COPY pom.xml /tmp/ 18 | COPY src /tmp/src/ 19 | 20 | # Set Working Directory 21 | WORKDIR /tmp/ 22 | RUN mvn test package 23 | 24 | # Start with a base image containing Java runtime 25 | FROM openjdk:8-jdk 26 | 27 | # Add a volume pointing to /tmp 28 | VOLUME /tmp 29 | 30 | # Make port 8080 available to the world outside this container 31 | EXPOSE 8080 32 | 33 | # Copy package from build container into the final container 34 | COPY --from=build /tmp/target/spring-dal-0.0.1-SNAPSHOT.jar spring-dal.jar 35 | 36 | # Run the jar file 37 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/spring-dal.jar"] -------------------------------------------------------------------------------- /api/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Maven 2 | # Build your Java project and run tests with Apache Maven. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/java 5 | 6 | trigger: 7 | branches: 8 | include: 9 | - master 10 | paths: 11 | include: 12 | - api/* 13 | 14 | pool: 15 | vmImage: 'Ubuntu 16.04' 16 | 17 | steps: 18 | - script: | 19 | docker build -t $(acrLoginServer)/$ACR_CONTAINER_TAG . 20 | workingDirectory: api/ 21 | displayName: 'Docker Test, Build and Package' 22 | - script: | 23 | docker login $(acrLoginServer) -u $(acrUsername) -p $(acrPassword) 24 | displayName: 'Docker Login' 25 | - script: | 26 | docker push $(acrLoginServer)/$ACR_CONTAINER_TAG 27 | displayName: 'Docker Push' 28 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 29 | -------------------------------------------------------------------------------- /api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.microsoft.cse 7 | spring-dal 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | SpringDAL 12 | Spring DAL RESTful reference 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.6.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-rest 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-data-mongodb 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-oauth2-resource-server 52 | 2.1.0.RELEASE 53 | 54 | 55 | 56 | org.springframework.security.oauth.boot 57 | spring-security-oauth2-autoconfigure 58 | 2.1.0.RELEASE 59 | 60 | 61 | 62 | com.microsoft.azure 63 | applicationinsights-web 64 | 2.2.1 65 | 66 | 67 | 68 | org.zalando 69 | logbook-spring-boot-starter 70 | 1.11.1 71 | 72 | 73 | 74 | 75 | de.flapdoodle.embed 76 | de.flapdoodle.embed.mongo 77 | 2.0.0 78 | 79 | 80 | 81 | 82 | 83 | 84 | oss-sonartype 85 | sonartype 86 | https://oss.sonatype.org/content/repositories/snapshots 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.springframework.boot 94 | spring-boot-maven-plugin 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-resources-plugin 99 | 3.1.0 100 | 101 | 102 | 103 | 104 | src/main/resources 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/SpringDAL.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; 7 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 8 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 9 | 10 | @SpringBootApplication 11 | @EnableMongoRepositories 12 | @EnableAutoConfiguration(exclude = {EmbeddedMongoAutoConfiguration.class}) 13 | @EnableResourceServer 14 | public class SpringDAL { 15 | /** 16 | * Application entry point. Scans for spring beans and automatically loads them 17 | * @param args passed arguments 18 | */ 19 | public static void main(String[] args) { 20 | SpringApplication.run(SpringDAL.class, args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/CORSConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.Ordered; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.http.HttpMethod; 10 | import org.springframework.web.cors.CorsConfiguration; 11 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 12 | import org.springframework.web.filter.CorsFilter; 13 | 14 | @Configuration 15 | public class CORSConfig { 16 | 17 | @Autowired 18 | Environment env; 19 | 20 | @Bean 21 | public FilterRegistrationBean corsFilter() { 22 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 23 | CorsConfiguration config = new CorsConfiguration(); 24 | 25 | config.addAllowedOrigin(env.getProperty(Constants.ENV_ALLOWED_ORIGIN)); 26 | config.addAllowedHeader("*"); 27 | config.addAllowedMethod(HttpMethod.GET); 28 | config.addAllowedMethod(HttpMethod.HEAD); 29 | config.addAllowedMethod(HttpMethod.POST); 30 | config.addAllowedMethod(HttpMethod.PUT); 31 | config.addAllowedMethod(HttpMethod.PATCH); 32 | config.addAllowedMethod(HttpMethod.DELETE); 33 | config.addAllowedMethod(HttpMethod.OPTIONS); 34 | config.addAllowedMethod(HttpMethod.TRACE); 35 | 36 | source.registerCorsConfiguration("/**", config); 37 | final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); 38 | bean.setOrder(Ordered.HIGHEST_PRECEDENCE); 39 | return bean; 40 | } 41 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/Constants.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | public class Constants { 4 | /** 5 | * The constant that represents our application name 6 | */ 7 | public static final String APP_NAME = "SpringDAL"; 8 | 9 | /** 10 | * The constant environment variable that we look to for the oauth2 resource id 11 | * For more info, see: https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-resource-server 12 | */ 13 | public static final String ENV_OAUTH_RES_ID = "OAUTH_RES_ID"; 14 | 15 | /** 16 | * The constant environment variable that we look to for the oauth2 keyset uri 17 | * For more info, see: https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-resource-server 18 | */ 19 | public static final String ENV_OAUTH_KEYSET_URI = "OAUTH_KEYSET_URI"; 20 | 21 | /** 22 | * The constant environment variable that we look to for production db connection string 23 | */ 24 | public static final String ENV_DB_CONNSTR = "DB_CONNSTR"; 25 | 26 | /** 27 | * The constant environment variable that we look to for production db name 28 | */ 29 | public static final String ENV_DB_NAME = "DB_NAME"; 30 | 31 | /** 32 | * The constant environment variable that we look to for exclusion filters 33 | * Note: this value may be a comma separated list (ie: ClassA,ClassB) 34 | * Note: regex is supported in each entry 35 | */ 36 | public static final String ENV_EXCLUDE_FILTER = "EXCLUDE_FILTER"; 37 | 38 | /** 39 | * The constant environment variable that we look to for app insights telemetry key 40 | * for more information, see the following: 41 | * https://docs.microsoft.com/en-us/azure/application-insights/app-insights-java-get-started#1-get-an-application-insights-instrumentation-key 42 | */ 43 | public static final String ENV_APPINSIGHTS_KEY = "APPLICATION_INSIGHTS_IKEY"; 44 | 45 | /** 46 | * The constant environment variable that we look to for the allowed origins strings for CORS 47 | */ 48 | public static final String ENV_ALLOWED_ORIGIN = "ALLOWED_ORIGIN"; 49 | 50 | /** 51 | * Error message used to indicate we couldn't read database configuration 52 | */ 53 | public static final String ERR_DB_CONF = "Failed to read database information from configuration"; 54 | 55 | /** 56 | * Error message used to indicate we couldn't read test data 57 | */ 58 | public static final String ERR_TEST_DATA_FAIL = "Failed to read test data"; 59 | 60 | /** 61 | * Error message used to indicate we couldn't properly parse test data 62 | */ 63 | public static final String ERR_TEST_DATA_FORMAT = "Json structure must be a top-level array"; 64 | 65 | /** 66 | * Status message that is used to display our database connection information 67 | * Note: should String.format({0}=database info) 68 | */ 69 | public static final String STATUS_DB_CONN_INFO = "Successfully read database configuration %s"; 70 | 71 | /** 72 | * Status message that is used to indicate we've loaded test data 73 | */ 74 | public static final String STATUS_TEST_DATA_USED = "Successfully loaded test data"; 75 | 76 | /** 77 | * Status message that is used to indicate we've configured appInsights 78 | */ 79 | public static final String STATUS_APPINSIGHTS_SUCCESS = "Successfully configured appInsights telemetry key"; 80 | 81 | /** 82 | * Status message that is used to indicate we've failed to configure appInsights: 83 | * Note: this isn't an ERR, as appInsights is optional in all cases today 84 | */ 85 | public static final String STATUS_APPINSIGHTS_FAILURE = "Unable to configure appInsights telemetry key"; 86 | 87 | /** 88 | * The collection from which we pull Person objects 89 | */ 90 | public static final String DB_PERSON_COLLECTION = "names"; 91 | 92 | /** 93 | * The collection from which we pull Principal objects 94 | */ 95 | public static final String DB_PRINCIPAL_COLLECTION = "principals_mapping"; 96 | 97 | /** 98 | * The collection from which we pull Title objects 99 | */ 100 | public static final String DB_TITLE_COLLECTION = "titles"; 101 | } 102 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/DatabaseInformation.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | /** 4 | * Represents a read-only database information object used for connection 5 | */ 6 | public class DatabaseInformation { 7 | private String connStr; 8 | private String name; 9 | 10 | /** 11 | * Create an instance of database information with the given connStr and name 12 | * @param connStr the connection string 13 | * @param name the name 14 | */ 15 | public DatabaseInformation(String connStr, String name) { 16 | this.connStr = connStr; 17 | this.name = name; 18 | } 19 | 20 | /** 21 | * The connection string with which to connect to the database 22 | * @return connection string 23 | */ 24 | public String getConnectionString() { 25 | return this.connStr; 26 | } 27 | 28 | /** 29 | * The name with which to connect to the database 30 | * @return name 31 | */ 32 | public String getName() { 33 | return this.name; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return '[' + this.getConnectionString() + "]/" + this.getName(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/DevelopmentConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import de.flapdoodle.embed.mongo.MongodExecutable; 4 | import de.flapdoodle.embed.mongo.MongodProcess; 5 | import de.flapdoodle.embed.mongo.MongodStarter; 6 | import de.flapdoodle.embed.mongo.config.IMongodConfig; 7 | import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; 8 | import de.flapdoodle.embed.mongo.config.Net; 9 | import de.flapdoodle.embed.mongo.distribution.Version; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Profile; 15 | import org.springframework.core.env.Environment; 16 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 18 | 19 | import java.io.IOException; 20 | 21 | @Configuration 22 | @Profile({"development", "default"}) 23 | public class DevelopmentConfig extends WebSecurityConfigurerAdapter implements IApplicationConfig { 24 | @Autowired 25 | Environment env; 26 | 27 | Logger logger = LoggerFactory.getLogger(DevelopmentConfig.class); 28 | 29 | Net embeddedBindInformation; 30 | MongodExecutable embeddedMongoInstance; 31 | 32 | @Override 33 | public DatabaseInformation getDatabaseInformation() throws Exception { 34 | String connStr = env.getProperty(Constants.ENV_DB_CONNSTR); 35 | String name; 36 | if (connStr == null) { 37 | if (this.embeddedMongoInstance == null) { 38 | // we need to try to startup embedded mongo 39 | this.embeddedBindInformation = new Net(); 40 | this.embeddedMongoInstance = this.setupMongoEmbed(this.embeddedBindInformation); 41 | } 42 | 43 | connStr = "mongodb://localhost:" + this.embeddedBindInformation.getPort(); 44 | name = "test"; 45 | } else { 46 | name = env.getRequiredProperty(Constants.ENV_DB_NAME); 47 | } 48 | 49 | return new DatabaseInformation(connStr, name); 50 | } 51 | 52 | @Override 53 | public void configure(WebSecurity webSecurity) throws Exception { 54 | // In development mode, we ignore all webSecurity features 55 | // effectively disabling oauth2 token requirements 56 | webSecurity.ignoring().antMatchers("/**"); 57 | } 58 | 59 | /** 60 | * Attempts to start a mongo instance, using embedMongo 61 | * @param bind the net info to bind to 62 | * @return the instance 63 | * @throws IOException indicates a failure 64 | */ 65 | private MongodExecutable setupMongoEmbed(Net bind) throws IOException { 66 | MongodStarter starter; 67 | starter = MongodStarter.getDefaultInstance(); 68 | 69 | IMongodConfig mongodConfig = new MongodConfigBuilder() 70 | .version(Version.Main.DEVELOPMENT) 71 | .net(bind) 72 | .build(); 73 | 74 | MongodExecutable mongodExecutable = starter.prepare(mongodConfig); 75 | MongodProcess mongod = mongodExecutable.start(); 76 | return mongodExecutable; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/DevelopmentEmbeddedData.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.bson.Document; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.context.event.ContextRefreshedEvent; 11 | import org.springframework.context.event.EventListener; 12 | import org.springframework.core.io.ClassPathResource; 13 | import org.springframework.data.mongodb.core.MongoTemplate; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.io.*; 17 | import java.nio.charset.Charset; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.ArrayList; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | 23 | /** 24 | * Responsible for populating test data in development deployments 25 | */ 26 | @Component 27 | @Profile({"default", "development"}) 28 | public class DevelopmentEmbeddedData { 29 | @Autowired 30 | MongoTemplate mongoInterface; 31 | 32 | Logger logger = LoggerFactory.getLogger(MongoConfig.class); 33 | 34 | @EventListener 35 | public void onApplicationEvent(ContextRefreshedEvent event) { 36 | try { 37 | // load all the testdata sets into their given collections 38 | mongoInterface.insert(parseTestData("testdata/titles.testdata.json"), Constants.DB_TITLE_COLLECTION); 39 | mongoInterface.insert(parseTestData("testdata/names.testdata.json"), Constants.DB_PERSON_COLLECTION); 40 | mongoInterface.insert(parseTestData("testdata/principals_mapping.testdata.json"), Constants.DB_PRINCIPAL_COLLECTION); 41 | 42 | logger.info(Constants.STATUS_TEST_DATA_USED); 43 | } catch (IOException e) { 44 | logger.error(Constants.ERR_TEST_DATA_FAIL, e); 45 | } 46 | } 47 | 48 | /** 49 | * Parse test data from a resources file 50 | * @param resourcePath 51 | * @return 52 | * @throws IOException 53 | */ 54 | private List parseTestData(String resourcePath) throws IOException { 55 | ClassPathResource resource = new ClassPathResource(resourcePath); 56 | InputStream inputStream = resource.getInputStream(); 57 | 58 | // first we read the data 59 | StringBuilder textBuilder = new StringBuilder(); 60 | try (Reader reader = new BufferedReader(new InputStreamReader 61 | (inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) { 62 | int c = 0; 63 | while ((c = reader.read()) != -1) { 64 | textBuilder.append((char) c); 65 | } 66 | } 67 | 68 | String jsonData = textBuilder.toString(); 69 | 70 | // then we convert that data to a json node 71 | ObjectMapper mapper = new ObjectMapper(); 72 | JsonNode jsonNode = mapper.readTree(jsonData); 73 | 74 | // we ensure it is an array of documents to insert 75 | if (!jsonNode.isArray()) { 76 | throw new InvalidObjectException(Constants.ERR_TEST_DATA_FORMAT); 77 | } 78 | 79 | // we parse and store the elements of the array 80 | Iterator it = jsonNode.elements(); 81 | ArrayList results = new ArrayList(); 82 | 83 | while (it.hasNext()) { 84 | JsonNode node = it.next(); 85 | 86 | Document parsed = Document.parse(node.toString()); 87 | 88 | results.add(parsed); 89 | } 90 | 91 | // return those elements 92 | return results; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/IApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | public interface IApplicationConfig { 4 | /** 5 | * Get the database information object that describes the database we'll connect to 6 | * @return Database information object 7 | * @throws Exception Thrown when we cannot get this information (usually due to misconfiguration) 8 | */ 9 | DatabaseInformation getDatabaseInformation() throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/MongoConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import com.mongodb.MongoClient; 4 | import com.mongodb.MongoClientURI; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.convert.converter.Converter; 10 | import org.springframework.data.mongodb.config.AbstractMongoConfiguration; 11 | import org.springframework.data.mongodb.core.convert.MongoCustomConversions; 12 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * Configures the mongo driver based on our application configuration 18 | */ 19 | @Configuration 20 | @EnableMongoRepositories 21 | public class MongoConfig extends AbstractMongoConfiguration { 22 | 23 | @Autowired 24 | IApplicationConfig appConfig; 25 | 26 | @Autowired 27 | List converters; 28 | 29 | Logger logger = LoggerFactory.getLogger(MongoConfig.class); 30 | 31 | @Override 32 | public MongoClient mongoClient() { 33 | DatabaseInformation info = null; 34 | try { 35 | info = this.appConfig.getDatabaseInformation(); 36 | 37 | logger.info(String.format(Constants.STATUS_DB_CONN_INFO, info)); 38 | } catch (Exception e) { 39 | logger.error(Constants.ERR_DB_CONF, e); 40 | } 41 | 42 | return new MongoClient(new MongoClientURI(info.getConnectionString())); 43 | } 44 | 45 | @Override 46 | protected String getDatabaseName() { 47 | DatabaseInformation info = null; 48 | try { 49 | info = this.appConfig.getDatabaseInformation(); 50 | 51 | logger.info(String.format(Constants.STATUS_DB_CONN_INFO, info)); 52 | } catch (Exception e) { 53 | logger.error(Constants.ERR_DB_CONF, e); 54 | } 55 | 56 | return info.getName(); 57 | } 58 | 59 | @Override 60 | public MongoCustomConversions customConversions() { 61 | return new MongoCustomConversions(converters); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/OauthConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 9 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 10 | import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore; 11 | 12 | /** 13 | * Configure oauth2 resource server 14 | */ 15 | @Configuration 16 | @ConditionalOnProperty(name = {Constants.ENV_OAUTH_KEYSET_URI, Constants.ENV_OAUTH_RES_ID}) 17 | public class OauthConfig extends ResourceServerConfigurerAdapter { 18 | @Autowired 19 | Environment env; 20 | 21 | @Override 22 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception { 23 | // setup the resource id 24 | resources.resourceId(this.env.getProperty(Constants.ENV_OAUTH_RES_ID)); 25 | 26 | // setup the token store 27 | resources.tokenStore(new JwkTokenStore(this.env.getProperty(Constants.ENV_OAUTH_KEYSET_URI))); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/ProductionConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import com.microsoft.applicationinsights.TelemetryConfiguration; 4 | import com.microsoft.applicationinsights.web.internal.WebRequestTrackingFilter; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | 15 | @Configuration 16 | @Profile("production") 17 | public class ProductionConfig extends WebSecurityConfigurerAdapter implements IApplicationConfig { 18 | @Autowired 19 | Environment env; 20 | 21 | Logger logger = LoggerFactory.getLogger(ProductionConfig.class); 22 | 23 | @Override 24 | public DatabaseInformation getDatabaseInformation() { 25 | return new DatabaseInformation(env.getRequiredProperty(Constants.ENV_DB_CONNSTR), 26 | env.getRequiredProperty(Constants.ENV_DB_NAME)); 27 | } 28 | 29 | /** 30 | * Gets the appInsights telemetry configuration information 31 | * @return telemetry key 32 | */ 33 | @Bean 34 | public String telemetryConfig() { 35 | // note: this is optional, if it isn't set we won't use appInsights 36 | String telemetryKey = env.getProperty(Constants.ENV_APPINSIGHTS_KEY); 37 | if (telemetryKey != null) { 38 | TelemetryConfiguration.getActive().setInstrumentationKey(telemetryKey); 39 | logger.info(Constants.STATUS_APPINSIGHTS_SUCCESS); 40 | } else { 41 | logger.info(Constants.STATUS_APPINSIGHTS_FAILURE); 42 | } 43 | return telemetryKey; 44 | } 45 | 46 | 47 | @Override 48 | public void configure(WebSecurity webSecurity) throws Exception { 49 | // In production mode, we ignore webSecurity features for /health endpoint 50 | // effectively disabling oauth2 token requirements for health probe 51 | webSecurity.ignoring().antMatchers("/health"); 52 | } 53 | 54 | /** 55 | * Creates bean of type WebRequestTrackingFilter for request tracking to appInsights 56 | * @return {@link Bean} of type {@link WebRequestTrackingFilter} 57 | */ 58 | @Bean 59 | public WebRequestTrackingFilter appInsightFilter() { 60 | return new WebRequestTrackingFilter(Constants.APP_NAME); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/config/RepositoryConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.config; 2 | 3 | import com.microsoft.cse.reference.spring.dal.models.Person; 4 | import com.microsoft.cse.reference.spring.dal.models.Title; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.data.repository.core.RepositoryMetadata; 9 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 10 | import org.springframework.data.rest.core.config.RepositoryRestConfiguration; 11 | import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy; 12 | import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; 13 | 14 | import java.util.regex.Pattern; 15 | 16 | @Configuration 17 | public class RepositoryConfig extends RepositoryRestConfigurerAdapter { 18 | @Autowired 19 | Environment env; 20 | 21 | @Override 22 | public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { 23 | // expose the ids for the given model types 24 | config.exposeIdsFor(Person.class); 25 | config.exposeIdsFor(Title.class); 26 | 27 | // configure how we find and load repositories to let us disable them at runtime with an environment variable 28 | config.setRepositoryDetectionStrategy(metadata -> { 29 | // if it's not exported, exclude it 30 | if (!metadata.getRepositoryInterface().getAnnotation(RepositoryRestResource.class).exported()) { 31 | return false; 32 | } 33 | 34 | String className = metadata.getRepositoryInterface().getName(); 35 | String exclusionList = env.getProperty(Constants.ENV_EXCLUDE_FILTER); 36 | 37 | if (exclusionList != null && !exclusionList.isEmpty()) { 38 | for (String exclude : exclusionList.split(",")) { 39 | // see if we get any hits, treating the exclusion list entry as a regex pattern 40 | // note: this allows us to hit 'ClassA' even if it's really 'com.package.ClassA' 41 | if (Pattern.compile(exclude).matcher(className).find()) { 42 | // exclude if we match 43 | return false; 44 | } 45 | } 46 | } 47 | 48 | // default to allowing the repository 49 | return true; 50 | }); 51 | } 52 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/controllers/HealthEndpointController.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.controllers; 2 | 3 | import org.springframework.web.bind.annotation.*; 4 | 5 | /** 6 | * Create a custom endpoint that returns status OK when Java App is running 7 | */ 8 | @RestController 9 | public class HealthEndpointController { 10 | 11 | @RequestMapping(method = RequestMethod.GET, value = "/health") 12 | public String getStatus() { 13 | return new String("Alive"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/controllers/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.controllers; 2 | 3 | import com.microsoft.cse.reference.spring.dal.models.Person; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Create a repository of Person models (mounted at /people) to facilitate route generation 13 | * for model CRUD operations as well as custom queries 14 | */ 15 | @Repository 16 | @RepositoryRestResource(path="people") 17 | public interface PersonRepository extends MongoRepository { 18 | /** 19 | * Create a custom query for searching by primaryName 20 | * @param primaryName the person primary name 21 | * @return the person(s) 22 | */ 23 | List findByPrimaryName(@Param("primaryName") String primaryName); 24 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/controllers/PrincipalRepository.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.controllers; 2 | 3 | import com.microsoft.cse.reference.spring.dal.models.Principal; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Create a repository of Principal models (not externally mounted) to facilitate route generation 13 | * for model CRUD operations as well as custom queries 14 | */ 15 | @Repository 16 | @RepositoryRestResource(exported = false) 17 | public interface PrincipalRepository extends MongoRepository { 18 | /** 19 | * Create a custom query for searching by tconst 20 | * @param tconst the tconst value 21 | * @return the principal(s) 22 | */ 23 | List findByTconst(@Param("tconst") String tconst); 24 | 25 | /** 26 | * Create a custom query for searching by nconst 27 | * @param nconst the nconst value 28 | * @return the principal(s) 29 | */ 30 | List findByNconst(@Param("nconst") String nconst); 31 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/controllers/TitleRepository.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.controllers; 2 | 3 | import com.microsoft.cse.reference.spring.dal.models.Title; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Create a repository of Title models (mounted at /titles) to facilitate route generation 13 | * for model CRUD operations as well as custom queries 14 | */ 15 | @Repository 16 | @RepositoryRestResource(path = "titles") 17 | public interface TitleRepository extends MongoRepository { 18 | /** 19 | * Create a custom query for searching by primaryTitle 20 | * @param primaryTitle the title primary title 21 | * @return the title(s) 22 | */ 23 | List findByPrimaryTitle(@Param("primaryTitle") String primaryTitle); 24 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/converters/BooleanToInteger.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.converters; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.convert.WritingConverter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @WritingConverter 9 | public class BooleanToInteger implements Converter<Boolean,Integer> { 10 | @Override 11 | public Integer convert(Boolean bool) { 12 | return bool == true ? 1 : 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/converters/EmptyStringToNull.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.converters; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.convert.ReadingConverter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @ReadingConverter 9 | public class EmptyStringToNull implements Converter<String,String> { 10 | @Override 11 | public String convert(String str) { 12 | return str == null || str.isEmpty() ? null : str; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/converters/IntegerToBoolean.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.converters; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.convert.ReadingConverter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @ReadingConverter 9 | public class IntegerToBoolean implements Converter<Integer,Boolean> { 10 | @Override 11 | public Boolean convert(Integer integer) { 12 | return !(integer == null || integer == 0); 13 | } 14 | } -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/converters/JsonArrayToStringList.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.converters; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.microsoft.cse.reference.spring.dal.config.Constants; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.data.convert.ReadingConverter; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | 16 | /** 17 | * Parses json arrays and csv literals to string lists 18 | * 19 | * Note: we don't convert back, so this is also a data cleaning component - that is, 20 | * any new writes will have a normalized schema using an array, not a string literal 21 | * 22 | * If we didn't want this behavior, we'd need a more complicated @WritingConverter to match 23 | */ 24 | @Component 25 | @ReadingConverter 26 | public class JsonArrayToStringList implements Converter<String, List<String>> { 27 | @Override 28 | public List<String> convert(String str) { 29 | try { 30 | str = str.isEmpty() || str == null ? "[]" : str; 31 | 32 | ObjectMapper mapper = new ObjectMapper(); 33 | JsonNode jsonNode = mapper.readTree(str); 34 | 35 | if (!jsonNode.isArray()) { 36 | throw new IOException(Constants.ERR_TEST_DATA_FORMAT); 37 | } else { 38 | ArrayList<String> results = new ArrayList<>(); 39 | 40 | Iterator<JsonNode> it = jsonNode.elements(); 41 | while (it.hasNext()) { 42 | results.add(it.next().asText()); 43 | } 44 | 45 | return results; 46 | } 47 | } catch (IOException e) { 48 | if (str.contains(",")) { 49 | return Arrays.asList(str.split(",")); 50 | } else { 51 | return Arrays.asList(str); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/converters/NullToEmptyString.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.converters; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.convert.WritingConverter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @WritingConverter 9 | public class NullToEmptyString implements Converter<String,String> { 10 | @Override 11 | public String convert(String str) { 12 | return str == null ? "" : str; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/models/Person.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.models; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import org.bson.types.ObjectId; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | 8 | import java.util.List; 9 | 10 | @Document(collection = Constants.DB_PERSON_COLLECTION) 11 | public class Person { 12 | private ObjectId id; 13 | @Id 14 | public String nconst; 15 | public String primaryName; 16 | public Integer birthYear; 17 | public Integer deathYear; 18 | public List<String> primaryProfession; 19 | public List<String> knownForTitles; 20 | } 21 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/models/Principal.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.models; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import org.bson.types.ObjectId; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | 8 | import java.util.List; 9 | 10 | @Document(collection = Constants.DB_PRINCIPAL_COLLECTION) 11 | public class Principal { 12 | @Id 13 | private ObjectId id; 14 | public String tconst; 15 | public Integer ordering; 16 | public String nconst; 17 | public String category; 18 | public String job; 19 | public List<String> characters; 20 | } 21 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/models/PrincipalWithName.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.models; 2 | 3 | import org.bson.types.ObjectId; 4 | import org.springframework.data.annotation.Id; 5 | 6 | import java.util.LinkedHashMap; 7 | import java.util.List; 8 | 9 | public class PrincipalWithName { 10 | @Id 11 | private ObjectId id; 12 | public String tconst; 13 | public Integer ordering; 14 | public LinkedHashMap<?,?> person; 15 | public String category; 16 | public String job; 17 | public List<String> characters; 18 | } 19 | -------------------------------------------------------------------------------- /api/src/main/java/com/microsoft/cse/reference/spring/dal/models/Title.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.models; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import org.bson.types.ObjectId; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | 8 | import java.util.List; 9 | 10 | @Document(collection = Constants.DB_TITLE_COLLECTION) 11 | public class Title { 12 | private ObjectId id; 13 | @Id 14 | public String tconst; 15 | public String titleType; 16 | public String primaryTitle; 17 | public String originalTitle; 18 | public Boolean isAdult; 19 | public Integer startYear; 20 | public Integer endYear; 21 | public Integer runtimeMinutes; 22 | public List<String> genres; 23 | } 24 | -------------------------------------------------------------------------------- /api/src/main/resources/ApplicationInsights.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings" schemaVersion="2014-05-30"> 3 | 4 | <!-- HTTP request component (not required for bare API) --> 5 | <TelemetryModules> 6 | <Add type="com.microsoft.applicationinsights.web.extensibility.modules.WebRequestTrackingTelemetryModule"/> 7 | <Add type="com.microsoft.applicationinsights.web.extensibility.modules.WebSessionTrackingTelemetryModule"/> 8 | <Add type="com.microsoft.applicationinsights.web.extensibility.modules.WebUserTrackingTelemetryModule"/> 9 | </TelemetryModules> 10 | 11 | <!-- Events correlation (not required for bare API) --> 12 | <!-- These initializers add context data to each event --> 13 | 14 | <TelemetryInitializers> 15 | <Add type="com.microsoft.applicationinsights.web.extensibility.initializers.WebOperationIdTelemetryInitializer"/> 16 | <Add type="com.microsoft.applicationinsights.web.extensibility.initializers.WebOperationNameTelemetryInitializer"/> 17 | <Add type="com.microsoft.applicationinsights.web.extensibility.initializers.WebSessionTelemetryInitializer"/> 18 | <Add type="com.microsoft.applicationinsights.web.extensibility.initializers.WebUserTelemetryInitializer"/> 19 | <Add type="com.microsoft.applicationinsights.web.extensibility.initializers.WebUserAgentTelemetryInitializer"/> 20 | 21 | </TelemetryInitializers> 22 | </ApplicationInsights> -------------------------------------------------------------------------------- /api/src/main/resources/testdata/names.testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nconst" : "nm0000496", 4 | "primaryName" : "Juliette Lewis", 5 | "birthYear" : 1973, 6 | "deathYear" : "", 7 | "primaryProfession" : "actress,soundtrack,director", 8 | "knownForTitles" : "tt0116367,tt1322269,tt0110632,tt0101540" 9 | }, 10 | { 11 | "nconst" : "nm0000342", 12 | "primaryName" : "James Cromwell", 13 | "birthYear" : 1940, 14 | "deathYear" : "", 15 | "primaryProfession" : "actor,producer,soundtrack", 16 | "knownForTitles" : "tt0112431,tt0120689,tt2245084,tt0119488" 17 | }, 18 | { 19 | "nconst" : "nm0000230", 20 | "primaryName" : "Sylvester Stallone", 21 | "birthYear" : 1946, 22 | "deathYear" : "", 23 | "primaryProfession" : "actor,writer,producer", 24 | "knownForTitles" : "tt3076658,tt0089927,tt0084602,tt0075148" 25 | }, 26 | { 27 | "nconst" : "nm0001500", 28 | "primaryName" : "Karl Malden", 29 | "birthYear" : 1912, 30 | "deathYear" : 2009, 31 | "primaryProfession" : "actor,soundtrack,director", 32 | "knownForTitles" : "tt0047296,tt0066206,tt0048973,tt0044081" 33 | }, 34 | { 35 | "nconst" : "nm0000548", 36 | "primaryName" : "Elizabeth Montgomery", 37 | "birthYear" : 1933, 38 | "deathYear" : 1995, 39 | "primaryProfession" : "actress,soundtrack,miscellaneous", 40 | "knownForTitles" : "tt0076981,tt0088713,tt0057733,tt0073273" 41 | }, 42 | { 43 | "nconst":"nm0000428", 44 | "primaryName": "D.W. Griffith", 45 | "birthYear": 1875, 46 | "deathYear": 1948, 47 | "primaryProfession": "director,writer,producer", 48 | "knownForTitles": "tt0006864,tt0010484,tt0004972,tt0012532" 49 | }, 50 | { 51 | "nconst":"nm0492757", 52 | "primaryName": "Florence Lawrence", 53 | "birthYear": 1886, 54 | "deathYear": 1938, 55 | "primaryProfession": "actress", 56 | "knownForTitles": "tt0143358,tt0200577,tt0000770,tt0200909" 57 | }, 58 | { 59 | "nconst":"nm0555522", 60 | "primaryName": "Arthur Marvin", 61 | "birthYear": 1859, 62 | "deathYear": 1911, 63 | "primaryProfession": "cinematographer,director,camera_department", 64 | "knownForTitles": "tt0291476,tt0000412,tt0233612,tt0300052" 65 | } 66 | 67 | ] -------------------------------------------------------------------------------- /api/src/main/resources/testdata/principals_mapping.testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tconst" : "tt0000843", 4 | "ordering" : 6, 5 | "nconst" : "nm0878494", 6 | "category" : "writer", 7 | "job" : "story", 8 | "characters" : "" 9 | }, 10 | { 11 | "tconst" : "tt0000854", 12 | "ordering" : 5, 13 | "nconst" : "nm0000428", 14 | "category" : "director", 15 | "job" : "", 16 | "characters" : "" 17 | }, 18 | { 19 | "tconst" : "tt0000442", 20 | "ordering" : 1, 21 | "nconst" : "nm0622273", 22 | "category" : "actress", 23 | "job" : "", 24 | "characters" : "[\"Barnemordersken\"]" 25 | }, 26 | { 27 | "tconst" : "tt0001008", 28 | "ordering" : 1, 29 | "nconst" : "nm0819384", 30 | "category" : "actress", 31 | "job" : "", 32 | "characters" : "[\"The Prince\",\"Tom Canty\"]" 33 | }, 34 | { 35 | "tconst" : "tt0000698", 36 | "ordering" : 3, 37 | "nconst" : "nm0000428", 38 | "category" : "actor", 39 | "job" : "", 40 | "characters" : "[\"Footman\"]" 41 | }, 42 | { 43 | "tconst" : "tt0092377", 44 | "ordering" : 4, 45 | "nconst" : "nm0000496", 46 | "category" : "actress", 47 | "characters" : "[\"Kate Farrell\"]" 48 | }, 49 | { 50 | "tconst" : "tt0000698", 51 | "ordering" : 1, 52 | "nconst" : "nm0492757", 53 | "category" : "actress", 54 | "characters" : "[\"O'Yama\"]" 55 | }, 56 | { 57 | "tconst" : "tt0000698", 58 | "ordering" : 6, 59 | "nconst" : "nm0555522", 60 | "category" : "cinematographer", 61 | "characters" : "" 62 | } 63 | ] -------------------------------------------------------------------------------- /api/src/main/resources/testdata/titles.testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tconst" : "tt0000003", 4 | "titleType" : "short", 5 | "primaryTitle" : "Pauvre Pierrot", 6 | "originalTitle" : "Pauvre Pierrot", 7 | "isAdult" : 0, 8 | "startYear" : 1892, 9 | "endYear" : "", 10 | "runtimeMinutes" : 4, 11 | "genres" : "Animation,Comedy,Romance" 12 | }, 13 | { 14 | "tconst" : "tt0000192", 15 | "titleType" : "short", 16 | "primaryTitle" : "Ella Lola, a la Trilby", 17 | "originalTitle" : "Ella Lola, a la Trilby", 18 | "isAdult" : 0, 19 | "startYear" : 1898, 20 | "endYear" : "", 21 | "runtimeMinutes" : "", 22 | "genres" : "Short" 23 | }, 24 | { 25 | "tconst" : "tt0001022", 26 | "titleType" : "short", 27 | "primaryTitle" : "A Rose of the Tenderloin", 28 | "originalTitle" : "A Rose of the Tenderloin", 29 | "isAdult" : 0, 30 | "startYear" : 1909, 31 | "endYear" : "", 32 | "runtimeMinutes" : "", 33 | "genres" : "Drama,Short" 34 | }, 35 | { 36 | "tconst" : "tt0001008", 37 | "titleType" : "short", 38 | "primaryTitle" : "The Prince and the Pauper", 39 | "originalTitle" : "The Prince and the Pauper", 40 | "isAdult" : 0, 41 | "startYear" : 1909, 42 | "endYear" : "", 43 | "runtimeMinutes" : "", 44 | "genres" : "Short" 45 | }, 46 | { 47 | "tconst" : "tt0075472", 48 | "titleType" : "tvSeries", 49 | "primaryTitle" : "All Creatures Great and Small", 50 | "originalTitle" : "All Creatures Great and Small", 51 | "isAdult" : 0, 52 | "startYear" : 1978, 53 | "endYear" : 1990, 54 | "runtimeMinutes" : 50, 55 | "genres" : "Comedy,Drama" 56 | }, 57 | { 58 | "tconst":"tt0000698", 59 | "titleType": "short", 60 | "primaryTitle": "The Heart of O Yama", 61 | "originalTitle": "The Heart of O Yama", 62 | "isAdult": 0, 63 | "startYear": 1908, 64 | "endYear": "", 65 | "runtimeMinutes": 15, 66 | "genres": "Drama,Romance,Short" 67 | } 68 | ] -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/integration/BasicExclusionTests.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.integration; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.client.TestRestTemplate; 9 | import org.springframework.boot.web.server.LocalServerPort; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | @RunWith(SpringRunner.class) 15 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 16 | @ContextConfiguration(initializers = BasicExclusionTests.Config.class) 17 | public class BasicExclusionTests { 18 | @Autowired 19 | TestRestTemplate rest; 20 | 21 | @LocalServerPort 22 | Integer httpPort; 23 | 24 | @Test 25 | public void ExcludeTitleRepository() { 26 | ResponseEntity<String> res = this.rest.getForEntity("http://localhost:" + httpPort + "/titles", String.class); 27 | Assert.assertTrue(res.getStatusCode().is4xxClientError()); 28 | } 29 | 30 | /** 31 | * A configuration instance for these tests 32 | */ 33 | public static class Config extends PropertyMockingApplicationContextInitializer { 34 | @Override 35 | protected String[] getExcludeList() { 36 | // we wish to disable the TitleRepository for the tests above, so we exclude them 37 | return new String[] { "TitleRepository" }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/integration/Helpers.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.integration; 2 | 3 | import de.flapdoodle.embed.mongo.MongodExecutable; 4 | import de.flapdoodle.embed.mongo.MongodStarter; 5 | import de.flapdoodle.embed.mongo.config.IMongodConfig; 6 | import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; 7 | import de.flapdoodle.embed.mongo.config.Net; 8 | import de.flapdoodle.embed.mongo.distribution.Version; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * Integration test utilities 14 | */ 15 | public class Helpers { 16 | /** 17 | * Setup a mongo instance 18 | * @param net the net instance to bind to 19 | * @return the mongo instance 20 | * @throws IOException thrown when unable to bind 21 | */ 22 | static MongodExecutable SetupMongo(Net net) throws IOException { 23 | MongodStarter starter = MongodStarter.getDefaultInstance(); 24 | 25 | IMongodConfig mongodConfig = new MongodConfigBuilder() 26 | .version(Version.Main.DEVELOPMENT) 27 | .net(net) 28 | .build(); 29 | 30 | MongodExecutable mongoProc = starter.prepare(mongodConfig); 31 | mongoProc.start(); 32 | 33 | return mongoProc; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/integration/PropertyMockingApplicationContextInitializer.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.integration; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import de.flapdoodle.embed.mongo.MongodExecutable; 5 | import de.flapdoodle.embed.mongo.config.Net; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.context.ApplicationContextInitializer; 8 | import org.springframework.context.ConfigurableApplicationContext; 9 | import org.springframework.core.env.MutablePropertySources; 10 | import org.springframework.core.env.StandardEnvironment; 11 | import org.springframework.mock.env.MockPropertySource; 12 | 13 | import java.io.IOException; 14 | 15 | /** 16 | * Allows mocking of application context properties 17 | */ 18 | public abstract class PropertyMockingApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { 19 | /** 20 | * Get the mongo data repository exclusion list 21 | * @apiNote see Constants.ENV_EXCLUDE_FILTER for more info 22 | * @return the mongo data repository exclusion list 23 | */ 24 | protected String[] getExcludeList() { 25 | return new String[0]; 26 | } 27 | 28 | /** 29 | * Get the CORS allowed origin value 30 | * @return the CORS allowed origin value 31 | */ 32 | protected String getAllowedOrigin() { return ""; } 33 | 34 | 35 | /** 36 | * Get the net binding information for the embedded mongo server 37 | * @return net binding information 38 | */ 39 | protected Net getMongoNet() { 40 | try { 41 | return new Net(); 42 | } catch (IOException e) { 43 | return null; 44 | } 45 | } 46 | 47 | /** 48 | * Get the database name 49 | * @return the database name 50 | */ 51 | protected String getDbName() { 52 | return "test"; 53 | } 54 | 55 | /** 56 | * Get the embedded mongo server instance 57 | * @param bind the net binding information to bind to 58 | * @return the embedded mongo server instance 59 | */ 60 | protected MongodExecutable getMongo(Net bind) { 61 | try { 62 | return Helpers.SetupMongo(bind); 63 | } catch (IOException e) { 64 | return null; 65 | } 66 | } 67 | 68 | /** 69 | * Get the rest template builder with which to test rest endpoints 70 | * @return the rest template builder 71 | */ 72 | protected RestTemplateBuilder getRestTemplateBuilder() { 73 | return new RestTemplateBuilder().setConnectTimeout(1000).setReadTimeout(1000); 74 | } 75 | 76 | @Override 77 | public void initialize(ConfigurableApplicationContext applicationContext) { 78 | // configure a net binding instance 79 | Net mongoNet = this.getMongoNet(); 80 | 81 | // register some autowire-able dependencies, to make leveraging the configured instances in a test possible 82 | applicationContext.getBeanFactory().registerResolvableDependency(RestTemplateBuilder.class, this.getRestTemplateBuilder()); 83 | applicationContext.getBeanFactory().registerResolvableDependency(Net.class, mongoNet); 84 | applicationContext.getBeanFactory().registerResolvableDependency(MongodExecutable.class, this.getMongo(mongoNet)); 85 | 86 | // configure the property sources that will be used by the application 87 | MutablePropertySources propertySources = applicationContext.getEnvironment().getPropertySources(); 88 | MockPropertySource mockEnvVars = new MockPropertySource() 89 | .withProperty(Constants.ENV_DB_NAME, this.getDbName()) 90 | .withProperty(Constants.ENV_DB_CONNSTR, "mongodb://localhost:" + mongoNet.getPort()) 91 | .withProperty(Constants.ENV_ALLOWED_ORIGIN, this.getAllowedOrigin()) 92 | .withProperty(Constants.ENV_EXCLUDE_FILTER, String.join(",", this.getExcludeList())); 93 | 94 | // inject the property sources into the application as environment variables 95 | propertySources.replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, mockEnvVars); 96 | } 97 | } -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/unit/PersonDataTests.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.unit; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import com.microsoft.cse.reference.spring.dal.config.DevelopmentConfig; 5 | import com.microsoft.cse.reference.spring.dal.config.MongoConfig; 6 | import com.microsoft.cse.reference.spring.dal.controllers.PersonRepository; 7 | import com.microsoft.cse.reference.spring.dal.converters.BooleanToInteger; 8 | import com.microsoft.cse.reference.spring.dal.converters.IntegerToBoolean; 9 | import com.microsoft.cse.reference.spring.dal.models.Person; 10 | import org.bson.Document; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration; 16 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.data.mongodb.core.MongoTemplate; 19 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 20 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.Optional; 26 | 27 | import static org.hamcrest.CoreMatchers.is; 28 | import static org.hamcrest.CoreMatchers.nullValue; 29 | import static org.junit.Assert.assertThat; 30 | 31 | /** 32 | * Define the tests using the built-in DataMongoTest attribute 33 | * However, since the builtin doesn't load other beans, we need to load 34 | * the converters, and the config that loads the converters - we do that with @Import 35 | */ 36 | @RunWith(SpringRunner.class) 37 | @DataMongoTest 38 | @EnableWebSecurity 39 | @EnableResourceServer 40 | @Import({IntegerToBoolean.class, 41 | BooleanToInteger.class, 42 | MongoConfig.class, 43 | DevelopmentConfig.class}) 44 | public class PersonDataTests { 45 | @Autowired 46 | public MongoTemplate mongoTemplate; 47 | 48 | @Autowired 49 | public PersonRepository repo; 50 | 51 | @Before 52 | public void setUp() { 53 | this.mongoTemplate.dropCollection(Constants.DB_PERSON_COLLECTION); 54 | 55 | this.mongoTemplate.insert(Document.parse("{\n" + 56 | " \"nconst\" : \"nm0001500\",\n" + 57 | " \"primaryName\" : \"Karl Malden\",\n" + 58 | " \"birthYear\" : 1912,\n" + 59 | " \"deathYear\" : 2009,\n" + 60 | " \"primaryProfession\" : \"actor,soundtrack,director\",\n" + 61 | " \"knownForTitles\" : \"tt0047296,tt0066206,tt0048973,tt0044081\"\n" + 62 | " },"), Constants.DB_PERSON_COLLECTION); 63 | } 64 | 65 | @Test 66 | public void findById_Success() { 67 | Person actual = this.repo.findById("nm0001500").get(); 68 | 69 | assertThat(actual.nconst, is("nm0001500")); 70 | assertThat(actual.primaryName, is("Karl Malden")); 71 | assertThat(actual.birthYear, is(1912)); 72 | assertThat(actual.deathYear, is(2009)); 73 | assertThat(actual.primaryProfession, is(Arrays.asList("actor", "soundtrack", "director"))); 74 | assertThat(actual.knownForTitles, is(Arrays.asList("tt0047296", "tt0066206", "tt0048973", "tt0044081"))); 75 | } 76 | 77 | @Test 78 | public void findById_Failure() { 79 | Optional<Person> actual = this.repo.findById("not-real"); 80 | 81 | assertThat(actual.isPresent(), is(false)); 82 | } 83 | 84 | @Test 85 | public void findByPrimaryName_Success() { 86 | List<Person> actuals = this.repo.findByPrimaryName("Karl Malden"); 87 | 88 | assertThat(actuals.size(), is(1)); 89 | 90 | Person actual = actuals.get(0); 91 | 92 | assertThat(actual.nconst, is("nm0001500")); 93 | assertThat(actual.primaryName, is("Karl Malden")); 94 | assertThat(actual.birthYear, is(1912)); 95 | assertThat(actual.deathYear, is(2009)); 96 | assertThat(actual.primaryProfession, is(Arrays.asList("actor", "soundtrack", "director"))); 97 | assertThat(actual.knownForTitles, is(Arrays.asList("tt0047296", "tt0066206", "tt0048973", "tt0044081"))); 98 | } 99 | 100 | @Test 101 | public void findByPrimaryName_Failure() { 102 | List<Person> actuals = this.repo.findByPrimaryName("not real"); 103 | 104 | assertThat(actuals.size(), is(0)); 105 | } 106 | 107 | @Test 108 | public void deleteAll_Success() { 109 | this.repo.deleteAll(); 110 | 111 | assertThat(this.repo.count(), is(0L)); 112 | } 113 | 114 | @Test 115 | public void deleteById_Success() { 116 | this.repo.deleteById("nm0001500"); 117 | 118 | assertThat(this.repo.count(), is(0L)); 119 | } 120 | 121 | @Test 122 | public void insert_Success() { 123 | String id = "nm0000001"; 124 | Person newPerson = new Person(); 125 | newPerson.nconst = id; 126 | newPerson.birthYear = 2020; 127 | newPerson.primaryName = "Tim Tam"; 128 | newPerson.primaryProfession = Arrays.asList("Test", "Dancer"); 129 | 130 | assertThat(this.repo.insert(newPerson), is(newPerson)); 131 | assertThat(this.repo.count(), is(2L)); 132 | 133 | Person actual = this.repo.findById(id).get(); 134 | 135 | assertThat(actual.nconst, is(id)); 136 | assertThat(actual.primaryName, is("Tim Tam")); 137 | assertThat(actual.birthYear, is(2020)); 138 | assertThat(actual.deathYear, is(nullValue())); 139 | assertThat(actual.primaryProfession, is(Arrays.asList("Test", "Dancer"))); 140 | assertThat(actual.knownForTitles, is(nullValue())); 141 | } 142 | 143 | @Test 144 | public void update_Success() { 145 | Person update = this.repo.findById("nm0001500").get(); 146 | 147 | update.deathYear = 2010; 148 | update.primaryProfession.add("Test"); 149 | 150 | assertThat(this.repo.save(update), is(update)); 151 | 152 | Person actual = this.repo.findById("nm0001500").get(); 153 | assertThat(actual.nconst, is("nm0001500")); 154 | assertThat(actual.primaryName, is("Karl Malden")); 155 | assertThat(actual.birthYear, is(1912)); 156 | assertThat(actual.deathYear, is(2010)); 157 | assertThat(actual.primaryProfession, is(Arrays.asList("actor", "soundtrack", "director", "Test"))); 158 | assertThat(actual.knownForTitles, is(Arrays.asList("tt0047296", "tt0066206", "tt0048973", "tt0044081"))); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/unit/PrincipalDataTests.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.unit; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import com.microsoft.cse.reference.spring.dal.config.DevelopmentConfig; 5 | import com.microsoft.cse.reference.spring.dal.config.MongoConfig; 6 | import com.microsoft.cse.reference.spring.dal.controllers.PrincipalRepository; 7 | import com.microsoft.cse.reference.spring.dal.converters.*; 8 | import com.microsoft.cse.reference.spring.dal.models.Principal; 9 | import org.bson.Document; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 15 | import org.springframework.context.annotation.Import; 16 | import org.springframework.data.mongodb.core.MongoTemplate; 17 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 18 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 19 | import org.springframework.test.context.junit4.SpringRunner; 20 | 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Optional; 24 | 25 | import static org.hamcrest.CoreMatchers.is; 26 | import static org.hamcrest.CoreMatchers.nullValue; 27 | import static org.junit.Assert.assertThat; 28 | 29 | /** 30 | * Define the tests using the built-in DataMongoTest attribute 31 | * However, since the builtin doesn't load other beans, we need to load 32 | * the converters, and the config that loads the converters - we do that with @Import 33 | */ 34 | @RunWith(SpringRunner.class) 35 | @DataMongoTest 36 | @EnableWebSecurity 37 | @EnableResourceServer 38 | @Import({EmptyStringToNull.class, 39 | NullToEmptyString.class, 40 | JsonArrayToStringList.class, 41 | MongoConfig.class, 42 | DevelopmentConfig.class}) 43 | public class PrincipalDataTests { 44 | @Autowired 45 | public MongoTemplate mongoTemplate; 46 | 47 | @Autowired 48 | public PrincipalRepository repo; 49 | 50 | @Before 51 | public void setUp() { 52 | this.mongoTemplate.dropCollection(Constants.DB_PRINCIPAL_COLLECTION); 53 | 54 | this.mongoTemplate.insert(Document.parse("{\n" + 55 | " \"tconst\" : \"tt0000442\",\n" + 56 | " \"ordering\" : 1,\n" + 57 | " \"nconst\" : \"nm0622273\",\n" + 58 | " \"category\" : \"actress\",\n" + 59 | " \"job\" : \"\",\n" + 60 | " \"characters\" : \"[\\\"Barnemordersken\\\"]\"\n" + 61 | " },"), Constants.DB_PRINCIPAL_COLLECTION); 62 | } 63 | 64 | @Test 65 | public void storageOperation_Success() { 66 | // we aren't really testing findAll here, we're testing that 67 | // our model inflates as expected - hence the name storageOperation 68 | Principal actual = this.repo.findAll().get(0); 69 | 70 | assertThat(actual.tconst, is("tt0000442")); 71 | assertThat(actual.ordering, is(1)); 72 | assertThat(actual.nconst, is("nm0622273")); 73 | assertThat(actual.category, is("actress")); 74 | assertThat(actual.job, is(nullValue())); 75 | assertThat(actual.characters, is(Arrays.asList("Barnemordersken"))); 76 | } 77 | 78 | @Test 79 | public void findById_Failure() { 80 | Optional<Principal> actual = this.repo.findById("not-real"); 81 | 82 | assertThat(actual.isPresent(), is(false)); 83 | } 84 | 85 | @Test 86 | public void findByNconst_Success() { 87 | List<Principal> actuals = this.repo.findByNconst("nm0622273"); 88 | 89 | assertThat(actuals.size(), is(1)); 90 | 91 | Principal actual = actuals.get(0); 92 | 93 | assertThat(actual.tconst, is("tt0000442")); 94 | assertThat(actual.ordering, is(1)); 95 | assertThat(actual.nconst, is("nm0622273")); 96 | assertThat(actual.category, is("actress")); 97 | assertThat(actual.job, is(nullValue())); 98 | assertThat(actual.characters, is(Arrays.asList("Barnemordersken"))); 99 | } 100 | 101 | @Test 102 | public void findByNconst_Failure() { 103 | List<Principal> actuals = this.repo.findByNconst("not real"); 104 | 105 | assertThat(actuals.size(), is(0)); 106 | } 107 | 108 | @Test 109 | public void findByTconst_Success() { 110 | List<Principal> actuals = this.repo.findByTconst("tt0000442"); 111 | 112 | assertThat(actuals.size(), is(1)); 113 | 114 | Principal actual = actuals.get(0); 115 | 116 | assertThat(actual.tconst, is("tt0000442")); 117 | assertThat(actual.ordering, is(1)); 118 | assertThat(actual.nconst, is("nm0622273")); 119 | assertThat(actual.category, is("actress")); 120 | assertThat(actual.job, is(nullValue())); 121 | assertThat(actual.characters, is(Arrays.asList("Barnemordersken"))); 122 | } 123 | 124 | @Test 125 | public void findByTconst_Failure() { 126 | List<Principal> actuals = this.repo.findByTconst("not real"); 127 | 128 | assertThat(actuals.size(), is(0)); 129 | } 130 | 131 | @Test 132 | public void deleteAll_Success() { 133 | this.repo.deleteAll(); 134 | 135 | assertThat(this.repo.count(), is(0L)); 136 | } 137 | 138 | @Test 139 | public void insert_Success() { 140 | String id = "tt0000001"; 141 | Principal newPrincipal = new Principal(); 142 | newPrincipal.tconst = id; 143 | newPrincipal.nconst = "nm0000001"; 144 | 145 | assertThat(this.repo.insert(newPrincipal), is(newPrincipal)); 146 | assertThat(this.repo.count(), is(2L)); 147 | 148 | Principal actual = this.repo.findAll().get(1); 149 | 150 | assertThat(actual.tconst, is("tt0000001")); 151 | assertThat(actual.ordering, is(nullValue())); 152 | assertThat(actual.nconst, is("nm0000001")); 153 | assertThat(actual.category, is(nullValue())); 154 | assertThat(actual.job, is(nullValue())); 155 | assertThat(actual.characters, is(nullValue())); 156 | } 157 | 158 | @Test 159 | public void update_Success() { 160 | Principal update = this.repo.findAll().get(0); 161 | 162 | update.job = "Test"; 163 | update.characters.add("Test"); 164 | 165 | assertThat(this.repo.save(update), is(update)); 166 | 167 | Principal actual = this.repo.findAll().get(0); 168 | assertThat(actual.tconst, is("tt0000442")); 169 | assertThat(actual.ordering, is(1)); 170 | assertThat(actual.nconst, is("nm0622273")); 171 | assertThat(actual.category, is("actress")); 172 | assertThat(actual.job, is("Test")); 173 | assertThat(actual.characters, is(Arrays.asList("Barnemordersken", "Test"))); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /api/src/test/java/com/microsoft/cse/reference/spring/dal/unit/TitleDataTests.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.cse.reference.spring.dal.unit; 2 | 3 | import com.microsoft.cse.reference.spring.dal.config.Constants; 4 | import com.microsoft.cse.reference.spring.dal.config.DevelopmentConfig; 5 | import com.microsoft.cse.reference.spring.dal.config.MongoConfig; 6 | import com.microsoft.cse.reference.spring.dal.controllers.TitleRepository; 7 | import com.microsoft.cse.reference.spring.dal.converters.BooleanToInteger; 8 | import com.microsoft.cse.reference.spring.dal.converters.IntegerToBoolean; 9 | import com.microsoft.cse.reference.spring.dal.models.Title; 10 | import org.bson.Document; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 16 | import org.springframework.context.annotation.Import; 17 | import org.springframework.data.mongodb.core.MongoTemplate; 18 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 19 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 20 | import org.springframework.test.context.junit4.SpringRunner; 21 | 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.Optional; 25 | 26 | import static org.hamcrest.CoreMatchers.is; 27 | import static org.hamcrest.CoreMatchers.nullValue; 28 | import static org.junit.Assert.assertThat; 29 | 30 | /** 31 | * Define the tests using the built-in DataMongoTest attribute 32 | * However, since the builtin doesn't load other beans, we need to load 33 | * the converters, and the config that loads the converters - we do that with @Import 34 | */ 35 | @RunWith(SpringRunner.class) 36 | @DataMongoTest 37 | @EnableWebSecurity 38 | @EnableResourceServer 39 | @Import({IntegerToBoolean.class, 40 | BooleanToInteger.class, 41 | MongoConfig.class, 42 | DevelopmentConfig.class}) 43 | public class TitleDataTests { 44 | @Autowired 45 | public MongoTemplate mongoTemplate; 46 | 47 | @Autowired 48 | public TitleRepository repo; 49 | 50 | @Before 51 | public void setUp() { 52 | this.mongoTemplate.dropCollection(Constants.DB_TITLE_COLLECTION); 53 | 54 | this.mongoTemplate.insert(Document.parse("{\n" + 55 | " \"tconst\" : \"tt0075472\",\n" + 56 | " \"titleType\" : \"tvSeries\",\n" + 57 | " \"primaryTitle\" : \"All Creatures Great and Small\",\n" + 58 | " \"originalTitle\" : \"All Creatures Great and Small\",\n" + 59 | " \"isAdult\" : 0,\n" + 60 | " \"startYear\" : 1978,\n" + 61 | " \"endYear\" : 1990,\n" + 62 | " \"runtimeMinutes\" : 50,\n" + 63 | " \"genres\" : \"Comedy,Drama\"\n" + 64 | " }"), Constants.DB_TITLE_COLLECTION); 65 | } 66 | 67 | @Test 68 | public void findById_Success() { 69 | Title actual = this.repo.findById("tt0075472").get(); 70 | 71 | assertThat(actual.tconst, is("tt0075472")); 72 | assertThat(actual.titleType, is("tvSeries")); 73 | assertThat(actual.primaryTitle, is("All Creatures Great and Small")); 74 | assertThat(actual.originalTitle, is("All Creatures Great and Small")); 75 | assertThat(actual.isAdult, is(false)); 76 | assertThat(actual.startYear, is(1978)); 77 | assertThat(actual.endYear, is(1990)); 78 | assertThat(actual.runtimeMinutes, is(50)); 79 | assertThat(actual.genres, is(Arrays.asList("Comedy", "Drama"))); 80 | } 81 | 82 | @Test 83 | public void findById_Failure() { 84 | Optional<Title> actual = this.repo.findById("not-real"); 85 | 86 | assertThat(actual.isPresent(), is(false)); 87 | } 88 | 89 | @Test 90 | public void findByPrimaryTitle_Success() { 91 | List<Title> actuals = this.repo.findByPrimaryTitle("All Creatures Great and Small"); 92 | 93 | assertThat(actuals.size(), is(1)); 94 | 95 | Title actual = actuals.get(0); 96 | 97 | assertThat(actual.tconst, is("tt0075472")); 98 | assertThat(actual.titleType, is("tvSeries")); 99 | assertThat(actual.primaryTitle, is("All Creatures Great and Small")); 100 | assertThat(actual.originalTitle, is("All Creatures Great and Small")); 101 | assertThat(actual.isAdult, is(false)); 102 | assertThat(actual.startYear, is(1978)); 103 | assertThat(actual.endYear, is(1990)); 104 | assertThat(actual.runtimeMinutes, is(50)); 105 | assertThat(actual.genres, is(Arrays.asList("Comedy", "Drama"))); 106 | } 107 | 108 | @Test 109 | public void findByPrimaryTitle_Failure() { 110 | List<Title> actuals = this.repo.findByPrimaryTitle("not real"); 111 | 112 | assertThat(actuals.size(), is(0)); 113 | } 114 | 115 | @Test 116 | public void deleteAll_Success() { 117 | this.repo.deleteAll(); 118 | 119 | assertThat(this.repo.count(), is(0L)); 120 | } 121 | 122 | @Test 123 | public void deleteById_Success() { 124 | this.repo.deleteById("tt0075472"); 125 | 126 | assertThat(this.repo.count(), is(0L)); 127 | } 128 | 129 | @Test 130 | public void insert_Success() { 131 | String id = "tt0000001"; 132 | Title newTitle = new Title(); 133 | newTitle.tconst = id; 134 | newTitle.originalTitle = "Test Movie"; 135 | newTitle.isAdult = true; 136 | newTitle.genres = Arrays.asList("Comedy", "Horror"); 137 | 138 | assertThat(this.repo.insert(newTitle), is(newTitle)); 139 | assertThat(this.repo.count(), is(2L)); 140 | 141 | Title actual = this.repo.findById(id).get(); 142 | 143 | assertThat(actual.tconst, is(id)); 144 | assertThat(actual.titleType, is(nullValue())); 145 | assertThat(actual.primaryTitle, is(nullValue())); 146 | assertThat(actual.originalTitle, is("Test Movie")); 147 | assertThat(actual.isAdult, is(true)); 148 | assertThat(actual.startYear, is(nullValue())); 149 | assertThat(actual.endYear, is(nullValue())); 150 | assertThat(actual.runtimeMinutes, is(nullValue())); 151 | assertThat(actual.genres, is(Arrays.asList("Comedy", "Horror"))); 152 | } 153 | 154 | @Test 155 | public void update_Success() { 156 | Title update = this.repo.findById("tt0075472").get(); 157 | 158 | update.isAdult = true; 159 | update.genres.add("Test"); 160 | 161 | assertThat(this.repo.save(update), is(update)); 162 | 163 | Title actual = this.repo.findById("tt0075472").get(); 164 | assertThat(actual.tconst, is("tt0075472")); 165 | assertThat(actual.titleType, is("tvSeries")); 166 | assertThat(actual.primaryTitle, is("All Creatures Great and Small")); 167 | assertThat(actual.originalTitle, is("All Creatures Great and Small")); 168 | assertThat(actual.isAdult, is(true)); 169 | assertThat(actual.startYear, is(1978)); 170 | assertThat(actual.endYear, is(1990)); 171 | assertThat(actual.runtimeMinutes, is(50)); 172 | assertThat(actual.genres, is(Arrays.asList("Comedy", "Drama", "Test"))); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # imdb data downloads 2 | *.tsv 3 | *.tsv.gz -------------------------------------------------------------------------------- /data/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use this Dockerfile to upload the sample data without needing to install the dependencies locally in your system. 2 | # - build the docker image: docker build . 3 | # - run the docker image: docker run -it <image id from previous step> 4 | 5 | FROM ubuntu:xenial 6 | 7 | # Update and Isntall Dependencies 8 | RUN apt-get update && \ 9 | apt-get install apt-transport-https lsb-release software-properties-common dirmngr git mongodb curl ed -y 10 | 11 | # Azure deps 12 | RUN AZ_REPO=$(lsb_release -cs) && \ 13 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \ 14 | tee /etc/apt/sources.list.d/azure-cli.list 15 | RUN apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv \ 16 | --keyserver packages.microsoft.com \ 17 | --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF 18 | 19 | # Mongo deps 20 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 21 | RUN echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.0 multiverse" | \ 22 | tee /etc/apt/sources.list.d/mongodb-org-4.0.list 23 | 24 | # Update + Install Mongo and Azure CLI tools. 25 | RUN apt-get update 26 | RUN apt-get install azure-cli mongodb-org-tools -y 27 | 28 | # Clone Code Repo 29 | RUN git clone https://github.com/Microsoft/containers-rest-cosmos-appservice-java.git 30 | WORKDIR /containers-rest-cosmos-appservice-java/data 31 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | ## Deploying Data 4 | This project uses CosmosDB to store our sample data. CosmosDB is a global resource that is provisioned using the Global Resources in this [directory]( https://github.com/Microsoft/containers-rest-cosmos-appservice-java/tree/master/infrastructure/global-resources). 5 | 6 | ## Creating and Importing sample data in CosmosDB. 7 | 8 | A Dockerfile has also been included in this directory to simplify the upload of the sample data without needing to install dependencies locally in your system. 9 | 10 | 1. Build the docker image: 11 | ``` bash 12 | $ docker build -t cosmos-import . 13 | ``` 14 | 15 | 2. Either run the docker image: 16 | ``` bash 17 | $ docker run -it cosmos-import bash 18 | ``` 19 | 20 | 3. Or run your local data directory into the container (helpful for testing changes on the script) use: 21 | ``` 22 | $ docker run -it --mount src="$(pwd)",target=/containers-rest-cosmos-appservice-java/data,type=bind cosmos-import bash 23 | ``` 24 | 25 | 4. Execute the all-in-on import script inside the running container: 26 | ``` bash 27 | chmod +x cosmos-import.sh 28 | ./cosmos-import.sh 29 | ``` -------------------------------------------------------------------------------- /data/cosmos-import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Executes all the commands required to create and import the data to CosmosDB 4 | az login 5 | ./load_env.sh 6 | source ./vars.env 7 | ./getdata.sh 8 | ./importdata.sh -------------------------------------------------------------------------------- /data/getdata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | printf "\nRunning the getdata.sh script.\n" 3 | 4 | # Accesses the IMDb data that gets aggregrated daily 5 | read -p "Confirm running by hitting the [enter] key" userInput 6 | if [[ -z "${userInput}" ]]; then 7 | echo "Getting IMDb data files (updated daily)..." 8 | curl --remote-name-all https://datasets.imdbws.com/{name.basics.tsv.gz,title.basics.tsv.gz,title.principals.tsv.gz} || ( echo "Failed to access data sets. Exiting" && exit 1 ) 9 | fi 10 | 11 | # Unzips the compressed downloads 12 | read -p "Press the [enter] key to continue" userInput 13 | if [[ -z "${userInput}" ]]; then 14 | echo "Unzipping the data files (overwrites any existing .tsv files)..." 15 | gunzip -vf *.gz || ( echo "Failed to unzip data sets. Exiting" && exit 1 ) 16 | fi 17 | 18 | # Scrubs bad values from the data set 19 | read -p "Press the [enter] key to continue" userInput 20 | if [[ -z "${userInput}" ]]; then 21 | printf "\n" 22 | echo "Removing IMDb '\N' values..." 23 | ed -s *.tsv <<< $',s|\\\N||g\nw' || ( echo "Failed to fix bad values in data sets. Exiting" && exit 1 ) 24 | fi 25 | 26 | # Shows the user the files 27 | printf "\n" 28 | echo "IMDb data files ready:" 29 | ls *.tsv 30 | -------------------------------------------------------------------------------- /data/importdata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ################################################################################ 5 | # Variables 6 | ################################################################################ 7 | 8 | resourceGroup=$RESOURCE_GROUP 9 | cosmosName=$COSMOSDB_NAME 10 | password=$COSMOSDB_PASSWORD 11 | databaseName=IMDb 12 | files=("title.basics.tsv" "name.basics.tsv" "title.principals.tsv") 13 | collections=(titles names principals_mapping) 14 | keys=("tconst" "nconst" "tconst") 15 | 16 | len=${#collections[@]} 17 | 18 | ################################################################################ 19 | # Helpers 20 | ################################################################################ 21 | confirm_locations() { 22 | # If the above key variables are empty, exit and suggest rerunning load_env.sh 23 | if [[ -z "${resourceGroup}" ]] || [[ -z "${cosmosName}" ]] || [[ -z "${password}" ]]; then 24 | echo "There are no set environment variables." 25 | echo "Please (re)run load_env.sh" 26 | exit 1 27 | fi 28 | 29 | # Check if the resource group is correctly set 30 | printf "Is this the correct resource group name? [%s] Y,N (leave blank for Y): " "${resourceGroup}" 31 | read userInput 32 | 33 | if [[ -z "${userInput}" ]] || [[ "${userInput}" =~ "Y" ]] || [[ "${userInput}" == "y" ]]; then 34 | echo "Resource group confirmed." 35 | elif [[ "${userInput}" == "N" ]] || [[ "${userInput}" == "n" ]]; then 36 | printf "\tPlease enter the correct resource group: " 37 | read userInput 38 | resourceGroup=${userInput} 39 | 40 | # Check to see if the provided resource group actually exists 41 | echo "Confirming whether the resource group '${resourceGroup}' exists..." 42 | resGroupExists="$(az cosmosdb check-name-exists -n "${resourceGroup}")" 43 | if [[ "${resGroupExists}" == "false" ]]; then 44 | echo "Exiting. Provided resource name does not exist. Rerun script." 45 | exit 1 46 | fi 47 | else 48 | echo "Exiting. Rerun this script or load_env.sh for best results." 49 | exit 1 50 | fi 51 | 52 | # Check if the CosmosDB account name is correctly set 53 | # If the account name is incorrect, then prompting for the access key follows up 54 | printf "Is this the correct CosmosDB account name? [%s] Y,N (leave blank for Y): " "${cosmosName}" 55 | read userInput 56 | 57 | if [[ -z "${userInput}" ]] || [[ "${userInput}" == "Y" ]] || [[ "${userInput}" == "y" ]]; then 58 | echo "Account name confirmed." 59 | elif [[ "${userInput}" == "N" ]] || [[ "${userInput}" == "n" ]]; then 60 | printf "\tPlease enter the correct CosmosDB account name: " 61 | read userInput 62 | cosmosName=${userInput} 63 | printf "\tPlease enter the associated primary key: " 64 | read userInput 65 | password="${userInput}" 66 | 67 | # Check to see if the provided cosmosName actually exists 68 | echo "Confirming whether the account '${cosmosName}' exists..." 69 | accountExists="$(az cosmosdb check-name-exists -n "${cosmosName}")" 70 | if [[ "${accountExists}" == "false" ]]; then 71 | echo "Exiting. Provided account name does not exist. Rerun script." 72 | exit 1 73 | fi 74 | else 75 | echo "Exiting. Rerun this script or load_env.sh for best results." 76 | exit 1 77 | fi 78 | } 79 | 80 | create_database() { 81 | printf "\n" 82 | echo "Checking whether the database '${databaseName}' already exists..." 83 | 84 | # Check to see if the database we're creating exists already 85 | dbExists="$(az cosmosdb database exists --db-name "${databaseName}" -n "${cosmosName}" --key "${password}")" 86 | 87 | if [[ "${dbExists}" = "false" ]]; then 88 | echo "Creating database '${databaseName}'..." 89 | 90 | # Try to create the database 91 | az cosmosdb database create -g "${resourceGroup}" -n "${cosmosName}" --db-name "${databaseName}" > /dev/null 92 | if [ "$?" != "0" ]; then 93 | # Catch any errors if the creation failed 94 | echo "Error when creating the database" 1>&2 95 | exit 1 96 | fi 97 | 98 | echo "Database '${databaseName}' has been created." 99 | else 100 | echo "Database '${databaseName}' already exists. Moving on..." 101 | fi 102 | } 103 | 104 | create_collections() { 105 | # Go through each collection and see whether it exists already 106 | for ((i=0; i<len; i++)); do 107 | step=$((i + 1)) 108 | echo "Checking whether the collection '${collections[i]}' exists..." 109 | 110 | collExists="$(az cosmosdb collection exists -c "${collections[i]}" -d "${databaseName}" -n "${cosmosName}" -g "${resourceGroup}")" 111 | 112 | if [[ "${collExists}" = "false" ]]; then 113 | echo "(${step} of ${len}) Creating collection '${collections[i]}'" 114 | 115 | # Create the collections with a high throughput since we'll be uploading a lot of data 116 | partition="/'\$v'/${keys[$i]}/'\$v'" 117 | az cosmosdb collection create -g "${resourceGroup}" -n "${cosmosName}" --db-name "${databaseName}" --collection-name "${collections[$i]}" \ 118 | --partition-key-path "${partition}" --throughput 100000 > /dev/null 119 | 120 | if [ "$?" != "0" ]; then 121 | echo "Error when creating the collection '${collections[$i]}'" 1>&2 122 | exit 1 123 | fi 124 | 125 | echo "Collection '${collections[i]}' has been created." 126 | else 127 | echo "Collection '${collections[i]}' already exists. Moving on..." 128 | fi 129 | done 130 | } 131 | 132 | delete_tsv_files() { 133 | # Clean up the download TSVs 134 | for ((i=0; i<len; i++)); do 135 | rm -v "${files[$i]}" 136 | done 137 | } 138 | 139 | import_data() { 140 | for ((i=0; i<len; i++)); do 141 | # For each file upload the data using the MongoImport Tool 142 | step=$((i + 1)) 143 | echo 144 | echo "(${step} of ${len}) Importing data from file '${files[$i]}' to collection '${collections[$i]}'..." 145 | 146 | hostName="${cosmosName}.documents.azure.com:10255" 147 | user=${cosmosName} 148 | 149 | mongoimport --host "${hostName}" -u "${user}" -p "${password}" --ssl --sslAllowInvalidCertificates --type tsv --headerline \ 150 | --db "${databaseName}" --collection "${collections[$i]}" --numInsertionWorkers 40 --file "${files[$i]}" 151 | 152 | if [ "$?" != "0" ]; then 153 | echo "Error while importing from file '${files[$i]}'" 1>&2 154 | exit 1 155 | fi 156 | 157 | echo 158 | echo "Import to '${collections[$i]}' is complete" 159 | done 160 | } 161 | 162 | set_throughput() { 163 | # Once we're done, throttle back the throughput to the low side - you can always increase it later 164 | RUs=1700 165 | for ((i=0; i<len; i++)); do 166 | echo 167 | echo "Setting '${collections[$i]}' throughput to ${RUs} to reduce cost..." 168 | az cosmosdb collection update -g "${resourceGroup}" -n "${cosmosName}" --db-name "${databaseName}" --collection-name "${collections[$i]}" --throughput "${RUs}" 169 | 170 | if [ "$?" != "0" ]; then 171 | echo "Error while updating the throughput on collection '${collections[$i]}'" 1>&2 172 | exit 1 173 | fi 174 | done 175 | } 176 | 177 | ################################################################################ 178 | # Main script 179 | ################################################################################ 180 | set -e 181 | 182 | printf "\nStep 1. Confirming locations...\n" 183 | confirm_locations 184 | 185 | printf "\nStep 2. Creating Cosmos DB database...\n" 186 | create_database 187 | 188 | printf "\nStep 3. Creating Cosmos DB collections...\n" 189 | create_collections 190 | 191 | printf "Step 4. Importing IMDb data to Cosmos DB...\n" 192 | import_data 193 | 194 | printf "Step 5. Finished importing data. Cleaning up...\n" 195 | delete_tsv_files 196 | 197 | printf "\nStep 6. Reducing throughput on Azure...\n" 198 | set_throughput 199 | printf "This may take 10 minutes to be reflected in the portal." 200 | 201 | printf "\nComplete!" 202 | 203 | -------------------------------------------------------------------------------- /data/load_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # -e: immediately exit if any command has a non-zero exit status 6 | # -o: prevents errors in a pipeline from being masked 7 | # IFS new value is less likely to cause confusing bugs when looping arrays or arguments (e.g. $@) 8 | 9 | #****************************************************************************** 10 | # Script to set environment variables 11 | #****************************************************************************** 12 | 13 | usage() { echo "Usage: $0 -i <subscriptionId> -g <resourceGroupName> -n <appName> -l <resourceGroupLocation>" 1>&2; exit 1; } 14 | 15 | declare subscriptionId="" 16 | declare resourceGroupName="" 17 | declare resourceGroupLocation="" 18 | declare dbName="" 19 | declare connString="" 20 | declare dbPassword="" 21 | 22 | # Initialize parameters specified from command line 23 | while getopts ":i:g:n:l:" arg; do 24 | case "${arg}" in 25 | i) 26 | subscriptionId=${OPTARG} 27 | ;; 28 | g) 29 | resourceGroupName=${OPTARG} 30 | ;; 31 | l) 32 | resourceGroupLocation=${OPTARG} 33 | ;; 34 | d) 35 | dbName=${OPTARG} 36 | esac 37 | done 38 | shift $((OPTIND-1)) 39 | 40 | #****************************************************************************** 41 | # Helper functions 42 | #****************************************************************************** 43 | 44 | azLogin() { 45 | ( 46 | set +e 47 | # Login to azure using your credentials 48 | az account show &> /dev/null 49 | 50 | if [ $? != 0 ]; 51 | then 52 | echo "Azure login required..." 53 | az login -o table 54 | else 55 | az account list -o table 56 | fi 57 | ) 58 | } 59 | 60 | validatedRead() { 61 | # Take a prompt for the user, the regex pattern and the error to return if the input fails to pattern-match 62 | prompt=$1 63 | regex=$2 64 | error=$3 65 | 66 | userInput="" 67 | while [[ ! ${userInput} =~ ${regex} ]]; do 68 | if [[ (-n ${userInput}) ]]; then 69 | printf "'%s' is not valid. %s\n" ${userInput} ${error} 70 | fi 71 | read -p "${prompt}" userInput 72 | done 73 | } 74 | 75 | readSubscriptionId() { 76 | # Display the subscriptions associated with the User. User picks, and we validate that choice 77 | 78 | currentSub="$(az account show -o tsv | cut -f2)" 79 | subNames="$(az account list -o tsv | cut -f4)" 80 | subIds="$(az account list -o tsv | cut -f2)" 81 | 82 | while ([[ -z "${subscriptionId}" ]]); do 83 | printf "Enter your subscription ID [%s]: " "${currentSub}" 84 | read userInput 85 | 86 | if [[ (-z "${userInput}") && (-n "${currentSub}")]]; then 87 | userInput=${currentSub} 88 | fi 89 | 90 | set +e 91 | nameExists="$(echo "${subNames}" | grep "${userInput}")" 92 | idExists="$(echo "${subIds}" | grep "${userInput}")" 93 | 94 | if [[ (-z "${nameExists}") && (-z "${idExists}") ]]; then 95 | printf "'%s' is not a valid subscription name or ID.\n" "${userInput}" 96 | else 97 | subscriptionId=${userInput} 98 | printf "Using subscription '%s'...\n" "${subscriptionId}" 99 | fi 100 | done 101 | } 102 | 103 | readResourceGroupName() { 104 | # Display a list of resource groups available to the signed in user 105 | # User picks, and we validate that choice 106 | 107 | printf "Existing resource groups:\n" 108 | groups="$(az group list -o tsv | cut -f4 | tr '\n' ', ' | sed "s/,/, /g")" 109 | printf "\n%s\n" "${groups%??}" 110 | validatedRead "Enter a resource group name: " "^[a-zA-Z0-9_-]+$" "Only letters, numbers and underscores are allowed." 111 | resourceGroupName=${userInput} 112 | 113 | set +e 114 | 115 | # Check for existing RG 116 | az group exists --name "${resourceGroupName}" &> /dev/null 117 | if [ "$?" == "false" ]; then 118 | echo "To create a new resource group, please enter an Azure location:" 119 | readLocation 120 | 121 | (set -ex; az group create --name "${resourceGroupName}" --location "${resourceGroupLocation}") 122 | else 123 | resourceGroupLocation="$(az group show -n "${resourceGroupName}" -o tsv | cut -f2)" 124 | printf "Using resource group '%s'...\n" "${resourceGroupName}" 125 | fi 126 | 127 | set -e 128 | } 129 | 130 | readLocation() { 131 | # For use when creating a new database 132 | # Displays a list of available database replication origins under the resource group 133 | # User picks, then we validate that choice 134 | 135 | if [[ -z "${resourceGroupLocation}" ]]; then 136 | locations="$(az account list-locations --output tsv | cut -f5 | tr '\n' ', ' | sed "s/,/, /g")" 137 | printf "\n%s\n" "${locations%??}" 138 | 139 | declare locationExists 140 | while ([[ -z ${resourceGroupLocation} ]]); do 141 | validatedRead "Enter resource group location: " "^[a-zA-Z0-9]+$" "Only letters & numbers are allowed." 142 | locationExists="$(echo "${locations}" | grep "${userInput}")" 143 | if [[ -z ${locationExists} ]]; then 144 | printf "'%s' is not a valid location.\n" "${userInput}" 145 | else 146 | resourceGroupLocation=${userInput} 147 | printf "Using resource group '%s'...\n" "${resourceGroupName}" 148 | fi 149 | done 150 | fi 151 | } 152 | 153 | readDbName() { 154 | # Displays the list of available databases under the previously specified resource group 155 | # User then picks their desired and we validate that choice (in case of typo, etc.) 156 | 157 | dbNames="$(az cosmosdb list -g ${resourceGroupName} -o tsv | cut -f13)" 158 | defaultDb=(${dbNames[@]}) 159 | 160 | while ([[ -z "${dbName}" ]]); do 161 | printf "Cosmos DB instances in group '%s':\n\n" "${resourceGroupName}" 162 | dbNames="$(az cosmosdb list -g "${resourceGroupName}" -o tsv | cut -f13 | tr '\n' ', ' | sed "s/,/, /g")" 163 | printf "%s\n\n" "${dbNames%??}" 164 | 165 | printf "Enter the Cosmos DB name [%s]: " "${defaultDb}" 166 | read userInput 167 | 168 | if [[ (-z "${userInput}") && (-n "${defaultDb}")]]; then 169 | userInput=${defaultDb} 170 | fi 171 | 172 | set +e 173 | nameExists="$(az cosmosdb check-name-exists -n "${userInput}")" 174 | 175 | if [[ "${nameExists}" == "false" ]]; then 176 | printf "'%s' is not a valid Cosmos DB name.\n" "${userInput}" 177 | else 178 | dbName=${userInput} 179 | printf "Using database '%s'...\n" "${dbName}" 180 | 181 | fi 182 | done 183 | } 184 | 185 | #****************************************************************************** 186 | # Script to set environment variables 187 | #****************************************************************************** 188 | 189 | azLogin 190 | 191 | # Prompt for parameters if some required parameters are missing 192 | if [[ -z "${subscriptionId}" ]]; then 193 | echo 194 | readSubscriptionId 195 | fi 196 | 197 | # Set the default subscription id 198 | az account set --subscription "${subscriptionId}" 199 | 200 | if [[ -z "${resourceGroupName}" ]]; then 201 | echo 202 | readResourceGroupName 203 | fi 204 | 205 | if [[ -z "${dbName}" ]]; then 206 | echo 207 | readDbName 208 | fi 209 | 210 | # At this time, list-connection-strings does not support '-o tsv', so this command uses sed to extract the connection string from json results 211 | connString="$(az cosmosdb list-connection-strings --name "${dbName}" -g "${resourceGroupName}" | sed -n -e '4 p' | sed -E -e 's/.*mongo(.*)true.*/mongo\1true/')" 212 | # But list-keys does support `-o tsv` 213 | dbPassword="$(az cosmosdb list-keys --resource-group "${resourceGroupName}" --name "${dbName}" -o tsv | sed -e 's/\s.*$//')" 214 | 215 | touch vars.env 216 | echo "export RESOURCE_GROUP=${resourceGroupName}" > vars.env 217 | echo "export COSMOSDB_NAME=${dbName}" >> vars.env 218 | echo "export COSMOSDB_PASSWORD=${dbPassword}" >> vars.env 219 | # Creates a distinction - some of these ENV variables will be used exclusively to connect to an Azure CosmosDB instance, 220 | # but in an Azure-agnostic setup, the DB_NAME and DB_CONNSTR may not be on CosmosDB 221 | echo "export DB_NAME=${dbName}" >> vars.env 222 | echo "export DB_CONNSTR=${connString}" >> vars.env 223 | printf "\nVariables written to file 'vars.env'\n" 224 | 225 | -------------------------------------------------------------------------------- /docs/azureActiveDirectory.md: -------------------------------------------------------------------------------- 1 | # Azure Active Directory 2 | 3 | > Note: This document roughly follows [this tutorial](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v1-add-azure-ad-app). 4 | 5 | To enable authentication, one may wish to use [Azure Active Directory (AAD)](https://azure.microsoft.com/en-us/services/active-directory/). 6 | This document will describe how to configure and leverage AAD for this application. 7 | 8 | ## Setup AAD App 9 | 10 | > TL;DR: After these steps you should have two values noted - an `Application ID` and an `App ID URI`. 11 | 12 | 1. Login to [Azure Portal](https://portal.azure.com) 13 | 2. Select "Azure Active Directory" from the portal left-hand sidebar 14 | 3. Select "App Registrations" from the left-most panel 15 | 4. Select "New Application" from the top panel 16 | 5. Name your application, and enter a valid url for sign-on (hint: we use the url of our hosted service) 17 | 6. When the application has been created, the newly visible panel will display its "Application ID" - please note its value 18 | 7. Select "Settings" from the top panel 19 | 8. Select "Properties" from the right-most panel 20 | 9. Find "App ID URI" in the panel, note its value 21 | 10. From the main AAD page toggle the `Multi-tenanted` switch to `Yes`. This step is required if you plan on using the accompanying [UI](../ui) 22 | 23 | ## Configure SpringDAL 24 | 25 | > Note: Authentication is only enabled in the `production` profile. 26 | > TL;DR: After these steps you should have two application environment variables set, `security.oauth2.resource.jwk.key-set-uri` and `security.oauth2.resource.id`. 27 | 28 | Ensure that the `security.oauth2.resource.jwk.key-set-uri` environment variable is set to `https://login.microsoftonline.com/common/discovery/keys` - this is because a common key set is used for all Azure Active Directory applications. This will validate that your service only allows valid AAD tokens, but it won't yet verify that the token is for your application. 29 | 30 | To ensure the token was granted for your application, you must set `security.oauth2.resource.id` to your `App ID URI` as noted [above](#setup-aad-app). 31 | 32 | ## Test Authentication 33 | 34 | > Note: Authentication is only enabled in the `production` profile. 35 | 36 | To ensure authentication is working properly, we need to issue ourselves a token and validate it works. To do so, we'll use [Postman](https://www.getpostman.com/). Please download and install it now. 37 | 38 | ### Configure AAD Test information 39 | 40 | > TL;DR: After these steps you should have one value noted - a `Value`. 41 | 42 | 1. Login to [Azure Portal](https://portal.azure.com) 43 | 2. Select "Azure Active Directory" from the portal left-hand sidebar 44 | 3. Select "App Registrations" from the left-most panel 45 | 4. Enter your `Application ID` as noted above, into the search field 46 | 5. Click your application, to enter its panel 47 | 6. Select "Settings" from the top panel 48 | 7. Select "Reply URLs" from the right-most panel 49 | 8. Ensure `https://www.getpostman.com/oauth2/callback` is the only reply URL entry 50 | 9. Select "Save" from the top panel 51 | 10. Close the "Reply URLs" panel by selecting the "x" in the top right 52 | 11. Select "Settings" from the top panel 53 | 12. Select "Keys" from the right-most panel 54 | 13. Under "Passwords" type a new "Key Description" in the field, and choose an expiration time 55 | 14. Select "Save" from the top panel - please note the value that appears in the `Value` field 56 | 57 | ### Configure Postman Test information 58 | 59 | > TL;DR: After these steps you should have one value noted - an `access_token`. 60 | 61 | 1. Open Postman 62 | 2. Navigate to the Body panel 63 | 3. Select "x-www-form-urlencoded" 64 | 4. Populate the table that appears with the following values 65 | + `grant_type`: `client_credentials` 66 | + `client_id`: `<yourApplicationId>` where `<yourApplicationId>` is `Application ID` from above 67 | + `client_secret`: `<yourKey>` where `<yourKey>` is the Key `Value` from above 68 | + `resource`: `<yourApplicationIdUrl>` where `<yourApplicationIdUrl>` is `App ID URI` from above 69 | 5. Change the method in the address bar to "POST" 70 | 6. Enter `https://login.microsoftonline.com/microsoft.onmicrosoft.com/oauth2/token` for the url 71 | 7. Select "Send" 72 | 8. The "Body" section of the bottom pane should now be populated 73 | 9. Select `access_token` from the "Body" section - please note its value 74 | 75 | ### Run test 76 | 77 | > TL;DR: After these steps you should know if authentication is working properly! 78 | 79 | 1. Start the `SpringDAL` application with the `production` profile (see the Readme for more information) 80 | 2. Open Postman 81 | 3. Change the method in the address bar to "GET" 82 | 4. Enter `http://localhost:8080/` for the url 83 | 5. Navigate to the Headers panel 84 | 6. Add one header in the table - `Authorization`: `Bearer <yourAccessToken>` where `<yourAccessToken>` is `access_token` from [above](#configure-postman-test-information) 85 | 7. Issue the Postman request by clicking "Send" 86 | 8. Validate that Postman shows a successful response 87 | 9. Navigate to "Headers" in Postman 88 | 10. Toggle off the "Authorization" header by clicking the checkbox next to it 89 | 11. Issue the Postman request by clicking "Send" 90 | 12. Validate that Postman shows a failure response -------------------------------------------------------------------------------- /docs/buildAndReleasePipelines.md: -------------------------------------------------------------------------------- 1 | # Build and Release Pipelines 2 | 3 | For Project Jackson, the team utilized [Azure Dev Ops](https://azure.microsoft.com/en-us/services/devops/) for source control, work tracking, build and release pipelines. The build pipelines were set up for the API and for the small client application. The build artifacts were used as the kickoff point for the release pipelines for each of the deployment pipelines. 4 | 5 | ## API Build 6 | 7 | The API utilizes Spring Boot to create a standalone Java application. The API is described in depth in [the swagger doc](../swagger.yml). The API uses [Maven](https://maven.apache.org/) with [Spring Boot](https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/) to test, compile, run and package the self-contained executable jar that runs in production. To create the executable jar, a dependency in `pom.xml` is added: 8 | 9 | ``` 10 | <build> 11 | <plugins> 12 | <plugin> 13 | <groupId>org.springframework.boot</groupId> 14 | <artifactId>spring-boot-maven-plugin</artifactId> 15 | </plugin> 16 | </plugins> 17 | </build> 18 | ``` 19 | Once this is added, the command `mvn package` can be run, which will produce a `example-name.jar` which can be run with the command `java -jar example-name.jar`. 20 | 21 | To build the Project Jackson API repo in Azure Dev Ops, a build pipeline was created and described with the [`azure-pipelines.yml`](../azure-pipelines.yml) file. It is broken up into sections based on build step. It starts by running `mvn test` which runs all the unit and integration tests and if that step is successful, it runs `mvn package` and creates that executable jar described previously. 22 | 23 | The next steps involve building the docker image defined in the [`Dockerfile`](../Dockerfile), tagging it with the Azure Container Registry (ACR) name and build number, and pushing the tagged Docker image to ACR. The variables for ACR such as `$ACR_SERVER`, `$ACR_CONTAINER_TAG`, `$ACR_PASSWORD`, and `$ACR_USERNAME` are defined in Azure Dev Ops under the build for this repo. The team created a variable group in Azure Dev Ops, under Pipelines, then Library, called `ACR Credentials` that contains the `ACR_SERVER`, `ACR_USERNAME`, and `ACR_PASSWORD` variables. Then in the build pipeline for the API repository, there is a `Variables` tab that has `Variable Groups` as an option and that is where the `ACR Credentials` variable group is linked to the build pipeline for the repository. The `$ACR_CONTAINER_TAG` is set in the `Variables` tab as well, but under `Pipeline Variables` and the team uses a value of `pj-api/pj-api-combined:$(Build.BuildNumber)`. 24 | 25 | ## Client Build 26 | 27 | The UI is a React and TypeScript web app that allows users to easily test the API endpoints and see the data that is returned from the database in a cleaner way. The build pipeline for it was also setup with an `azure-pipelines.yml` file, broken into npm and docker steps. The npm steps include linting, testing and building through the following commands. 28 | ``` 29 | npm run lint 30 | npm run test 31 | npm run build 32 | ``` 33 | 34 | After those steps finish successfully, a docker image is built, tagged, and pushed into ACR in the same way that the API container is dockerized. The difference is that the `$ACR_CONTAINER_TAG` variable in the UI build `Variables` is `pj-ui:$(Build.BuildNumber)` to differentiate the docker container from the API in ACR. 35 | 36 | 37 | ## API Release Pipeline 38 | 39 | The release pipeline for the API was built with Azure Dev Ops, under `Pipelines` and `Releases`. The pipeline was configured so that when there was a new build of the master branch in the API repository, a deployment of the API to Azure App Services would start. The API Deployment pipeline has two tasks in the `Stages` section, which are `Person Endpoint Deploy` and `Title Endpoint Deploy`. Each task deploys an Azure App Service of the type `Linux App` to the `jackson-person` and `jackson-title`, respectively, App Services. The `Image Source` is `Container Registry`, the `Registry or Namespace` is `jacksoncontainer.azurecr.io`, the `Repository` is `pj-api/pj-api-combined`, and the `Tag` is `$(Build.BuildNumber)` which match the values for the docker image pushed into ACR that was set in the build step. 40 | 41 | ## Client Release Pipeline 42 | 43 | Like the build pipeline, the release pipeline for the UI is almost identical to the API release pipeline, with key differences being that only the `pj-client` App Service is deployed to and that the docker image being pulled from ACR is from the `pj-client` repository. Only a single environment variable is defined in `Application settings` and it is named `WEBSITES_PORT` with a value of `8080`. In a similar fashion to the API, when there is a build on the master branch of the UI repository, a new release deployment is kicked off. -------------------------------------------------------------------------------- /docs/images/high_level_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/entref-spring-boot/0d846f641b04e40367f6d3f43051075c1fcc71f3/docs/images/high_level_architecture.png -------------------------------------------------------------------------------- /infrastructure/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Project Jackson Infrastructure 3 | 4 | This repository includes the ARM templates for Project Jackson. 5 | 6 | ## Global ARM Template 7 | 8 | __Regional resources depend on Global ARM resources. Complete this deployment first, so those variables can be used below.__ 9 | 10 | To deploy all the global resources, see the [Global Readme](./global-resources/README.md) which includes details on populating a [CosmosDB](https://azure.microsoft.com/en-us/services/cosmos-db/) instance with test data. 11 | 12 | ## Regional ARM Template 13 | 14 | To deploy all the resources, the script deploy.sh can be used. 15 | The below values are required as inputs to the script: 16 | 17 | 1. Azure Subscription ID 18 | 2. Azure Resource Group (Add existing if one exists; else create a new one) 19 | 3. Azure Deployment Location (i.e., EastUS, WestUS) 20 | 4. App-name: Application Name 21 | 5. Key Vault Resource Group 22 | 6. Key Vault Name 23 | 24 | Another way to deploy is to run one-click deploy for all resources using Deploy to Azure: 25 | 26 | [![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) 27 | 28 | Once the ACR is deployed, follow these manual steps to set up CD pipeline: 29 | 30 | 1. Create variable ACR_SERVER and set value to the server name, which will be the output of your deployment (<application name>container.azurecr.io) 31 | 2. The values of ACR username and password values are referenced and used automatically from Key Vault deployed in `global resources` resource group when you pass the `Key Vault Resource group` and `Key Vault Name` as parameters above. 32 | 3. The value of Cosmos Database connection string is also referenced automatically from Key Vault during the Regional ARM deployment. 33 | 34 | Your deployment resources can now be used as part of your CD pipeline. 35 | 36 | ## Environments 37 | 38 | - Different environments like Dev, QA, Staging and Production environments are created under the resource group for all the resources to be deployed using ARM Template. 39 | - Policies can be created between each of the environments to promote builds from one environment to another based on the requirements of the customer. 40 | - These policies can differ for each customer and product. 41 | - Once the tests under Dev environment passes, they can be approved to run on the QA environment based on policies set for approvals on each. These policies can be set under Azure DevOps Release Pipeline. 42 | 43 | ## Auto Scaling 44 | 45 | - The app service uses auto scaling. 46 | - When the CPU usage for an instance exceeds 70%, the app will automatically be scaled to add another compute instance, up to 5 instances. This limit can be changed based on requirements. 47 | - When memory usage for an instance exceeds 70%, the app will automatically be scaled to add another compute instance, up to 5 instances. This limit can be changed based on requirements. 48 | - The minimum number of instances is set to 1. This means that if the memory used on an instance is less, the instances will be scaled down automatically. 49 | 50 | ## Performance Testing 51 | 52 | To test the performance of an App Service: 53 | 54 | 1. Navigate to the Azure Portal for your App Service 55 | 2. Under `Developer Tools` select `Performance Testing` 56 | !['This image is of the performance testing menu item'](./images/perftest1.png) 57 | 3. Select `New` 58 | !['This image is of new performance testing button'](./images/perftest2.png) 59 | 4. Name the new performance test and configure the settings appropriately 60 | !['This image is of performance testing settings'](./images/perftest3.png) 61 | 5. Submit the test. After resources are automatically allocated, the test will run. 62 | 6. After the test completes, Azure will automatically generate graphs and charts for analysis. 63 | -------------------------------------------------------------------------------- /infrastructure/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | paths: 6 | include: 7 | - infrastructure/* 8 | pool: 9 | vmImage: 'Ubuntu 16.04' 10 | 11 | steps: 12 | # Validate ARM template 13 | - task: AzureResourceGroupDeployment@2 14 | displayName: 'Validate ARM template' 15 | inputs: 16 | azureSubscription: $(serviceConnectionAzureSubscription) 17 | resourceGroupName: $(resourceGroupName) 18 | location: $(location) 19 | csmFile: infrastructure/azuredeploy.json 20 | overrideParameters: '-application_name $(applicationName) -docker_title_url $(acrLoginServer) -docker_person_url $(acrLoginServer) -docker_ui_url $(acrLoginServer) -vaultResourceGroup $(vaultResourceGroup) -vaultName $(vaultName)' 21 | deploymentMode: Validation 22 | 23 | # Copy ARM deployment JSON 24 | - task: CopyFiles@2 25 | displayName: 'Copy ARM deployment JSON' 26 | inputs: 27 | contents: 'infrastructure/azuredeploy.json' 28 | targetFolder: '$(Build.ArtifactStagingDirectory)' 29 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 30 | 31 | # Publish Build Artifacts 32 | - task: PublishBuildArtifacts@1 33 | inputs: 34 | PathToPublish: '$(Build.ArtifactStagingDirectory)' 35 | ArtifactName: package 36 | publishLocation: Container 37 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 38 | -------------------------------------------------------------------------------- /infrastructure/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "application_name": { 6 | "type": "string", 7 | "defaultValue": "[uniqueString(resourceGroup().id)]" 8 | }, 9 | "dev_env": { 10 | "type": "bool", 11 | "defaultValue": true, 12 | "allowedValues": [ 13 | true, 14 | false 15 | ] 16 | }, 17 | "docker_title_url": { 18 | "type": "string" 19 | }, 20 | "docker_person_url": { 21 | "type": "string" 22 | }, 23 | "docker_ui_url": { 24 | "type": "string" 25 | }, 26 | "docker_title_tag": { 27 | "type": "string", 28 | "defaultValue": "v1.0" 29 | }, 30 | "docker_person_tag": { 31 | "type": "string", 32 | "defaultValue": "v1.0" 33 | }, 34 | "docker_ui_tag": { 35 | "type": "string", 36 | "defaultValue": "v1.0" 37 | }, 38 | "oauth_id": { 39 | "type": "securestring", 40 | "defaultValue": "" 41 | }, 42 | "oauth_keyset": { 43 | "type": "securestring", 44 | "defaultValue": "https://login.microsoftonline.com/common/discovery/keys" 45 | }, 46 | "vnet_address_prefix": { 47 | "type": "string", 48 | "defaultValue": "10.0.0.0/16", 49 | "metadata": { 50 | "description": "Address prefix for the Virtual Network" 51 | } 52 | }, 53 | "vnet_subnet_prefix": { 54 | "type": "string", 55 | "defaultValue": "10.0.0.0/28", 56 | "metadata": { 57 | "description": "Subnet prefix" 58 | } 59 | }, 60 | "_artifactsLocation": { 61 | "type": "string", 62 | "metadata": { 63 | "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." 64 | }, 65 | "defaultValue": "https://raw.githubusercontent.com/Microsoft/containers-rest-cosmos-appservice-java/master/infrastructure/" 66 | }, 67 | "_artifactsLocationSasToken": { 68 | "type": "securestring", 69 | "metadata": { 70 | "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." 71 | }, 72 | "defaultValue": "" 73 | }, 74 | "vaultResourceGroup": { 75 | "type": "string" 76 | }, 77 | "vaultName": { 78 | "type": "string" 79 | } 80 | }, 81 | "variables": {}, 82 | "resources": [ 83 | { 84 | "apiVersion": "2015-01-01", 85 | "name": "applicationTemplate", 86 | "type": "Microsoft.Resources/deployments", 87 | "properties": { 88 | "mode": "Incremental", 89 | "templateLink": { 90 | "uri": "[concat(parameters('_artifactsLocation'), 'azuredeploy_application.json', parameters('_artifactsLocationSasToken'))]", 91 | "contentVersion": "1.0.0.0" 92 | }, 93 | "parameters": { 94 | "application_name": { 95 | "value": "[parameters('application_name')]" 96 | }, 97 | "dev_env": { 98 | "value": "[parameters('dev_env')]" 99 | }, 100 | "docker_registry_username": { 101 | "reference": { 102 | "keyVault": { 103 | "id": "[resourceId(subscription().subscriptionId, parameters('vaultResourceGroup'), 'Microsoft.KeyVault/vaults', parameters('vaultName'))]" 104 | }, 105 | "secretName": "acrUsername" 106 | } 107 | }, 108 | "docker_registry_password": { 109 | "reference": { 110 | "keyVault": { 111 | "id": "[resourceId(subscription().subscriptionId, parameters('vaultResourceGroup'), 'Microsoft.KeyVault/vaults', parameters('vaultName'))]" 112 | }, 113 | "secretName": "acrPassword" 114 | } 115 | }, 116 | "docker_title_url": { 117 | "value": "[parameters('docker_title_url')]" 118 | }, 119 | "docker_person_url": { 120 | "value": "[parameters('docker_person_url')]" 121 | }, 122 | "docker_ui_url": { 123 | "value": "[parameters('docker_ui_url')]" 124 | }, 125 | "docker_title_tag": { 126 | "value": "[parameters('docker_title_tag')]" 127 | }, 128 | "docker_person_tag": { 129 | "value": "[parameters('docker_person_tag')]" 130 | }, 131 | "docker_ui_tag": { 132 | "value": "[parameters('docker_ui_tag')]" 133 | }, 134 | "oauth_id": { 135 | "value": "[parameters('oauth_id')]" 136 | }, 137 | "oauth_keyset": { 138 | "value": "[parameters('oauth_keyset')]" 139 | }, 140 | "vnet_address_prefix": { 141 | "value": "[parameters('vnet_address_prefix')]" 142 | }, 143 | "vnet_subnet_prefix": { 144 | "value": "[parameters('vnet_subnet_prefix')]" 145 | }, 146 | "database_connection_string": { 147 | "reference": { 148 | "keyVault": { 149 | "id": "[resourceId(subscription().subscriptionId, parameters('vaultResourceGroup'), 'Microsoft.KeyVault/vaults', parameters('vaultName'))]" 150 | }, 151 | "secretName": "databaseConnectionString" 152 | } 153 | } 154 | } 155 | } 156 | } 157 | ], 158 | "outputs": {} 159 | } 160 | -------------------------------------------------------------------------------- /infrastructure/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # -e: immediately exit if any command has a non-zero exit status 6 | # -o: prevents errors in a pipeline from being masked 7 | # IFS new value is less likely to cause confusing bugs when looping arrays or arguments (e.g. $@) 8 | 9 | usage() { echo "Usage: $0 -i <subscriptionId> -g <resourceGroupName> -n <deploymentName> -l <resourceGroupLocation>" 1>&2; exit 1; } 10 | 11 | declare subscriptionId="" 12 | declare resourceGroupName="" 13 | declare deploymentName="" 14 | declare resourceGroupLocation="" 15 | 16 | # Initialize parameters specified from command line 17 | while getopts ":i:g:n:l:" arg; do 18 | case "${arg}" in 19 | i) 20 | subscriptionId=${OPTARG} 21 | ;; 22 | g) 23 | resourceGroupName=${OPTARG} 24 | ;; 25 | n) 26 | deploymentName=${OPTARG} 27 | ;; 28 | l) 29 | resourceGroupLocation=${OPTARG} 30 | ;; 31 | esac 32 | done 33 | shift $((OPTIND-1)) 34 | 35 | #Prompt for parameters is some required parameters are missing 36 | if [[ -z "$subscriptionId" ]]; then 37 | echo "Your subscription ID can be looked up with the CLI using: az account show --out json " 38 | echo "Enter your subscription ID:" 39 | read subscriptionId 40 | [[ "${subscriptionId:?}" ]] 41 | fi 42 | 43 | if [[ -z "$resourceGroupName" ]]; then 44 | echo "This script will look for an existing resource group, otherwise a new one will be created " 45 | echo "You can create new resource groups with the CLI using: az group create " 46 | echo "Enter a resource group name" 47 | read resourceGroupName 48 | [[ "${resourceGroupName:?}" ]] 49 | fi 50 | 51 | if [[ -z "$resourceGroupLocation" ]]; then 52 | echo "If creating a *new* resource group, you need to set a location " 53 | echo "You can lookup locations with the CLI using: az account list-locations " 54 | 55 | echo "Enter resource group location:" 56 | read resourceGroupLocation 57 | fi 58 | 59 | if [[ -z "$deploymentName" ]]; then 60 | echo "Please enter a Deployment Name for your Application" 61 | read deploymentName 62 | [[ "${deploymentName:?}" ]] 63 | fi 64 | 65 | #templateFile Path - template file to be used 66 | templateFilePath="azuredeploy.json" 67 | 68 | if [ ! -f "$templateFilePath" ]; then 69 | echo "$templateFilePath not found" 70 | exit 1 71 | fi 72 | 73 | #parameter file path 74 | #parametersFilePath="parameters.json" 75 | 76 | #if [ ! -f "$parametersFilePath" ]; then 77 | # echo "$parametersFilePath not found" 78 | # exit 1 79 | #fi 80 | 81 | if [ -z "$subscriptionId" ] || [ -z "$resourceGroupName" ]; then 82 | echo "Either one of subscriptionId, resourceGroupName, deploymentName is empty" 83 | usage 84 | fi 85 | 86 | #login to azure using your credentials 87 | az account show 1> /dev/null 88 | 89 | if [ $? != 0 ]; 90 | then 91 | az login 92 | fi 93 | 94 | #set the default subscription id 95 | az account set --subscription $subscriptionId 96 | 97 | set +e 98 | 99 | #Check for existing RG 100 | az group show --name $resourceGroupName 1> /dev/null 101 | 102 | if [ $? != 0 ]; then 103 | echo "Resource group with name" $resourceGroupName "could not be found. Creating new resource group.." 104 | set -e 105 | ( 106 | set -x 107 | az group create --name $resourceGroupName --location $resourceGroupLocation 1> /dev/null 108 | ) 109 | else 110 | echo "Using existing resource group..." 111 | fi 112 | 113 | #Start deployment 114 | echo "Starting deployment..." 115 | ( 116 | set -x 117 | az group deployment create --name "$deploymentName" --resource-group "$resourceGroupName" --template-file "$templateFilePath" #--parameters "@${parametersFilePath}" 118 | ) 119 | 120 | if [ $? == 0 ]; 121 | then 122 | echo "Template has been successfully deployed" 123 | fi 124 | -------------------------------------------------------------------------------- /infrastructure/global-resources/README.md: -------------------------------------------------------------------------------- 1 | # Global Resource Deployment 2 | 3 | ## Deploy Global Resources 4 | 5 | Global resources deployment is going to create: 6 | - Cosmos Database - To support NoSQL API's. 7 | - Azure Container Registry - To push and pull images. 8 | - Traffic Manager - To increase app responsiveness by leveraging performance routing. 9 | - Key Vault - To encrypt keys and small secrets like passwords that use keys stored in HSM's 10 | 11 | To do this, run: 12 | ``` 13 | az group deployment create --resource-group <your-resource-group> --template-file infrastructure/global-resources/azuredeploy.json --parameters objectId=<object Id> 14 | ``` 15 | 16 | Another way is to run one-click deploy for all resources using Deploy to Azure: 17 | 18 | [![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) 19 | 20 | These are global resources and should be deployed independently of application infrastructure. 21 | 22 | ## Traffic Manger Endpoints 23 | 24 | Project Jackson uses Traffic Manager to route a specific URL to the correct region. In order to create these endpoints, the [`endpoint_deploy.json`](./endpoint_deploy.json) needs to be run. To do this, run: 25 | 26 | ``` 27 | az group deployment create --template-file infrastructure/global-resoruces/endpoint_deploy.json --resource-group your-resource-group --parameters traffic_manager_endpoints=app1,app2 traffic_manager_endpoint_locations=eastus,westus traffic_manager_profiles_name=<traffic_manager_profile_name> 28 | ``` 29 | 30 | with `your-resource-group` as the name of the resource group you are creating the global resources in, `app1` being the target for an endpoint that correlates to the azure region specified by the first parameter in the `traffic_manager_endpoint_locations` parameter. For example, `app1` is created in `eastus` and `app2` is created in `westus`. 31 | 32 | ## Key Vault 33 | 34 | Project Jackson uses Key Vault to encrypt and store keys and secrets like username and password for the Azure Container Registry. 35 | 36 | It also stores the connection string for the Cosmos database created above in the Key Vault. 37 | 38 | ## Deploying Data 39 | 40 | > Note: This is optional, but is a good way to injest some sample data without needing to manually create it. 41 | 42 | In this section we'll explain how to populate the [CosmosDB](https://azure.microsoft.com/en-us/services/cosmos-db/) instance that was created above with sample data. 43 | 44 | ### Install Dependencies 45 | 46 | * Install [the latest Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) 47 | * Install MongoDB: 48 | * Windows: [Install MongoDB Community Edition on Windows](https://docs.mongodb.com/v3.2/tutorial/install-mongodb-on-windows/) 49 | * MacOS: From a command line, run `brew install mongodb` 50 | * Linux: From command line, run `apt-get install mongodb` 51 | * Open a [shell](https://en.wikipedia.org/wiki/Shell_%28computing%29) in the project root directory (the next steps depend on this) 52 | 53 | ### Set up your environment variables 54 | 55 | - Locate the provisioned CosmosDB instance in the [Azure Portal](https://portal.azure.com) 56 | - Open the Cosmos Connection String blade 57 | - Make sure the Cosmos DB resource is already created as described above 58 | - From Bash command line, run `load_env.sh`. This will write/load any needed variables to the `vars.env` file 59 | - `RESOURCE_GROUP` - the Azure resource group name 60 | - `COSMOSDB_NAME` - the CosmosDB collection name (which is case sensitive) 61 | - `COSMOSDB_PASSWORD` - the CosmosDB's password (needed for when you load the data into Cosmos) 62 | - Load `vars.env` into your environment or the VM where the app is being built locally 63 | ``` bash 64 | source ./vars.env 65 | ``` 66 | - Or in your chosen IDE, set your environment variables within your project 67 | - NB: there will also be a DB_NAME and DB_CONNSTR for the Spring application (see the database section below in Application Configuration) 68 | 69 | ### Prepare the command line 70 | 71 | - Switch into the project `data` directory: `cd data` 72 | - Log into Azure: `az login` 73 | - If you have multiple subscriptions, confirm that the project subscription is active: 74 | 75 | ``` Bash 76 | az account show 77 | az account set --subscription <subscription name/ID> 78 | ``` 79 | 80 | ### Import the sample IMDb data to Cosmos DB 81 | 82 | - Open a Bash command line 83 | - Download and prepare the required IMDb data files: 84 | 85 | ``` Bash 86 | data/getdata.sh 87 | ``` 88 | 89 | - Before starting to import data make sure the step `Set up your environment variables` is completed. 90 | - Import the data into Cosmos collections 91 | 92 | ``` Bash 93 | data/importdata.sh 94 | ``` 95 | 96 | ### TIP: Explore the data from the MongoDB command-line 97 | 98 | - Copy the Cosmos DB connection string from the "Connection String" blade 99 | - Start the MongoDB CLI with this command: `mongo <connection string>` 100 | - Begin executing MongoDB commands, such as: 101 | 102 | ``` Mongo 103 | use moviesdb 104 | show collections 105 | db.titles.count() 106 | db.titles.find ({primaryTitle: "Casablanca"}) 107 | ``` 108 | -------------------------------------------------------------------------------- /infrastructure/global-resources/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | paths: 6 | include: 7 | - infrastructure/global-resources/* 8 | pool: 9 | vmImage: 'Ubuntu 16.04' 10 | 11 | steps: 12 | # Validate ARM template 13 | - task: AzureResourceGroupDeployment@2 14 | displayName: 'Validate ARM template' 15 | inputs: 16 | azureSubscription: $(serviceConnectionAzureSubscription) 17 | resourceGroupName: $(resourceGroupName) 18 | location: $(location) 19 | csmFile: infrastructure/global-resources/azuredeploy.json 20 | overrideParameters: '-database_account_name $(database_account_name) -database-name $(database-name) -container_registry_name $(container_registry_name) -key_vault_name $(key_vault_name) -traffic_manager_profiles_name $(traffic_manager_profiles_name) -objectId $(objectId)' 21 | deploymentMode: Validation 22 | 23 | # Copy ARM deployment JSON 24 | - task: CopyFiles@2 25 | displayName: 'Copy ARM deployment JSON' 26 | inputs: 27 | contents: 'infrastructure/global-resources/azuredeploy.json' 28 | targetFolder: '$(Build.ArtifactStagingDirectory)' 29 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 30 | 31 | # Publish Build Artifacts 32 | - task: PublishBuildArtifacts@1 33 | inputs: 34 | PathToPublish: '$(Build.ArtifactStagingDirectory)' 35 | ArtifactName: package 36 | publishLocation: Container 37 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 38 | -------------------------------------------------------------------------------- /infrastructure/global-resources/endpoint_deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "traffic_manager_profiles_name": { 6 | "type": "string", 7 | "defaultValue": "tm-profiles" 8 | }, 9 | "traffic_manager_endpoints": { 10 | "type": "string", 11 | "defaultValue": "test" 12 | }, 13 | "traffic_manager_endpoint_locations": { 14 | "type": "string", 15 | "defaultValue": "East US" 16 | } 17 | }, 18 | "variables": { 19 | "separated_endpoints": "[split(parameters('traffic_manager_endpoints'),',')]", 20 | "separated_endpoint_locations": "[split(parameters('traffic_manager_endpoint_locations'),',')]" 21 | }, 22 | "resources": [ 23 | { 24 | "apiVersion": "2015-11-01", 25 | "type": "Microsoft.Network/trafficManagerProfiles/externalEndpoints", 26 | "location": "global", 27 | "name": "[concat(parameters('traffic_manager_profiles_name'), '/Endpoint', copyIndex())]", 28 | "copy": { 29 | "name": "endpointloop", 30 | "count": "[length(variables('separated_endpoints'))]" 31 | }, 32 | "properties": { 33 | "endpointStatus": "Enabled", 34 | "target": "[variables('separated_endpoints')[copyIndex('endpointloop')]]", 35 | "endpointLocation": "[variables('separated_endpoint_locations')[copyIndex('endpointloop')]]" 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /infrastructure/images/perftest1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/entref-spring-boot/0d846f641b04e40367f6d3f43051075c1fcc71f3/infrastructure/images/perftest1.png -------------------------------------------------------------------------------- /infrastructure/images/perftest2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/entref-spring-boot/0d846f641b04e40367f6d3f43051075c1fcc71f3/infrastructure/images/perftest2.png -------------------------------------------------------------------------------- /infrastructure/images/perftest3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/entref-spring-boot/0d846f641b04e40367f6d3f43051075c1fcc71f3/infrastructure/images/perftest3.png -------------------------------------------------------------------------------- /integration-test-tool/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /integration-test-tool/README.md: -------------------------------------------------------------------------------- 1 | # Integration test tool 2 | 3 | [![Build Status](https://dev.azure.com/csebostoncrew/ProjectJackson/_apis/build/status/ProjectJackson-IntegrationTestTool-Github?branchName=master)](https://dev.azure.com/csebostoncrew/ProjectJackson/_build/latest?definitionId=29&branchName=master) 4 | 5 | This tool runs integration tests against production instances. 6 | 7 | ## How To Use 8 | 9 | Follow the steps in the sections below to get up and running! 10 | 11 | ### Install dependencies 12 | 13 | Run `npm i` from the project directory. 14 | 15 | ### Configure Environment Variables 16 | 17 | On Windows: 18 | 19 | ``` 20 | set TARGET_BACKEND_SITE=<backendBaseUrl> 21 | set TARGET_FRONTEND_SITE=<frontendBaseUrl> 22 | set TARGET_USERNAME=<aadUsername> 23 | set TARGET_PASSWORD=<aadPassword> 24 | ``` 25 | 26 | On Linux: 27 | 28 | ``` 29 | export TARGET_BACKEND_SITE=<backendBaseUrl> 30 | export TARGET_FRONTEND_SITE=<frontendBaseUrl> 31 | export TARGET_USERNAME=<aadUsername> 32 | export TARGET_PASSWORD=<aadPassword> 33 | ``` 34 | 35 | Where... 36 | 37 | + `backendBaseUrl` - The URL of the backend service that hosts `Title` and `Person` endpoints 38 | + `frontendBaseUrl` - The URL of the frontend service that hosts the UI 39 | + `aadUsername` - The Username of some Azure Active Directory Account 40 | + `aadPassword` - The password of some Azure Active Directory Account 41 | 42 | ### Run 43 | 44 | Run `npm test` from the project directory. 45 | 46 | ## FAQ 47 | 48 | ### What browser does this use to simulate user interaction? 49 | 50 | We use [puppeteer](https://github.com/GoogleChrome/puppeteer), which in turn uses Chrome. 51 | 52 | ### What values do I use for the base URL properties? 53 | 54 | If you're running our service on Azure and are using Application Gateway to front the traffic (as is default) you can point `backendBaseUrl` at `http://<yourAppGatewayIp>` or `http://<yourConfiguredDnsName>`, and `frontendBaseUrl` at `http://<yourAppGatewayIp>/ui` or `http://yourConfiguredDnsName>/ui`. 55 | 56 | ### How can I generate an AAD account? 57 | 58 | If you're using AAD internal user accounts, you can create a new one following along with [this article](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/add-users-azure-active-directory). If you're using AAD public Microsoft accounts, you can create a new a new outlook account [here](https://outlook.com). 59 | 60 | ### How can I determine which type of AAD account I need to use? 61 | 62 | This is in reference to [the above question](#how-can-i-generate-an-aad-account) - To determine which account type you are using you can inspect your AAD application's manifest. If your manifest contains `signInAudience: AzureADAndPersonalMicrosoftAccount` then it supports public Microsoft accounts. If it does not, then you support only AAD internal user accounts. For reference, see [this document](https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest). 63 | -------------------------------------------------------------------------------- /integration-test-tool/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | paths: 6 | include: 7 | - integration-test-tool/* 8 | 9 | pool: 10 | vmImage: 'Ubuntu 16.04' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.13.0' 16 | failOnStandardError: 'true' 17 | displayName: 'Install Node.js' 18 | - script: | 19 | npm install 20 | workingDirectory: integration-test-tool/ 21 | displayName: 'NPM Install Step' 22 | - script: | 23 | npm run lint 24 | workingDirectory: integration-test-tool/ 25 | displayName: 'NPM Lint Step' 26 | -------------------------------------------------------------------------------- /integration-test-tool/config.ts: -------------------------------------------------------------------------------- 1 | export const config = Object.freeze({ 2 | backend_site: process.env.TARGET_BACKEND_SITE || 'https://www.<your_domain>.io', 3 | password: process.env.TARGET_PASSWORD || '1234567', 4 | ui_site: process.env.TARGET_FRONTEND_SITE || 'https://www.<your_domain>.io/ui', 5 | username: process.env.TARGET_USERNAME || 'testuser@<your_domain>.onmicrosoft.com', 6 | environment: process.env.NODE_ENV || 'development' 7 | }) 8 | -------------------------------------------------------------------------------- /integration-test-tool/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-puppeteer", 3 | moduleFileExtensions: [ "ts", "tsx", "js" ], 4 | transform: { 5 | "^.+\\.(ts|tsx)$": "ts-jest" 6 | }, 7 | globals: { 8 | "ts-jest": { 9 | "tsConfig": "tsconfig.json" 10 | } 11 | }, 12 | testMatch: [ 13 | "**/src/**/*.test.(ts|tsx|js)" 14 | ], 15 | collectCoverageFrom: [ 16 | "**/*.{js,jsx}", 17 | "!**/node_modules/**", 18 | "!**/vendor/**" 19 | ] 20 | } -------------------------------------------------------------------------------- /integration-test-tool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-test-tool", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run test", 8 | "test": "jest --config jest.config.js", 9 | "lint": "tslint -c tslint.json 'src/**/*.tsx'" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "jest": "^23.6.0", 15 | "jest-puppeteer": "^3.8.0", 16 | "node-fetch": "^2.3.0", 17 | "puppeteer": "^1.11.0", 18 | "ts-jest": "^23.10.5" 19 | }, 20 | "devDependencies": { 21 | "@types/expect-puppeteer": "^3.3.0", 22 | "@types/jest": "^23.3.12", 23 | "@types/jest-environment-puppeteer": "^2.2.1", 24 | "@types/node-fetch": "^2.1.4", 25 | "@types/puppeteer": "^1.11.1", 26 | "tslint": "^5.12.1", 27 | "typescript": "^3.3.3333" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /integration-test-tool/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": [ "es2015", "dom" ], 7 | "allowSyntheticDefaultImports": true, 8 | "typeRoots": [ 9 | "./node_modules/@types", 10 | "./types" 11 | ] 12 | }, 13 | "exclude": [ 14 | "./node_modules/**/*" 15 | ], 16 | "include": [ 17 | "./src/**/*" 18 | ] 19 | } -------------------------------------------------------------------------------- /integration-test-tool/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "semicolon": [true, "never"], 10 | "indent": [true, "spaces", 4], 11 | "import-spacing": true, 12 | "ordered-imports": [ 13 | true, 14 | { 15 | "grouped-imports": true 16 | } 17 | ] 18 | }, 19 | "rulesDirectory": [] 20 | } -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode/ -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Nginx web server from the official docker registry 2 | FROM nginx:1.14.0-alpine 3 | 4 | EXPOSE 8080 5 | 6 | RUN rm -rv /etc/nginx/conf.d 7 | COPY conf /etc/nginx 8 | 9 | # The static site is built using npm run build 10 | # the output of build is stored in the dist dir 11 | WORKDIR /usr/share/nginx/html 12 | COPY ./dist/ /usr/share/nginx/html -------------------------------------------------------------------------------- /ui/Dockerfile-developer: -------------------------------------------------------------------------------- 1 | FROM node:10 as build 2 | 3 | RUN mkdir app 4 | COPY . /app 5 | WORKDIR /app 6 | 7 | # Install, lint, test and build 8 | RUN npm i 9 | RUN npm run lint 10 | RUN npm run test 11 | RUN npm run build 12 | 13 | # Nginx web server from the official docker registry 14 | FROM nginx:1.14.0-alpine 15 | 16 | EXPOSE 8080 17 | 18 | RUN rm -rv /etc/nginx/conf.d 19 | COPY conf /etc/nginx 20 | 21 | # The static site is built using npm run build 22 | # the output of build is stored in the dist dir 23 | WORKDIR /usr/share/nginx/html 24 | COPY --from=build /app/dist/ /usr/share/nginx/html -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Project Jackson Client 2 | 3 | ## Quickstart 4 | 5 | This UI is written entirely in [TypeScript](https://www.typescriptlang.org/) and [JavaScript](https://en.wikipedia.org/wiki/JavaScript). It uses [Node.js](https://nodejs.org/en/) , [React.js](https://reactjs.org), and [webpack](https://webpack.js.org/) as well as various dependencies from [NPM](https://www.npmjs.com/). To get started: 6 | - Install (or update) latest version of `Node.js` 7 | This includes the latest version of `NPM` as well. 8 | - Run `cd ui` from the root of this project to navigate into the client directory 9 | - Run `npm install` to install necessary dependencies 10 | - Start the dev server using `npm run dev`. 11 | To learn about configuring Authentication with AAD and an API URL, read the [Environment Variables](#environment-variables) section. 12 | - Access your local dev instance at http://localhost:3000. `webpack-dev-server` should open it automatically in your default browser. If everything was setup correctly you should see a landing page like this: 13 | !['This image is of the UI landing page'](./images/uiScreenshot.png) 14 | - When you are ready to ship to production, you can run `npm run build` for an optimized production build. It will be output to the `dist/` directory. Refer to our [infrastructure](../infrastructure/readme.md) documentation for more information on deploying this application to a production environment. 15 | 16 | ## Environment Variables 17 | 18 | The application is built to support authentication with Azure Active Directory and interact with the API defined in [`containers-rest-cosmos-appservice-java/api`](../api). To obtain your AAD keys follow [this](docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) tutorial. For more information on setting up AAD read our [AAD setup guide](../docs/azureActiveDirectory.md). You must specify the following environment variables to connect the UI to rest of your project: 19 | - `WEBPACK_PROP_AAD_CLIENT_ID` - OAuth provider application ID from Azure Active Directory portal 20 | - `WEBPACK_PROP_API_BASE_URL` - API Base url such as `http://localhost:8080` or `https://example.com/api` 21 | - `WEBPACK_PROP_UI_BASEPATH` - basepath string for which the UI will be served from. This is used for client side routing. **The basepath is defaulted to `ui`**. 22 | - Valid entries for this variable include the following literals: `\abc`, `abc`. 23 | - Invalid entries include: `/abc` or `\/abc`. The system file path will be prepended to your route and break the client side router. 24 | - `webpack-dev-server` is configured to automatically start the UI app from the basepath environment variable 25 | - *Note* both environment variables are optional and the app will build without them. 26 | ```bash 27 | # On Windows we can use: 28 | set WEBPACK_PROP_AAD_CLIENT_ID=abc123 29 | # On Linux we can use: 30 | export WEBPACK_PROP_AAD_CLIENT_ID=abc123 31 | ``` 32 | - Alternatively, you can prepend these environment variables to the `npm run dev` command such as: 33 | ```bash 34 | WEBPACK_PROP_AAD_CLIENT_ID=abc123 WEBPACK_PROP_API_BASE_URL=http://localhost:8080 WEBPACK_PROP_UI_BASEPATH=ui npm run dev 35 | ``` 36 | - Remember to set up these environment variables with your deployment pipeline, since shipping a production build without these values will cause the app to fail. 37 | 38 | ## Dependency Walkthrough 39 | 40 | This project is written in a typed version of `JavaScript` called [`TypeScript`](https://www.typescriptlang.org/). It requires a `build` step that is handled by [`webpack`](https://webpack.js.org/). There is one main `webpack` config filed called [`webpack.common.js`](./webpack.common.js) and two aditional configs for [`development`](./webpack.dev.js) and [`production`](./webpack.prod.js). Using the `npm run dev` command will merge and use the `common` and `dev` configs, and `npm run build` will merge and use the `common` and `prod` configs. 41 | 42 | `webpack` compiles the `TypeScript` source code into `JavaScript`, then it bundles all of that code into a single `bundle.js` file that is loaded into a basic `index.html` file. Running `npm run build` will generate these files in a `dist/` directory. `webpack` is also responsible for loading in the Azure Active Directory Client ID and API Base URL through process environment variables. We have included `webpack-dev-server` in the `development` config to simpify the developer experience. It hot-reloads UI changes in the browser as you save changes in your editor. Be sure to use the output from `npm run build` when shipping to production, since the build output from this command is compressed and optimized for production. 43 | 44 | The UI is built using [`React.js`](https://reactjs.org/). The `package.json` should enforce the correct `React.js` version; however, if you need to make changes, you must use `React.js` version `16.6.x` or greater. The application makes use of the new `Context` api for managing state across DOM Nodes. It uses a module called [`Reach Router`](https://reach.tech/router) which is an accessibility-first client-side routing library. Microsoft's own authentication library [`MSAL`](https://github.com/AzureAD/microsoft-authentication-library-for-js) is used for the client side authentication. It is essential if your API is configured with OAuth tokens (as such in the case of the code in `./../api`). 45 | 46 | ## Testing & Linting 47 | 48 | > *tldr:* Run test suite using: `npm run test`. Run linter using: `npm run lint` 49 | 50 | The UI is tested using `Jest` and `Enzyme`. All tests are written in `TypeScript` and are compiled/run similar to how the `webpack` configs are set up. You can run the test suite using `npm run test`. Most tests utilize snapshots; if you make a user interface change, be sure to update the snapshots using: `npm run test -- --updateSnapshot`. 51 | 52 | In conjunction with `TypeScript` typings, developers can use our `tslint` configuration to lint their projects. If you are using VS Code, the editor will lint for you as you develop; otherwise, run `npm run lint` to verify the code fits the project's formatting standards. 53 | 54 | ## Azure Pipeline Environmental Variables 55 | 56 | Create the require pipeline variables for the Azure Pipelines. These are: 57 | 58 | - ACR_CONTAINER_TAG = <image_name>-client:$(Build.BuildNumber) 59 | 60 | Create an Azure Pipeline Variable Group to store the environment variables needed for the pipeline API yaml file. Creating a Variable Group will allow you to use these variables across multiple pipelines. 61 | 62 | - ACR_USERNAME = <your_registry_username> 63 | - ACR_PASSWORD = <your_registry_password> 64 | - ACR_SERVER = <yourregistry.azurecr.io> -------------------------------------------------------------------------------- /ui/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | paths: 6 | include: 7 | - ui/* 8 | 9 | pool: 10 | vmImage: 'Ubuntu 16.04' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.13.0' 16 | failOnStandardError: 'true' 17 | displayName: 'Install Node.js' 18 | - script: | 19 | npm install 20 | workingDirectory: ui/ 21 | displayName: 'NPM Install Step' 22 | - script: | 23 | npm run lint 24 | workingDirectory: ui/ 25 | displayName: 'NPM Lint Step' 26 | - script: | 27 | npm run test 28 | workingDirectory: ui/ 29 | displayName: 'NPM Test Step' 30 | - script: | 31 | npm run build 32 | workingDirectory: ui/ 33 | displayName: 'NPM Build Step' 34 | - script: | 35 | docker build -t $(acrLoginServer)/$ACR_CONTAINER_TAG . 36 | workingDirectory: ui/ 37 | displayName: 'Docker Build' 38 | - script: | 39 | docker login $(acrLoginServer) -u $(acrUsername) -p $(acrPassword) 40 | displayName: 'Docker Login' 41 | - script: | 42 | docker push $(acrLoginServer)/$ACR_CONTAINER_TAG 43 | displayName: 'Docker Push' 44 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 45 | -------------------------------------------------------------------------------- /ui/conf/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri $uri/ /index.html; 9 | } 10 | 11 | # redirect server error pages to the static page /50x.html 12 | # 13 | error_page 500 502 503 504 /50x.html; 14 | location = /50x.html { 15 | root /usr/share/nginx/html; 16 | } 17 | } -------------------------------------------------------------------------------- /ui/images/uiScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/entref-spring-boot/0d846f641b04e40367f6d3f43051075c1fcc71f3/ui/images/uiScreenshot.png -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ "ts", "tsx", "js" ], 3 | transform: { 4 | "^.+\\.(ts|tsx)$": "ts-jest" 5 | }, 6 | globals: { 7 | "ts-jest": { 8 | "tsConfig": "tsconfig.json" 9 | } 10 | }, 11 | testMatch: [ 12 | "**/__tests__/**/*.(ts|tsx|js)" 13 | ], 14 | testPathIgnorePatterns: [ 15 | "setup.ts" 16 | ], 17 | moduleNameMapper: { 18 | "\\.(css)$": "identity-obj-proxy" 19 | }, 20 | setupTestFrameworkScriptFile: "<rootDir>/src/__tests__/setup.ts" 21 | } -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "projectjacksonclient", 3 | "version": "1.0.0", 4 | "description": "Web App demo for Project Jackson ", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --hot --open --config webpack.dev.js", 8 | "test": "jest --config jest.config.js", 9 | "build": "webpack --config webpack.prod.js", 10 | "lint": "tslint -c tslint.json 'src/**/*.tsx'" 11 | }, 12 | "author": "Ethan Arrowood", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@reach/router": "^1.2.1", 16 | "msal": "^0.2.3", 17 | "react": "^16.6.0", 18 | "react-dom": "^16.6.0", 19 | "whatwg-fetch": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/enzyme": "^3.1.15", 23 | "@types/jest": "^23.3.8", 24 | "@types/reach__router": "^1.2.1", 25 | "@types/react": "^16.4.18", 26 | "@types/react-dom": "^16.0.9", 27 | "awesome-typescript-loader": "^5.2.1", 28 | "css-loader": "^1.0.1", 29 | "enzyme": "^3.7.0", 30 | "enzyme-adapter-react-16": "^1.6.0", 31 | "html-webpack-plugin": "^3.2.0", 32 | "identity-obj-proxy": "^3.0.0", 33 | "jest": "^23.6.0", 34 | "jest-dom": "^2.1.0", 35 | "react-test-renderer": "^16.6.0", 36 | "source-map-loader": "^0.2.4", 37 | "style-loader": "^0.23.1", 38 | "ts-jest": "^23.10.4", 39 | "ts-loader": "^5.2.2", 40 | "tslint": "^5.11.0", 41 | "typescript": "^3.1.3", 42 | "webpack": "^4.23.1", 43 | "webpack-cli": "^3.1.2", 44 | "webpack-dev-server": "^3.1.14", 45 | "webpack-merge": "^4.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/__tests__/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import { App } from '../components/App' 5 | 6 | describe('<App />', () => { 7 | it('renders correctly', () => { 8 | const tree = renderer 9 | .create(<App />) 10 | .toJSON() 11 | expect(tree).toMatchSnapshot() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /ui/src/__tests__/DefaultComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createHistory, 3 | createMemorySource, 4 | LocationProvider, 5 | } from '@reach/router' 6 | import { render } from 'enzyme' 7 | import * as React from 'react' 8 | import * as renderer from 'react-test-renderer' 9 | 10 | import { App } from '../components/App' 11 | import { DefaultComponent } from '../components/DefaultComponent' 12 | 13 | describe('<DefaultComponent />', () => { 14 | beforeAll(() => { 15 | process.env = Object.assign(process.env, { WEBPACK_PROP_UI_BASEPATH: 'ui' }) 16 | }) 17 | 18 | it('renders correctly', () => { 19 | const tree = renderer 20 | .create(<DefaultComponent default />) 21 | .toJSON() 22 | expect(tree).toMatchSnapshot() 23 | }) 24 | 25 | describe('It is the default 404 page', () => { 26 | test('It finds the page if user tries a nonsense path', () => { 27 | const badPath = '/awefwaef' 28 | const source = createMemorySource(badPath) 29 | const hist = createHistory(source) 30 | const r = render( 31 | <LocationProvider history={hist}> 32 | <App /> 33 | </LocationProvider>, 34 | ) 35 | 36 | expect(r.find('.default-component')).toHaveLength(1) 37 | expect(r.find('.default-component1')).toHaveLength(0) 38 | }) 39 | 40 | test('It doesn\'t find the page if user tries a valid path', () => { 41 | const goodPath = '/titles' 42 | const source = createMemorySource(goodPath) 43 | const hist = createHistory(source) 44 | const r = render( 45 | <LocationProvider history={hist}> 46 | <App /> 47 | </LocationProvider>, 48 | ) 49 | 50 | expect(r.find('.default-component')).toHaveLength(0) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /ui/src/__tests__/Home.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import * as React from 'react' 3 | import * as renderer from 'react-test-renderer' 4 | 5 | import { Home } from '../components/Home' 6 | 7 | describe('<Home />', () => { 8 | it('renders correctly', () => { 9 | const tree = renderer 10 | .create(<Home path='/'/>) 11 | .toJSON() 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /ui/src/__tests__/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import {mount } from 'enzyme' 2 | import * as React from 'react' 3 | import * as renderer from 'react-test-renderer' 4 | 5 | import { Navbar } from '../components/Navbar' 6 | 7 | describe('<Navbar />', () => { 8 | it('renders correctly', () => { 9 | const tree = renderer 10 | .create(<Navbar />) 11 | .toJSON() 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | it('should activate Home link by default', () => { 15 | const navbarWrapper = mount(<Navbar />) 16 | // @reach/router sets the 'aria-current' property to 'page' when the Link element is active 17 | // The Navbar component adds the 'active' class to the className list too 18 | const linkElementWrapper = navbarWrapper.find({ 19 | 'aria-current': 'page', 20 | 'className': 'nav-link active', 21 | 'href': '/', 22 | }) 23 | expect(linkElementWrapper.text()).toBe('Home') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /ui/src/__tests__/PageForm.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import * as React from 'react' 3 | import * as renderer from 'react-test-renderer' 4 | 5 | import { PageForm } from '../components/PageForm' 6 | 7 | describe('<PageForm />', () => { 8 | it('renders correctly', () => { 9 | const tree = renderer 10 | .create( 11 | <PageForm 12 | inputTitle='test' 13 | inputPlaceholder='test' 14 | onInputChange={() => null} 15 | onSubmitClick={() => null} 16 | />, 17 | ) 18 | .toJSON() 19 | expect(tree).toMatchSnapshot() 20 | }) 21 | it('should fire input change handler', () => { 22 | const mockFn = jest.fn() 23 | const pageFormWrapper = mount( 24 | <PageForm 25 | inputTitle='test' 26 | inputPlaceholder='test' 27 | onInputChange={mockFn} 28 | onSubmitClick={() => null} 29 | />, 30 | ) 31 | const textInput = pageFormWrapper.find('.form-field-input') 32 | textInput.simulate('change', {}) 33 | expect(mockFn.mock.calls.length).toBe(1) 34 | }) 35 | it('should fire submit button handler', () => { 36 | const mockFn = jest.fn() 37 | const pageFormWrapper = mount( 38 | <PageForm 39 | inputTitle='test' 40 | inputPlaceholder='test' 41 | onInputChange={() => null} 42 | onSubmitClick={mockFn} 43 | />, 44 | ) 45 | const submitButton = pageFormWrapper.find('.form-field-submit') 46 | submitButton.simulate('click', {}) 47 | expect(mockFn.mock.calls.length).toBe(1) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /ui/src/__tests__/Person.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import { Person } from '../components/Person' 5 | 6 | describe('<Person />', () => { 7 | it('renders correctly', () => { 8 | const tree = renderer 9 | .create(<Person path='/people'/>) 10 | .toJSON() 11 | expect(tree).toMatchSnapshot() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /ui/src/__tests__/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import { Title } from '../components/Title' 5 | 6 | describe('<Title />', () => { 7 | it('renders correctly', () => { 8 | const tree = renderer 9 | .create(<Title path='/titles'/>) 10 | .toJSON() 11 | expect(tree).toMatchSnapshot() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/App.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<App /> renders correctly 1`] = ` 4 | <div 5 | className="app-container" 6 | > 7 | <nav 8 | className="nav-container" 9 | > 10 | <span 11 | className="nav-title" 12 | > 13 | Project Jackson 14 | </span> 15 | <div 16 | className="nav-link-container" 17 | > 18 | <a 19 | aria-current="page" 20 | className="nav-link active" 21 | href="/" 22 | onClick={[Function]} 23 | > 24 | Home 25 | </a> 26 | <a 27 | className="nav-link" 28 | href="/people" 29 | onClick={[Function]} 30 | > 31 | People 32 | </a> 33 | <a 34 | className="nav-link" 35 | href="/titles" 36 | onClick={[Function]} 37 | > 38 | Titles 39 | </a> 40 | <button 41 | className="login-button" 42 | onClick={[Function]} 43 | > 44 | Log In 45 | </button> 46 | </div> 47 | </nav> 48 | <div 49 | role="group" 50 | style={ 51 | Object { 52 | "outline": "none", 53 | } 54 | } 55 | tabIndex="-1" 56 | > 57 | <div 58 | className="page-container--home" 59 | > 60 | <h1 61 | className="home-title" 62 | > 63 | Welcome to Project Jackson! 64 | </h1> 65 | <p 66 | className="center" 67 | > 68 | <b> 69 | Project Jackson 70 | </b> 71 | is an open-source project that creates an application and deployment infrastructure using Azure App Service for Containers. 72 | <br /> 73 | <br /> 74 | Azure resources used include 75 | <a 76 | href="https://azure.microsoft.com/en-us/services/app-service/containers/" 77 | > 78 | App Service for Containers 79 | </a> 80 | , 81 | <a 82 | href="https://docs.microsoft.com/en-us/azure/cosmos-db/" 83 | > 84 | CosmosDB 85 | </a> 86 | , 87 | <a 88 | href="https://azure.microsoft.com/en-us/services/traffic-manager/" 89 | > 90 | Traffic Manager 91 | </a> 92 | , and 93 | <a 94 | href="https://azure.microsoft.com/en-us/services/application-gateway/" 95 | > 96 | Application Gateway 97 | </a> 98 | . 99 | <br /> 100 | <br /> 101 | To demonstrate Cosmos DB performance with large amounts of data, the project imports historical movie data from IMDb. See 102 | <a 103 | href="https://datasets.imdbws.com/" 104 | > 105 | here for downloadable IMDB datasets 106 | </a> 107 | . The datasets include 8.9 million people, 5.3 million movies and 30 million relationships between them. 108 | <br /> 109 | <br /> 110 | Languages used for this project include Java, Javascript, and Typescript. 111 | <br /> 112 | <br /> 113 | Technologies used include: 114 | </p> 115 | <ul 116 | className="center" 117 | > 118 | <li> 119 | <i> 120 | Java Spring 121 | </i> 122 | , a platform that provides infrastructure support for Java applications 123 | </li> 124 | <li> 125 | <i> 126 | Docker 127 | </i> 128 | , a tool used in order to isolate microservices,simplifying maintenance and testing 129 | </li> 130 | <li> 131 | <i> 132 | React 133 | </i> 134 | , a component-based front-end Javascript library, used for building UIs 135 | </li> 136 | <li> 137 | <i> 138 | Reach Router 139 | </i> 140 | , an accessible Javascript library that manages the focus of apps on route transitions 141 | </li> 142 | <li> 143 | <i> 144 | Jest 145 | </i> 146 | , a Javascript library used to test front-end rendering 147 | </li> 148 | <li> 149 | <i> 150 | Webpack 151 | </i> 152 | , a bundler for Javascript files 153 | </li> 154 | <li> 155 | <i> 156 | Azure Resource Manager (ARM) templates 157 | </i> 158 | , to simplify deployment of Azure resources and services 159 | </li> 160 | </ul> 161 | </div> 162 | </div> 163 | </div> 164 | `; 165 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/DefaultComponent.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<DefaultComponent /> renders correctly 1`] = ` 4 | <div 5 | className="default-component cover" 6 | > 7 | <h1> 8 | Error 404 9 | </h1> 10 | <p 11 | className="lead" 12 | > 13 | The requested resource could not be found but may be available again in the future. 14 | </p> 15 | </div> 16 | `; 17 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/Home.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Home /> renders correctly 1`] = ` 4 | <div 5 | className="page-container--home" 6 | > 7 | <h1 8 | className="home-title" 9 | > 10 | Welcome to Project Jackson! 11 | </h1> 12 | <p 13 | className="center" 14 | > 15 | <b> 16 | Project Jackson 17 | </b> 18 | is an open-source project that creates an application and deployment infrastructure using Azure App Service for Containers. 19 | <br /> 20 | <br /> 21 | Azure resources used include 22 | <a 23 | href="https://azure.microsoft.com/en-us/services/app-service/containers/" 24 | > 25 | App Service for Containers 26 | </a> 27 | , 28 | <a 29 | href="https://docs.microsoft.com/en-us/azure/cosmos-db/" 30 | > 31 | CosmosDB 32 | </a> 33 | , 34 | <a 35 | href="https://azure.microsoft.com/en-us/services/traffic-manager/" 36 | > 37 | Traffic Manager 38 | </a> 39 | , and 40 | <a 41 | href="https://azure.microsoft.com/en-us/services/application-gateway/" 42 | > 43 | Application Gateway 44 | </a> 45 | . 46 | <br /> 47 | <br /> 48 | To demonstrate Cosmos DB performance with large amounts of data, the project imports historical movie data from IMDb. See 49 | <a 50 | href="https://datasets.imdbws.com/" 51 | > 52 | here for downloadable IMDB datasets 53 | </a> 54 | . The datasets include 8.9 million people, 5.3 million movies and 30 million relationships between them. 55 | <br /> 56 | <br /> 57 | Languages used for this project include Java, Javascript, and Typescript. 58 | <br /> 59 | <br /> 60 | Technologies used include: 61 | </p> 62 | <ul 63 | className="center" 64 | > 65 | <li> 66 | <i> 67 | Java Spring 68 | </i> 69 | , a platform that provides infrastructure support for Java applications 70 | </li> 71 | <li> 72 | <i> 73 | Docker 74 | </i> 75 | , a tool used in order to isolate microservices,simplifying maintenance and testing 76 | </li> 77 | <li> 78 | <i> 79 | React 80 | </i> 81 | , a component-based front-end Javascript library, used for building UIs 82 | </li> 83 | <li> 84 | <i> 85 | Reach Router 86 | </i> 87 | , an accessible Javascript library that manages the focus of apps on route transitions 88 | </li> 89 | <li> 90 | <i> 91 | Jest 92 | </i> 93 | , a Javascript library used to test front-end rendering 94 | </li> 95 | <li> 96 | <i> 97 | Webpack 98 | </i> 99 | , a bundler for Javascript files 100 | </li> 101 | <li> 102 | <i> 103 | Azure Resource Manager (ARM) templates 104 | </i> 105 | , to simplify deployment of Azure resources and services 106 | </li> 107 | </ul> 108 | </div> 109 | `; 110 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/Navbar.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Navbar /> renders correctly 1`] = ` 4 | <nav 5 | className="nav-container" 6 | > 7 | <span 8 | className="nav-title" 9 | > 10 | Project Jackson 11 | </span> 12 | <div 13 | className="nav-link-container" 14 | > 15 | <a 16 | aria-current="page" 17 | className="nav-link active" 18 | href="/" 19 | onClick={[Function]} 20 | > 21 | Home 22 | </a> 23 | <a 24 | className="nav-link" 25 | href="/people" 26 | onClick={[Function]} 27 | > 28 | People 29 | </a> 30 | <a 31 | className="nav-link" 32 | href="/titles" 33 | onClick={[Function]} 34 | > 35 | Titles 36 | </a> 37 | <button 38 | className="login-button" 39 | onClick={[Function]} 40 | > 41 | Log In 42 | </button> 43 | </div> 44 | </nav> 45 | `; 46 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/PageForm.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<PageForm /> renders correctly 1`] = ` 4 | <form 5 | className="form" 6 | > 7 | <label> 8 | <span 9 | className="form-field-pre-text" 10 | > 11 | test 12 | </span> 13 | <input 14 | className="form-field-input" 15 | name="id" 16 | onChange={[Function]} 17 | placeholder="test" 18 | type="text" 19 | /> 20 | <span 21 | className="form-field-sub-text" 22 | /> 23 | </label> 24 | <input 25 | className="form-field-submit" 26 | onClick={[Function]} 27 | type="submit" 28 | value="Search" 29 | /> 30 | </form> 31 | `; 32 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/Person.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Person /> renders correctly 1`] = ` 4 | <div 5 | className="page-container" 6 | > 7 | <div 8 | className="form-container" 9 | > 10 | <h1> 11 | Search People 12 | </h1> 13 | <p 14 | className="form-description" 15 | > 16 | Enter the ID for a person to retrieve their information from the database. Leave the form empty to get information about a random person. 17 | </p> 18 | <form 19 | className="form" 20 | > 21 | <label> 22 | <span 23 | className="form-field-pre-text" 24 | > 25 | Person ID: 26 | </span> 27 | <input 28 | className="form-field-input" 29 | name="id" 30 | onChange={[Function]} 31 | placeholder="Person ID" 32 | type="text" 33 | /> 34 | <span 35 | className="form-field-sub-text" 36 | /> 37 | </label> 38 | <input 39 | className="form-field-submit" 40 | onClick={[Function]} 41 | type="submit" 42 | value="Search" 43 | /> 44 | </form> 45 | </div> 46 | <div 47 | className="results-container" 48 | > 49 | <h2 50 | className="results-title" 51 | > 52 | Results for Person ID: 53 | </h2> 54 | <pre 55 | className="results-view" 56 | > 57 | null 58 | </pre> 59 | <h4 /> 60 | </div> 61 | </div> 62 | `; 63 | -------------------------------------------------------------------------------- /ui/src/__tests__/__snapshots__/Title.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Title /> renders correctly 1`] = ` 4 | <div 5 | className="page-container" 6 | > 7 | <div 8 | className="form-container" 9 | > 10 | <h1> 11 | Search Titles 12 | </h1> 13 | <p 14 | className="form-description" 15 | > 16 | Enter an ID to get information about a title from the database. Leave the form empty to get information about a random title. 17 | </p> 18 | <form 19 | className="form" 20 | > 21 | <label> 22 | <span 23 | className="form-field-pre-text" 24 | > 25 | Title ID: 26 | </span> 27 | <input 28 | className="form-field-input" 29 | name="id" 30 | onChange={[Function]} 31 | placeholder="Title ID" 32 | type="text" 33 | /> 34 | <span 35 | className="form-field-sub-text" 36 | /> 37 | </label> 38 | <input 39 | className="form-field-submit" 40 | onClick={[Function]} 41 | type="submit" 42 | value="Search" 43 | /> 44 | </form> 45 | </div> 46 | <div 47 | className="results-container" 48 | > 49 | <h2 50 | className="results-title" 51 | > 52 | Results for Title ID: 53 | </h2> 54 | <pre 55 | className="results-view" 56 | > 57 | null 58 | </pre> 59 | <h4 /> 60 | </div> 61 | </div> 62 | `; 63 | -------------------------------------------------------------------------------- /ui/src/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import * as Enzyme from 'enzyme' 2 | import * as Adapter from 'enzyme-adapter-react-16' 3 | 4 | Enzyme.configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /ui/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from '@reach/router' 2 | import * as Msal from 'msal' 3 | import * as React from 'react' 4 | 5 | import { AuthProvider } from './AuthContext' 6 | import { AuthResponseBar } from './AuthResponseBar' 7 | import { DefaultComponent } from './DefaultComponent' 8 | import { Home } from './Home' 9 | import { Navbar } from './Navbar' 10 | import { Person } from './Person' 11 | import { PrivateRoute } from './PrivateRoute' 12 | import { Title } from './Title' 13 | 14 | import './../styles/App.css' 15 | 16 | // Webpack will automatically replace this variable during build time 17 | const appConfig = { 18 | clientID: process.env.WEBPACK_PROP_AAD_CLIENT_ID, // defaulted to '' when no OAuth client id is passed in 19 | } 20 | 21 | // initialize the UserAgentApplication globally so popup and iframe can run in the background 22 | // short circuit userAgentApp. If clientID is null so is userAgentApp 23 | const userAgentApp = appConfig.clientID && new Msal.UserAgentApplication(appConfig.clientID, null, null) 24 | 25 | // webpack replaces this variable on build time 26 | let basepath = process.env.WEBPACK_PROP_UI_BASEPATH || '' // default to empty string for testing 27 | const c = basepath.charAt(0) 28 | basepath = c === '' || c === '/' || c === '\\' ? basepath : `/${basepath}` 29 | 30 | export class App extends React.Component { 31 | 32 | // App is still responsible for managing the auth state 33 | // it uses the AuthContext to share this with other aspects of the UI 34 | // including the navbar, private route component, and the pages themselves 35 | public state = { 36 | accessToken: null, 37 | authResponse: null, 38 | } 39 | 40 | public handleAuth = async () => { 41 | if (appConfig.clientID === '') { // no auth flow case 42 | if (this.state.accessToken !== null) { 43 | this.setState({ accessToken: null }) 44 | } else { 45 | // tslint:disable-next-line no-console max-line-length 46 | console.warn('AAD Client ID has not been configured. If you are currently in production mode, see the \'deploy\' documentation for details on how to fix this.') 47 | this.setState({ accessToken: '' }) 48 | } 49 | } else { // normal or 'production' auth flow 50 | let accessToken = null 51 | try { 52 | if (this.state.accessToken !== null) { 53 | // log out 54 | await userAgentApp.logout() 55 | } else { 56 | // log in 57 | const graphScopes = [appConfig.clientID] 58 | await userAgentApp.loginPopup(graphScopes) 59 | accessToken = await userAgentApp.acquireTokenSilent(graphScopes) 60 | } 61 | this.setState({ accessToken }) 62 | } catch (err) { 63 | this.setAuthResponse(err.toString()) 64 | } 65 | } 66 | } 67 | 68 | public setAuthResponse = (msg: string) => { 69 | this.setState({ authResponse: msg }) 70 | } 71 | 72 | public render() { 73 | // Implementing the authprovider at app root to share the auth state 74 | // with all internal components (if they subscribe to it) 75 | // by linking the accessToken to the app state we can be certain the 76 | // context will always update and propogate the value to subscribed nodes 77 | return ( 78 | <AuthProvider value={{ 79 | accessToken: this.state.accessToken, 80 | authResponse: this.state.authResponse, 81 | handleAuth: this.handleAuth, 82 | setAuthResponse: this.setAuthResponse, 83 | }}> 84 | <div className='app-container'> 85 | <Navbar basepath={basepath} /> 86 | <AuthResponseBar /> 87 | <Router basepath={basepath}> 88 | <Home path='/' /> 89 | <PrivateRoute as={Person} path='/people' /> 90 | <PrivateRoute as={Title} path='/titles' /> 91 | <DefaultComponent default /> 92 | </Router> 93 | </div> 94 | </AuthProvider> 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ui/src/components/AuthButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { AuthContext } from './AuthContext' 4 | 5 | export class AuthButton extends React.Component { 6 | public static contextType = AuthContext 7 | public render() { 8 | return ( 9 | <button 10 | onClick={this.context.handleAuth} 11 | className='login-button' 12 | > 13 | {this.context.accessToken !== null ? 'Log Out' : 'Log In'} 14 | </button> 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/components/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export interface IAuthContextValues { 4 | accessToken: string, 5 | authResponse: string, 6 | handleAuth: () => void, 7 | setAuthResponse: (msg?: string) => void, 8 | } 9 | 10 | // Default values and enforce use of interface 11 | const defaultAuthContextValue: IAuthContextValues = { 12 | accessToken: null, 13 | authResponse: null, 14 | handleAuth: () => null, 15 | setAuthResponse: () => null, 16 | } 17 | 18 | export const AuthContext = createContext<IAuthContextValues>(defaultAuthContextValue) 19 | // exporting the Provider and Consumer components for more specific imports 20 | export const AuthProvider = AuthContext.Provider 21 | export const AuthConsumer = AuthContext.Consumer 22 | -------------------------------------------------------------------------------- /ui/src/components/AuthResponseBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { AuthContext } from './AuthContext' 4 | 5 | export class AuthResponseBar extends React.Component { 6 | public static contextType = AuthContext 7 | public render() { 8 | return this.context.authResponse && ( 9 | <div className='auth-message-container'> 10 | <button className='auth-message-close-button' onClick={() => this.context.setAuthResponse(null)}>X</button> 11 | <span className='auth-message'>{this.context.authResponse}</span> 12 | </div> 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/components/DefaultComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface IDefaultComponentProps { 4 | default: boolean, 5 | } 6 | 7 | export class DefaultComponent extends React.Component<IDefaultComponentProps> { 8 | public render() { 9 | return ( 10 | <div className='default-component cover'> 11 | <h1>Error 404</h1> 12 | <p className='lead'> 13 | The requested resource could not be found but may be available again in the future. 14 | </p> 15 | </div> 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface IHomeProps { path?: string, } 4 | 5 | export interface IHomeState { image: string } 6 | 7 | export class Home extends React.Component<IHomeProps, IHomeState> { 8 | public render() { 9 | return( 10 | <div className = 'page-container--home' > 11 | <h1 className='home-title'>Welcome to Project Jackson!</h1> 12 | <p className='center'> 13 | <b>Project Jackson</b> is an open-source project that creates an application and deployment 14 | infrastructure using Azure App Service for Containers. 15 | <br /> 16 | <br /> 17 | 18 | Azure resources used include 19 | <a href='https://azure.microsoft.com/en-us/services/app-service/containers/'> 20 | App Service for Containers 21 | </a>, <a 22 | href='https://docs.microsoft.com/en-us/azure/cosmos-db/'>CosmosDB 23 | </a>, <a href='https://azure.microsoft.com/en-us/services/traffic-manager/'> 24 | Traffic Manager</a>, 25 | and <a href='https://azure.microsoft.com/en-us/services/application-gateway/'>Application Gateway</a>. 26 | <br /> 27 | <br /> 28 | 29 | To demonstrate Cosmos DB performance with large amounts of data, 30 | the project imports historical movie data from IMDb. 31 | See <a href='https://datasets.imdbws.com/'> here for downloadable IMDB datasets</a>. 32 | The datasets include 8.9 million people, 5.3 million movies and 30 million relationships between them. 33 | <br /> 34 | <br /> 35 | 36 | Languages used for this project include Java, Javascript, and Typescript. 37 | <br /> 38 | <br /> 39 | Technologies used include: 40 | </p> 41 | <ul className='center'> 42 | <li><i>Java Spring</i>, a platform that provides infrastructure support for Java applications </li> 43 | <li><i>Docker</i>, a tool used in order to isolate microservices,simplifying maintenance and 44 | testing</li> 45 | <li><i>React</i>, a component-based front-end Javascript library, 46 | used for building UIs </li> 47 | <li><i>Reach Router</i>, an accessible Javascript library that manages the focus of apps on 48 | route transitions</li> 49 | <li><i>Jest</i>, a Javascript library used to test front-end rendering</li> 50 | <li><i>Webpack</i>, a bundler for Javascript files</li> 51 | <li><i>Azure Resource Manager (ARM) templates</i>, to simplify deployment of Azure resources 52 | and services</li> 53 | </ul> 54 | </div> 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@reach/router' 2 | import * as React from 'react' 3 | 4 | import { AuthButton } from './AuthButton' 5 | 6 | import './../styles/Navbar.css' 7 | 8 | const isActive = ({ isCurrent }) => { 9 | return { 10 | className: isCurrent ? 'nav-link active' : 'nav-link', 11 | } 12 | } 13 | 14 | export interface INavbarProps { basepath?: string } 15 | 16 | export class Navbar extends React.Component<INavbarProps> { 17 | public render() { 18 | const basepath = this.props.basepath || '' 19 | 20 | return ( 21 | <nav className='nav-container'> 22 | <span className='nav-title'> 23 | Project Jackson 24 | </span> 25 | <div className='nav-link-container'> 26 | <Link getProps={isActive} to={`${basepath}/`}>Home</Link> 27 | <Link getProps={isActive} to={`${basepath}/people`}>People</Link> 28 | <Link getProps={isActive} to={`${basepath}/titles`}>Titles</Link> 29 | <AuthButton /> 30 | </div> 31 | </nav> 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/components/PageForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface IPageFormProps { 4 | inputTitle: string, 5 | inputPlaceholder: string, 6 | onInputChange: (event) => void, 7 | onSubmitClick: (event) => void, 8 | } 9 | 10 | export function PageForm(props: IPageFormProps) { 11 | return ( 12 | <form className='form'> 13 | <label> 14 | <span className='form-field-pre-text'>{props.inputTitle}</span> 15 | <input 16 | className='form-field-input' 17 | type='text' 18 | name='id' 19 | onChange={props.onInputChange} 20 | placeholder={props.inputPlaceholder} 21 | /> 22 | <span className='form-field-sub-text'></span> 23 | 24 | </label> 25 | <input 26 | className='form-field-submit' 27 | type='submit' 28 | value='Search' 29 | onClick={props.onSubmitClick} 30 | /> 31 | </form> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/components/Person.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { AuthContext } from './AuthContext' 4 | import { PageForm } from './PageForm' 5 | 6 | export interface IPersonProps { path: string } 7 | 8 | export interface IPersonState { loading: boolean, personId: string, result: object } 9 | 10 | export interface IPersonResponse extends Response { 11 | _embedded?: { persons: Array<{ nconst: string }> }, 12 | error?: string, 13 | error_description?: string 14 | } 15 | 16 | export interface IPersonResult { nconst: string } 17 | 18 | export class Person extends React.Component<IPersonProps, IPersonState> { 19 | public static contextType = AuthContext 20 | 21 | public state = { 22 | loading: false, 23 | personId: null, 24 | result: null, 25 | } 26 | 27 | public render() { 28 | return ( 29 | <div className='page-container'> 30 | <div className='form-container'> 31 | <h1>Search People</h1> 32 | <p className='form-description'>Enter the ID for a person to retrieve their information from the database. 33 | Leave the form empty to get information about a random person.</p> 34 | <PageForm 35 | inputTitle='Person ID:' 36 | inputPlaceholder='Person ID' 37 | onInputChange={this.handleNameInputChange} 38 | onSubmitClick={this.handleFormSubmit} 39 | /> 40 | </div> 41 | <div className='results-container'> 42 | <h2 className='results-title'>{`Results for Person ID: ${this.state.result ? this.state.personId : ''}`}</h2> 43 | <pre className='results-view'>{JSON.stringify(this.state.result, null, 2)}</pre> 44 | <h4>{this.state.loading ? 'Loading. . .' : null}</h4> 45 | </div> 46 | </div> 47 | ) 48 | } 49 | 50 | private handleNameInputChange = (event) => this.setState({ 51 | personId: event.target.value, 52 | result: null, 53 | }) 54 | 55 | private handleFormSubmit = async (event) => { 56 | event.preventDefault() 57 | 58 | this.setState({ loading: true }) 59 | 60 | // set up endpoint 61 | const id = this.state.personId && this.state.personId.replace(/\s+/g, '') 62 | const base = `${process.env.WEBPACK_PROP_API_BASE_URL}/people` 63 | const endpoint = id ? base + '/' + id : base 64 | 65 | // set up request header with Bearer token 66 | const headers = new Headers() 67 | const bearer = `Bearer ${this.context.accessToken}` 68 | headers.append('Authorization', bearer) 69 | const options = { 70 | headers, 71 | method: 'GET', 72 | } 73 | 74 | try { 75 | const response = await fetch(endpoint, options) 76 | const resOut: IPersonResponse = await response.json() 77 | 78 | if (resOut.hasOwnProperty('error')) { 79 | throw new Error(`Error: ${resOut.error}, ${resOut.error_description}`) 80 | } else if (id) { 81 | this.setState({ result: resOut }) 82 | } else { 83 | const persons = resOut._embedded.persons 84 | const result: IPersonResult = persons[Math.floor(Math.random() * persons.length)] 85 | this.setState({ result, personId: result.nconst }) 86 | } 87 | } catch (err) { 88 | this.setState({ result: { error: err.message } }) 89 | } finally { 90 | this.setState({ loading: false }) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui/src/components/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { AuthContext } from './AuthContext' 4 | import { Home } from './Home' 5 | 6 | export interface IPrivateRouteProps { as: React.ComponentType, path: string } 7 | 8 | export class PrivateRoute extends React.Component<IPrivateRouteProps> { 9 | public static contextType = AuthContext 10 | 11 | public componentDidMount() { 12 | if (this.context.accessToken === null) { 13 | this.context.setAuthResponse(`Please log in to access: ${this.props.path}`) 14 | } 15 | } 16 | 17 | public componentDidUpdate(prevProps, prevState, snapshot) { 18 | if (this.context.accessToken === null && this.props.path !== prevProps.path) { 19 | this.context.setAuthResponse(`Please log in to access: ${this.props.path}`) 20 | } else if (this.context.authResponse !== null && this.context.accessToken !== null) { 21 | this.context.setAuthResponse(null) 22 | } 23 | } 24 | 25 | public componentWillUnmount() { 26 | this.context.setAuthResponse(null) 27 | } 28 | 29 | public render() { 30 | const { as: Component, ...props } = this.props 31 | // this private route uses the existence of the accessToken to 32 | // lock/unlock the private routes. We don't pass down the values from 33 | // context as we would rather subscribe to them directly in the 34 | // components themselves. 35 | return this.context.accessToken !== null ? <Component {...props} /> : <Home {...props} /> 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { AuthContext } from './AuthContext' 4 | import { PageForm } from './PageForm' 5 | 6 | export interface ITitleProps { path: string } 7 | 8 | export interface ITitleState { loading: boolean, titleId: string, result: object } 9 | 10 | export interface ITitleResponse extends Response { 11 | _embedded?: { titles: Array<{ tconst: string }> }, 12 | error?: string, 13 | error_description?: string 14 | } 15 | 16 | export interface ITitleResult { tconst: string } 17 | 18 | export class Title extends React.Component<ITitleProps, ITitleState> { 19 | public static contextType = AuthContext 20 | 21 | public state = { 22 | loading: false, 23 | result: null, 24 | titleId: null, 25 | } 26 | 27 | public render() { 28 | return ( 29 | <div className='page-container'> 30 | <div className='form-container'> 31 | <h1>Search Titles</h1> 32 | <p className='form-description'>Enter an ID to get information about a title from the database. 33 | Leave the form empty to get information about a random title.</p> 34 | <PageForm 35 | inputTitle='Title ID:' 36 | inputPlaceholder='Title ID' 37 | onInputChange={this.handleNameInputChange} 38 | onSubmitClick={this.handleFormSubmit} 39 | /> 40 | </div> 41 | <div className='results-container'> 42 | <h2 className='results-title'>{`Results for Title ID: ${this.state.result ? this.state.titleId : ''}`}</h2> 43 | <pre className='results-view'>{JSON.stringify(this.state.result, null, 2)}</pre> 44 | <h4>{this.state.loading ? 'Loading. . .' : null}</h4> 45 | </div> 46 | </div> 47 | ) 48 | } 49 | 50 | private handleNameInputChange = (event) => this.setState({ 51 | result: null, 52 | titleId: event.target.value, 53 | }) 54 | 55 | private handleFormSubmit = async (event) => { 56 | event.preventDefault() 57 | 58 | this.setState({ loading: true }) 59 | 60 | // set up endpoint 61 | const id = this.state.titleId && this.state.titleId.replace(/\s+/g, '') 62 | const base = `${process.env.WEBPACK_PROP_API_BASE_URL}/titles` 63 | const endpoint = id ? base + '/' + id : base 64 | 65 | // set up request header with Bearer token 66 | const headers = new Headers() 67 | const bearer = `Bearer ${this.context.accessToken}` 68 | headers.append('Authorization', bearer) 69 | const options = { 70 | headers, 71 | method: 'GET', 72 | } 73 | 74 | try { 75 | const response = await fetch(endpoint, options) 76 | const resOut: ITitleResponse = await response.json() 77 | 78 | if (resOut.hasOwnProperty('error')) { 79 | throw new Error(`Error: ${resOut.error}, ${resOut.error_description}`) 80 | } else if (id) { 81 | this.setState({ result: resOut }) 82 | } else { 83 | const titles = resOut._embedded.titles 84 | const result: ITitleResult = titles[Math.floor(Math.random() * titles.length)] 85 | this.setState({ result, titleId: result.tconst }) 86 | } 87 | } catch (err) { 88 | this.setState({ result: { error: err.message } }) 89 | } finally { 90 | this.setState({ loading: false }) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | 4 | import { App } from './components/App' 5 | 6 | ReactDOM.render( 7 | <App />, 8 | document.getElementById('root'), 9 | ) 10 | -------------------------------------------------------------------------------- /ui/src/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <meta http-equiv="X-UA-Compatible" content="ie=edge"> 7 | <title>Project Jackson 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /ui/src/public/mountains.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/styles/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans'); 2 | 3 | body { 4 | font-family: 'Open Sans', Helvetica, sans-serif; 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | .app-container { 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | margin-bottom: 20px; 14 | } 15 | 16 | .page-container { 17 | flex-grow: 1; 18 | display: flex; 19 | flex-direction: row; 20 | padding: 0 10px; 21 | justify-content: space-around; 22 | } 23 | 24 | .page-container--home { 25 | flex-grow: 1; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | } 30 | 31 | .form-container { 32 | width: 300px; 33 | } 34 | 35 | .form-description { 36 | text-align: justify; 37 | } 38 | 39 | .form { 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | 44 | .form-field-input { 45 | display: block; 46 | width: 300px; 47 | border-radius: 5px; 48 | font-size: 1rem; 49 | -webkit-box-shadow: inset 2px 2px 2px 0px #dddddd; 50 | -moz-box-shadow: inset 2px 2px 2px 0px #dddddd; 51 | box-shadow: inset 2px 2px 2px 0px #dddddd; 52 | border: 1px solid #ccc; 53 | padding: 7px 10px; 54 | box-sizing: border-box; 55 | } 56 | 57 | .form-field-submit { 58 | width: 300px; 59 | margin-top: 20px; 60 | background-color: rgb(50, 146, 202); 61 | border: 1px solid rgb(50, 146, 202); 62 | border-radius: 5px; 63 | color: #FFF; 64 | font-size: 1.2rem; 65 | padding: 10px 0; 66 | } 67 | 68 | .results-container { 69 | padding: 20px; 70 | width: max-content; 71 | max-width: 50%; 72 | } 73 | 74 | .results-view { 75 | overflow-x: scroll; 76 | border: 1px solid #ccc; 77 | background: rgba(0, 0, 0, 0.1); 78 | box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2); 79 | padding: 20px; 80 | } 81 | 82 | .home-title { 83 | text-align: center; 84 | } 85 | 86 | .home-img { 87 | width: 250px; 88 | margin: auto; 89 | } 90 | 91 | .center { 92 | margin: auto; 93 | width: 50%; 94 | } 95 | 96 | .auth-message-container { 97 | background-color: #ccc; 98 | padding: 5px 15px; 99 | } 100 | .auth-message { 101 | color: rgb(221, 26, 26) 102 | } 103 | 104 | .auth-message-close-button { 105 | color: rgb(221, 26, 26); 106 | font-weight: 700; 107 | padding: 5px; 108 | margin-right: 20px; 109 | border: none; 110 | cursor: pointer; 111 | } 112 | 113 | .login-button { 114 | border: none; 115 | cursor: pointer; 116 | background-color: rgb(50, 146, 202); 117 | } 118 | 119 | .default-page-image { 120 | padding-left: 33%; 121 | width: 400px; 122 | } 123 | -------------------------------------------------------------------------------- /ui/src/styles/Navbar.css: -------------------------------------------------------------------------------- 1 | .nav-container { 2 | height: 65px; 3 | background-color: rgb(43, 43, 43); 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | .nav-title { 13 | font-size: 1.6rem; 14 | color: rgb(50, 146, 202); 15 | } 16 | 17 | .nav-link-container { 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: right; 21 | } 22 | 23 | .nav-link { 24 | margin: 0 10px; 25 | font-size: 1.3rem; 26 | color: rgb(50, 146, 202); 27 | text-decoration: none; 28 | } 29 | 30 | .active { 31 | color: rgb(61, 173, 85); 32 | } -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "jsx": "react", 7 | "lib": [ "es2015", "dom" ], 8 | "allowSyntheticDefaultImports": true, 9 | "typeRoots": [ 10 | "./node_modules/@types", 11 | "./types" 12 | ] 13 | }, 14 | "exclude": [ 15 | "./node_modules/**/*" 16 | ], 17 | "include": [ 18 | "./src/**/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "semicolon": [true, "never"], 10 | "indent": [true, "spaces", 4], 11 | "ordered-imports": [ 12 | true, 13 | { 14 | "grouped-imports": true 15 | } 16 | ] 17 | }, 18 | "rulesDirectory": [] 19 | } -------------------------------------------------------------------------------- /ui/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | entry: "./src/index.tsx", 7 | output: { 8 | path: path.resolve(__dirname + "/dist"), 9 | filename: "bundle.js", 10 | publicPath: "/" 11 | }, 12 | devtool: "source-map", 13 | resolve: { 14 | extensions: [".ts", ".tsx", ".js", ".json"] 15 | }, 16 | module: { 17 | rules: [ 18 | { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, 19 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, 20 | { test: /\.css$/, loader: ["style-loader", "css-loader"] } 21 | ] 22 | }, 23 | plugins: [ 24 | new HtmlWebpackPlugin({ 25 | template: './src/public/index.html', 26 | favicon: './src/public/mountains.svg' 27 | }), 28 | // DefinePlugin will inject this env variable anywhere in the code base it ref the attr name 29 | // new webpack.DefinePlugin({ 30 | // WEBPACK_PROP_AAD_CLIENT_ID: process.env.WEBPACK_PROP_AAD_CLIENT_ID ? `"${process.env.WEBPACK_PROP_AAD_CLIENT_ID}"` : null, 31 | // WEBPACK_PROP_API_BASE_URL: process.env.WEBPACK_PROP_API_BASE_URL ? `"${process.env.WEBPACK_PROP_API_BASE_URL}"` : null, 32 | // WEBPACK_PROP_UI_BASEPATH: process.env.WEBPACK_PROP_UI_BASEPATH ? `"${process.env.WEBPACK_PROP_UI_BASEPATH}"` : null 33 | // }), 34 | new webpack.EnvironmentPlugin({ 35 | WEBPACK_PROP_AAD_CLIENT_ID: '', 36 | WEBPACK_PROP_API_BASE_URL: '', 37 | WEBPACK_PROP_UI_BASEPATH: 'ui' 38 | }) 39 | ], 40 | } -------------------------------------------------------------------------------- /ui/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | port: 3000, 9 | open: true, 10 | historyApiFallback: true, 11 | openPage: process.env.WEBPACK_PROP_UI_BASEPATH || 'ui' 12 | }, 13 | }) -------------------------------------------------------------------------------- /ui/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | }) --------------------------------------------------------------------------------