├── .gitignore ├── LICENSE ├── README.adoc ├── README.md ├── build.gradle ├── cities-client └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── cities │ │ └── client │ │ ├── CityRepository.java │ │ ├── CityRepositoryFactory.java │ │ ├── cloud │ │ ├── WebServiceInfo.java │ │ ├── cloudfoundry │ │ │ └── CitiesWebServiceInfoCreator.java │ │ ├── connector │ │ │ └── CitiesRepositoryConnectionCreator.java │ │ └── localconfig │ │ │ └── CitiesWebServiceInfoCreator.java │ │ └── model │ │ ├── City.java │ │ └── PagedCities.java │ └── resources │ └── META-INF │ └── services │ ├── org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator │ ├── org.springframework.cloud.localconfig.LocalConfigServiceInfoCreator │ └── org.springframework.cloud.service.ServiceConnectorCreator ├── cities-service ├── README.adoc ├── build.gradle ├── demo-script.adoc ├── manifest.yml └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── cities │ │ ├── Application.java │ │ ├── config │ │ └── CloudDataSourceConfig.java │ │ ├── domain │ │ └── City.java │ │ └── repositories │ │ └── CityRepository.java │ └── resources │ ├── application.properties │ ├── import.sql │ └── logback.xml ├── cities-ui ├── manifest.yml └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── cities │ │ ├── AppInitializer.java │ │ ├── Application.java │ │ ├── config │ │ ├── CloudConfiguration.java │ │ └── DefaultConfiguration.java │ │ └── controller │ │ └── CitiesController.java │ └── resources │ ├── application.properties │ └── static │ ├── images │ ├── 404-icon.png │ ├── homepage-bg.jpg │ ├── platform-bg.png │ ├── platform-spring-xd.png │ ├── spring-logo-platform.png │ └── spring-logo-xd-mobile.png │ ├── index.html │ ├── less │ ├── header.less │ ├── main.less │ ├── responsive.less │ └── typography.less │ ├── scripts │ ├── app.js │ ├── controllers │ │ └── cities-controller.js │ └── routes.js │ ├── styles │ ├── header.css │ ├── main.css │ ├── responsive.css │ └── typography.css │ └── views │ └── cities.html ├── data ├── import-TX-pgsql.sql ├── import-TX.sql └── import-pgsql.sql ├── docs ├── components.graffle └── components.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .settings 6 | .classpath 7 | .project 8 | .springbeans 9 | .gradle 10 | target 11 | out 12 | build 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015-Present Pivotal Software Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Building and consuming a cloud-ready Microservice 2 | 3 | This repository contains a sample project to demonstrate building and deploying a simple Spring Boot based microservice and a web app to consume the microservice. It was built in part to be used as part of a http://www.slideshare.net/ramnivas2/spring-one2014-springcloudconnector[presentation on Spring Cloud Connectors]. 4 | 5 | This project contains three sub-projects: 6 | 7 | image:docs/components.png["Components",50%] 8 | 9 | The following sections describe the components from the bottom up. 10 | 11 | === cities-service 12 | 13 | The **cities-service** subproject is an example of a very simple but fully functional microservice. The microservice is built using http://projects.spring.io/spring-boot[Spring Boot]. It uses http://projects.spring.io/spring-data-jpa[Spring Data JPA] to access a database and http://projects.spring.io/spring-data-rest[Spring Data REST] to expose the database contents via a REST API. http://cloud.spring.io/spring-cloud-connectors[Spring Cloud Connectors] is used to create the database connection from information exposed by the cloud environment. 14 | 15 | This subproject also provides a link:cities-service/demo-script.adoc[script] that can be used to demonstrate construction and deployment of a microservice from scratch with just a few source code files. 16 | 17 | **Key concepts:** 18 | 19 | * Simple application configuration using Spring Boot 20 | ** link:cities-service/src/main/java/com/example/cities/Application.java[Application.java] 21 | ** link:cities-service/src/main/java/com/example/cities/WebConfiguration.java[WebConfiguration.java] 22 | * Data access using Spring Data JPA and exposing a REST API using Spring Data REST 23 | ** link:cities-service/src/main/java/com/example/cities/repositories/CityRepository.java[CityRepository.java] 24 | * Consuming a cloud data source using Spring Cloud Connectors 25 | ** link:cities-service/src/main/java/com/example/cities/config/CloudDataSourceConfig.java[CloudDataSourceConfig.java] 26 | 27 | === cities-client 28 | 29 | The **cities-client** subproject provides a client library for use by Java apps consuming the microservice. The main goal of this library is to show an example of a http://cloud.spring.io/spring-cloud-connectors[Spring Cloud Connectors] extension for consuming a microservice in a cloud environment. 30 | 31 | The client library uses https://github.com/Netflix/feign[Feign] to expose the microservice REST API using a http://martinfowler.com/eaaCatalog/repository.html[Repository] pattern. This provides a nice analog to the Repository abstraction used by Spring Data. 32 | 33 | The same Spring Cloud Connectors extension technique could be used to create lower-level REST API connection objects like Spring http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#rest-resttemplate[RestTemplate] or https://hc.apache.org/httpcomponents-client-ga[Apache HttpClient]. 34 | 35 | **Key concepts:** 36 | 37 | * A `ServiceInfo` class to model the connection information needed to access the REST API (just a URL in this example) 38 | ** link:cities-client/src/main/java/com/example/cities/client/cloud/WebServiceInfo.java[WebServiceInfo.java] 39 | * A `ServiceInfoCreator` class to populate the `ServiceInfo` class from information exposed by Cloud Foundry 40 | ** link:cities-client/src/main/java/com/example/cities/client/cloud/cloudfoundry/CitiesWebServiceInfoCreator.java[CitiesWebServiceInfoCreator.java] 41 | * A `ServiceConnectionCreator` class to create the Feign repository from the information contained in the `ServiceInfo` 42 | ** link:cities-client/src/main/java/com/example/cities/client/CityRepository.java[CityRepository.java] 43 | ** link:cities-client/src/main/java/com/example/cities/client/CityRepositoryFactory.java[CityRepositoryFactory.java] 44 | ** link:cities-client/src/main/java/com/example/cities/client/cloud/connector/CitiesRepositoryConnectionCreator.java[CitiesRepositoryConnectionCreator.java] 45 | * Registration of the `ServiceInfoCreator` and `ServiceConnectionCreator` to the Spring Cloud Connectors framework 46 | ** link:cities-client/src/main/resources/META-INF/services/org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator[CloudFoundryServiceInfoCreator] 47 | ** link:cities-client/src/main/resources/META-INF/services/org.springframework.cloud.service.ServiceConnectorCreator[ServiceConnectorCreator] 48 | 49 | === cities-ui 50 | 51 | The **cities-ui** subproject is a web UI application that uses the client library to consume the microservice REST API. It is built using http://projects.spring.io/spring-boot[Spring Boot] and https://angularjs.org[AngularJS]. 52 | 53 | **Key concepts:** 54 | 55 | * Simple application configuration using Spring Boot 56 | ** link:cities-ui/src/main/java/com/example/cities/Application.java[Application.java] 57 | * Proxying calls from the AngularJS front-end to the repository backend with Spring MVC 58 | ** link:cities-ui/src/main/java/com/example/cities/controller/CitiesController.java[CitiesController.java] 59 | * Consuming the client library using Spring Cloud Connectors 60 | ** link:cities-ui/src/main/java/com/example/cities/config/CloudConfiguration.java[CloudConfiguration.java] 61 | 62 | == Building the project 63 | 64 | To build applications and library, you will need to install http://www.gradle.org/installation[Gradle]. Once Gradle is installed, you can run this command from the project root: 65 | 66 | [source,bash] 67 | ---- 68 | $ gradle assemble 69 | ---- 70 | 71 | == Deploying the apps to Cloud Foundry 72 | 73 | To deploy the microservice and web UI applications to Cloud Foundry, you will need to: 74 | 75 | * install the http://docs.cloudfoundry.org/devguide/installcf/install-go-cli.html[Cloud Foundry CLI] 76 | * use the Cloud Foundry CLI to http://docs.cloudfoundry.org/devguide/installcf/whats-new-v6.html#login[log into your Cloud Foundry system] 77 | 78 | === Deploying the microservice 79 | 80 | Use the following commands to create a database service instance for the microservice and push the microservice to Cloud Foundry: 81 | 82 | [source,bash] 83 | ---- 84 | $ cf create-service [service-label] [service-plan] cities-db 85 | $ cd cities-service && cf push && cd .. 86 | ---- 87 | 88 | ==== Note on data import 89 | 90 | The microservice loads a very large dataset at startup to show the power of the paging, sorting, and search capabilities in Spring Data. The default link:cities-service/src/main/resources/import.sql[`import.sql`] file contains just under 43,000 small rows (representing all postal codes in the United States) that get loaded when the application starts. 91 | 92 | Free database service tiers on public Cloud Foundry services often limit the size of the database you can use and the number of records you can load at startup. You will likely need to reduce the size of the dataset when deploying to a public Cloud Foundry service with a free database tier. You can use the provided link:data/import-TX.sql[`import-TX.sql`], which contains just under 2,700 rows (representing postal codes in the US state of Texas), or you can edit the `import.sql` file to create your own subset. 93 | 94 | The default `import.sql` file works with the in-memory HyperSQL database (HSQLDB) and MySQL. If you want the microservice to use a PostgreSQL database, you can use the link:data/import-pgsql.sql[`import-pgsql.sql`] import file or the reduced link:data/import-TX-pgsql.sql[`import-TX-pgsql.sql`] file. 95 | 96 | To use any import file other than the default `import.sql`, copy the file from link:data[`data`] to link:cities-service/src/main/resources[`cities-service/src/main/resources`] and edit the file link:cities-service/src/main/resources/application.properties[`application.properties`] and add a line like this, using the appropriate file name: 97 | 98 | [source] 99 | ---- 100 | spring.jpa.properties.hibernate.hbm2ddl.import_files=import-pgsql.sql 101 | ---- 102 | 103 | A path is not necessary, just the file name will suffice. You will need to re-build the .war file with `gradle assemble` after changing `application.properties`. 104 | 105 | === Deploying the web UI 106 | 107 | Once the microservice is deployed and running, you can create a user-provided service with the connection details for the microservice (which will be used by the client library) and then push the web UI app: 108 | [source,bash] 109 | ---- 110 | $ cf create-user-provided-service cities-ws -p '{ "citiesuri": "[route to cities-service]" }' 111 | $ cd cities-ui && cf push && cd .. 112 | ---- 113 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-cities is no longer actively maintained by VMware. 2 | 3 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.1.6.RELEASE' 4 | springCloudVersion = '1.1.1.RELEASE' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | buildscript { 12 | dependencies { 13 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 14 | } 15 | } 16 | } 17 | 18 | subprojects { 19 | apply plugin: 'java' 20 | apply plugin: 'idea' 21 | apply plugin: 'eclipse' 22 | 23 | version = '0.0.1-SNAPSHOT' 24 | 25 | sourceCompatibility = 1.7 26 | targetCompatibility = 1.7 27 | 28 | repositories { 29 | mavenCentral() 30 | } 31 | } 32 | 33 | project(':cities-service') { 34 | apply plugin: 'spring-boot' 35 | 36 | jar { 37 | baseName = 'cities-service' 38 | version = '' 39 | } 40 | 41 | dependencies { 42 | // Spring 43 | compile("org.springframework.boot:spring-boot-starter-web") 44 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 45 | compile("org.springframework.boot:spring-boot-starter-actuator") 46 | compile("org.springframework.boot:spring-boot-starter-data-rest") 47 | compile("org.springframework.cloud:spring-cloud-spring-service-connector:${springCloudVersion}") 48 | compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:${springCloudVersion}") 49 | 50 | // JDBC drivers 51 | runtime("org.hsqldb:hsqldb") 52 | runtime("mysql:mysql-connector-java:5.1.25") 53 | 54 | testCompile("junit:junit") 55 | 56 | versionManagement 'io.spring.platform:platform-versions:1.0.2.RELEASE@properties' 57 | } 58 | } 59 | 60 | project(':cities-client') { 61 | jar { 62 | baseName = 'cities-client' 63 | version = '' 64 | } 65 | 66 | dependencies { 67 | // Spring 68 | compile "org.springframework.hateoas:spring-hateoas:0.16.0.RELEASE" 69 | compile "org.springframework.cloud:spring-cloud-spring-service-connector:${springCloudVersion}" 70 | compile "org.springframework.cloud:spring-cloud-cloudfoundry-connector:${springCloudVersion}" 71 | compile "org.springframework.cloud:spring-cloud-localconfig-connector:${springCloudVersion}" 72 | 73 | // Feign 74 | compile 'com.netflix.feign:feign-core:6.1.3' 75 | compile 'com.netflix.feign:feign-jackson:6.1.3' 76 | } 77 | } 78 | 79 | project(':cities-ui') { 80 | buildscript { 81 | repositories { 82 | jcenter() 83 | } 84 | dependencies { 85 | classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" 86 | classpath 'de.obqo.gradle:gradle-lesscss-plugin:1.0-1.3.3' 87 | } 88 | } 89 | 90 | apply plugin: 'spring-boot' 91 | 92 | jar { 93 | baseName = 'cities-ui' 94 | version = '' 95 | } 96 | 97 | dependencies { 98 | compile project(':cities-client') 99 | 100 | // Spring 101 | compile "org.springframework.boot:spring-boot-starter-web" 102 | compile "org.springframework.boot:spring-boot-starter-actuator" 103 | compile "org.springframework.data:spring-data-commons" 104 | compile("org.springframework.cloud:spring-cloud-spring-service-connector:${springCloudVersion}") 105 | 106 | // Webjars 107 | compile 'org.webjars:bootstrap:3.2.0' 108 | compile 'org.webjars:angularjs:1.2.23' 109 | compile 'org.webjars:angular-ui-bootstrap:0.11.0-2' 110 | compile 'org.webjars:angular-ui-router:0.2.11' 111 | compile 'org.webjars:angular-ui:0.4.0-3' 112 | compile 'org.webjars:angular-ui-bootstrap:0.11.0-2' 113 | compile 'org.webjars:angularjs-google-maps:0.6.0' 114 | compile 'org.webjars:jquery:2.1.1' 115 | 116 | testCompile "org.springframework.boot:spring-boot-starter-test" 117 | 118 | versionManagement 'io.spring.platform:platform-versions:1.0.2.RELEASE@properties' 119 | } 120 | } 121 | 122 | task wrapper(type: Wrapper) { 123 | gradleVersion = '1.12' 124 | } 125 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/CityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client; 2 | 3 | import com.example.cities.client.model.PagedCities; 4 | import feign.RequestLine; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import javax.inject.Named; 8 | 9 | @Repository 10 | public interface CityRepository { 11 | @RequestLine("GET /cities?page={page}&size={size}") 12 | public PagedCities findAll(@Named("page") Integer page, @Named("size") Integer size); 13 | 14 | @RequestLine("GET /cities/search/nameContains?q={name}&page={page}&size={size}") 15 | public PagedCities findByNameContains(@Named("name") String name, @Named("page") Integer page, @Named("size") Integer size); 16 | } 17 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/CityRepositoryFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import feign.Feign; 6 | import feign.jackson.JacksonDecoder; 7 | import org.springframework.hateoas.hal.Jackson2HalModule; 8 | 9 | import java.net.URL; 10 | 11 | public class CityRepositoryFactory { 12 | public CityRepository create(String url) { 13 | ObjectMapper mapper = new ObjectMapper() 14 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 15 | .registerModule(new Jackson2HalModule()); 16 | 17 | return Feign.builder() 18 | .decoder(new JacksonDecoder(mapper)) 19 | .target(CityRepository.class, url); 20 | } 21 | 22 | public CityRepository create(URL url) { 23 | return create(url.toString()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/cloud/WebServiceInfo.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.cloud; 2 | 3 | import org.springframework.cloud.service.UriBasedServiceInfo; 4 | 5 | public class WebServiceInfo extends UriBasedServiceInfo { 6 | public WebServiceInfo(String id, String url) { 7 | super(id, url); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/cloud/cloudfoundry/CitiesWebServiceInfoCreator.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.cloud.cloudfoundry; 2 | 3 | import com.example.cities.client.cloud.WebServiceInfo; 4 | import org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator; 5 | import org.springframework.cloud.cloudfoundry.Tags; 6 | 7 | import java.util.Map; 8 | 9 | public class CitiesWebServiceInfoCreator extends CloudFoundryServiceInfoCreator { 10 | 11 | public static final String CITIES_PREFIX = "cities"; 12 | 13 | public CitiesWebServiceInfoCreator() { 14 | super(new Tags(), CITIES_PREFIX); 15 | } 16 | 17 | @Override 18 | public WebServiceInfo createServiceInfo(Map serviceData) { 19 | String id = (String) serviceData.get("name"); 20 | 21 | Map credentials = getCredentials(serviceData); 22 | String uri = getUriFromCredentials(credentials); 23 | 24 | return new WebServiceInfo(id, uri); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/cloud/connector/CitiesRepositoryConnectionCreator.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.cloud.connector; 2 | 3 | import com.example.cities.client.CityRepositoryFactory; 4 | import com.example.cities.client.cloud.WebServiceInfo; 5 | import com.example.cities.client.CityRepository; 6 | import feign.Feign; 7 | import feign.jackson.JacksonDecoder; 8 | import org.springframework.cloud.service.AbstractServiceConnectorCreator; 9 | import org.springframework.cloud.service.ServiceConnectorConfig; 10 | 11 | public class CitiesRepositoryConnectionCreator extends AbstractServiceConnectorCreator { 12 | @Override 13 | public CityRepository create(WebServiceInfo serviceInfo, ServiceConnectorConfig serviceConnectorConfig) { 14 | return new CityRepositoryFactory().create(serviceInfo.getUri()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/cloud/localconfig/CitiesWebServiceInfoCreator.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.cloud.localconfig; 2 | 3 | import com.example.cities.client.cloud.WebServiceInfo; 4 | import org.springframework.cloud.localconfig.LocalConfigServiceInfoCreator; 5 | import org.springframework.cloud.service.UriBasedServiceData; 6 | 7 | public class CitiesWebServiceInfoCreator extends LocalConfigServiceInfoCreator { 8 | public static final String CITIES_TAG = "cities"; 9 | 10 | public CitiesWebServiceInfoCreator() { 11 | super("http"); 12 | } 13 | 14 | @Override 15 | public boolean accept(UriBasedServiceData serviceData) { 16 | return super.accept(serviceData) && CITIES_TAG.equals(serviceData.getKey()); 17 | } 18 | 19 | @Override 20 | public WebServiceInfo createServiceInfo(String id, String uri) { 21 | return new WebServiceInfo(id, uri); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/model/City.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public class City implements Serializable { 6 | private static final long serialVersionUID = 1L; 7 | 8 | private long id; 9 | 10 | private String name; 11 | 12 | private String county; 13 | 14 | private String stateCode; 15 | 16 | private String postalCode; 17 | 18 | private String latitude; 19 | 20 | private String longitude; 21 | 22 | public City() { 23 | } 24 | 25 | public City(String name, String stateCode, String postalCode) { 26 | this.name = name; 27 | this.stateCode = stateCode; 28 | this.postalCode = postalCode; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public String getPostalCode() { 40 | return postalCode; 41 | } 42 | 43 | public void setPostalCode(String postalCode) { 44 | this.postalCode = postalCode; 45 | } 46 | 47 | public long getId() { 48 | return id; 49 | } 50 | 51 | public void setId(long id) { 52 | this.id = id; 53 | } 54 | 55 | public String getStateCode() { 56 | return stateCode; 57 | } 58 | 59 | public void setStateCode(String stateCode) { 60 | this.stateCode = stateCode; 61 | } 62 | 63 | public String getCounty() { 64 | return county; 65 | } 66 | 67 | public void setCounty(String county) { 68 | this.county = county; 69 | } 70 | 71 | public String getLatitude() { 72 | return latitude; 73 | } 74 | 75 | public void setLatitude(String latitude) { 76 | this.latitude = latitude; 77 | } 78 | 79 | public String getLongitude() { 80 | return longitude; 81 | } 82 | 83 | public void setLongitude(String longitude) { 84 | this.longitude = longitude; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cities-client/src/main/java/com/example/cities/client/model/PagedCities.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.client.model; 2 | 3 | import com.example.cities.client.model.City; 4 | import org.springframework.hateoas.PagedResources; 5 | 6 | public class PagedCities extends PagedResources { 7 | } 8 | -------------------------------------------------------------------------------- /cities-client/src/main/resources/META-INF/services/org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator: -------------------------------------------------------------------------------- 1 | com.example.cities.client.cloud.cloudfoundry.CitiesWebServiceInfoCreator -------------------------------------------------------------------------------- /cities-client/src/main/resources/META-INF/services/org.springframework.cloud.localconfig.LocalConfigServiceInfoCreator: -------------------------------------------------------------------------------- 1 | com.example.cities.client.cloud.localconfig.CitiesWebServiceInfoCreator -------------------------------------------------------------------------------- /cities-client/src/main/resources/META-INF/services/org.springframework.cloud.service.ServiceConnectorCreator: -------------------------------------------------------------------------------- 1 | com.example.cities.client.cloud.connector.CitiesRepositoryConnectionCreator -------------------------------------------------------------------------------- /cities-service/README.adoc: -------------------------------------------------------------------------------- 1 | = Building a Cloud-ready Microservice 2 | 3 | This repository contains a link:demo-script.adoc[script] for demonstrating the construction of a REST microservice using Spring Boot, Spring Data, and Spring Cloud, and deploying the microservice to Cloud Foundry. 4 | 5 | The repository also contains the completed example, ready for deployment to Cloud Foundry. 6 | 7 | To build and deploy the completed application, you will need to: 8 | 9 | * install http://www.gradle.org/installation[Gradle] 10 | * install the http://docs.cloudfoundry.org/devguide/installcf/install-go-cli.html[Cloud Foundry CLI] 11 | * use the Cloud Foundry CLI to http://docs.cloudfoundry.org/devguide/installcf/whats-new-v6.html#login[log into your Cloud Foundry system] 12 | 13 | Once these prerequisites are met, you can follow these steps to build and deploy the microservice to Cloud Foundry: 14 | 15 | [source,bash] 16 | ---- 17 | $ gradle assemble 18 | $ cf create-service p-mysql 100mb-dev cities-db 19 | $ cf push --random-route 20 | ---- 21 | 22 | See the link:demo-script.adoc[script] for additional information on using and testing the microservice. -------------------------------------------------------------------------------- /cities-service/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { url "http://repo.spring.io/milestone" } 5 | } 6 | dependencies { 7 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.5.RELEASE") 8 | classpath("org.cloudfoundry:cf-gradle-plugin:1.0.3") 9 | } 10 | } 11 | 12 | apply plugin: 'idea' 13 | apply plugin: 'java' 14 | apply plugin: 'spring-boot' 15 | apply plugin: 'cloudfoundry' 16 | 17 | repositories { 18 | mavenCentral() 19 | maven { url "http://repo.spring.io/milestone" } 20 | } 21 | 22 | dependencies { 23 | compile("org.springframework.boot:spring-boot-starter-web") 24 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 25 | compile("org.springframework.boot:spring-boot-starter-actuator") 26 | compile("org.springframework.boot:spring-boot-starter-data-rest") 27 | compile("org.springframework.cloud:spring-cloud-spring-service-connector:1.1.0.RELEASE") 28 | compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:1.1.0.RELEASE") 29 | runtime("org.hsqldb:hsqldb") 30 | runtime("mysql:mysql-connector-java:5.1.25") 31 | 32 | testCompile("junit:junit") 33 | } 34 | 35 | project.group = 'com.example.cities.service' 36 | project.version = '0.0.1.SNAPSHOT' 37 | 38 | jar { 39 | baseName = 'cities-service' 40 | version = '' 41 | } 42 | 43 | task wrapper(type: Wrapper) { 44 | gradleVersion = '1.12' 45 | } 46 | 47 | tasks.cfPush.dependsOn assemble 48 | 49 | cloudfoundry { 50 | target = "https://api.cf.mycloud.com" 51 | space = "development" 52 | 53 | memory = 512 54 | 55 | trustSelfSignedCerts = true 56 | 57 | services { 58 | "cities-db" { 59 | label = "p-mysql" 60 | plan = "100mb-dev" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cities-service/demo-script.adoc: -------------------------------------------------------------------------------- 1 | = Building a Cloud-ready Microservice 2 | 3 | This document is intended to be used as a script for demonstrating the construction of a database-backed REST microservice based on http://projects.spring.io/spring-boot/[Spring Boot], http://projects.spring.io/spring-data/[Spring Data], and http://projects.spring.io/spring-cloud/[Spring Cloud], and deployment of the service to http://www.cloudfoundry.org[Cloud Foundry]. It can also be used as a self-guided tutorial. 4 | 5 | == Prerequisites 6 | 7 | * http://www.gradle.org/installation[Gradle] 8 | ** can also be installed via http://brew.sh/[Homebrew] (+brew install gradle+) or http://gvmtool.net/[GVM] (+gvm install gradle+) 9 | * An IDE or text editor 10 | * An account on a http://www.gopivotal.com/platform-as-a-service/pivotal-cf[Pivotal CF] system with a MySQL service available 11 | ** the script can be modified for other Cloud Foundry systems or other database services as required 12 | 13 | == Steps 14 | 15 | === Initializing the Application 16 | 17 | . Browse to http://start.spring.io[Spring Initializr] and complete the form as follows: 18 | + 19 | Group:: org.example 20 | Artifact:: cities 21 | Name:: cities 22 | Description:: Demo project 23 | Package name:: org.example.cities 24 | Styles:: Actuator, JPA, Rest Repositories, Web 25 | Type:: Gradle project 26 | 27 | + 28 | Then click +Generate+. This will result in a file called +starter.zip+ being downloaded to your workstation. 29 | 30 | . Create a directory to house your code, and into that directory unzip +starter.zip+. 31 | 32 | . Optionally generate project files for your favorite IDE by running +gradle idea+ or +gradle eclipse+. Then open the project in your editor or IDE of choice. 33 | 34 | . Add a runtime dependency on the http://hsqldb.org/[HyperSQL in-memory database] to +build.gradle+: 35 | + 36 | [source,groovy] 37 | ---- 38 | dependencies { 39 | // ... 40 | runtime("org.hsqldb:hsqldb") 41 | } 42 | ---- 43 | 44 | . Create the package +org.example.cities.domain+ and in that package create the class +City+. Into that file you can paste the following source code, which represents cities based on postal codes, global coordinates, etc: 45 | + 46 | [source,java] 47 | ---- 48 | @Entity 49 | @Table(name="city") 50 | public class City implements Serializable { 51 | private static final long serialVersionUID = 1L; 52 | 53 | @Id 54 | @GeneratedValue 55 | private long id; 56 | 57 | @Column(nullable = false) 58 | private String name; 59 | 60 | @Column(nullable = false) 61 | private String county; 62 | 63 | @Column(nullable = false) 64 | private String stateCode; 65 | 66 | @Column(nullable = false) 67 | private String postalCode; 68 | 69 | @Column 70 | private String latitude; 71 | 72 | @Column 73 | private String longitude; 74 | 75 | public String getName() { return name; } 76 | 77 | public void setName(String name) { this.name = name; } 78 | 79 | public String getPostalCode() { return postalCode; } 80 | 81 | public void setPostalCode(String postalCode) { this.postalCode = postalCode; } 82 | 83 | public long getId() { return id; } 84 | 85 | public void setId(long id) { this.id = id; } 86 | 87 | public String getStateCode() { return stateCode; } 88 | 89 | public void setStateCode(String stateCode) { this.stateCode = stateCode; } 90 | 91 | public String getCounty() { return county; } 92 | 93 | public void setCounty(String county) { this.county = county; } 94 | 95 | public String getLatitude() { return latitude; } 96 | 97 | public void setLatitude(String latitude) { this.latitude = latitude; } 98 | 99 | public String getLongitude() { return longitude; } 100 | 101 | public void setLongitude(String longitude) { this.longitude = longitude; } 102 | } 103 | ---- 104 | + 105 | Notice that we're using JPA annotations on the class and its fields. You'll need to use your IDE's features to add the appropriate import statements. 106 | 107 | . Create the package +org.example.cities.repositories+ and in that package create the interface +CityRepository+. Paste the following code and add appropriate imports: 108 | + 109 | [source,java] 110 | ---- 111 | @RepositoryRestResource(collectionResourceRel = "cities", path = "cities") 112 | public interface CityRepository extends PagingAndSortingRepository { 113 | } 114 | ---- 115 | 116 | . Add JPA and REST Repository support to the +org.example.cities.Application+ class that was generated by Spring Initializr. 117 | + 118 | [source,java] 119 | ---- 120 | @Configuration 121 | @ComponentScan 122 | @EnableAutoConfiguration 123 | @EnableJpaRepositories // <---- Add this 124 | @Import(RepositoryRestMvcConfiguration.class) // <---- And this 125 | public class Application { 126 | 127 | public static void main(String[] args) { 128 | SpringApplication.run(Application.class, args); 129 | } 130 | } 131 | ---- 132 | 133 | . Build the application: 134 | + 135 | [source,bash] 136 | ---- 137 | $ gradle assemble 138 | ---- 139 | 140 | . Run the application: 141 | + 142 | [source,bash] 143 | ---- 144 | $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar 145 | ---- 146 | 147 | . Access the application using +curl+. You'll see that the primary endpoint automatically exposes the ability to page, size, and sort the response JSON. 148 | + 149 | So what have you done? Created four small classes and one build file, resulting in a fully-functional REST microservice. The application's +DataSource+ is created automatically by Spring Boot using the in-memory database because no other +DataSource+ was detected in the project. 150 | + 151 | [source,bash] 152 | ---- 153 | $ curl -i localhost:8080/cities 154 | HTTP/1.1 200 OK 155 | Server: Apache-Coyote/1.1 156 | X-Application-Context: application 157 | Content-Type: application/hal+json 158 | Transfer-Encoding: chunked 159 | Date: Tue, 27 May 2014 19:34:45 GMT 160 | 161 | { 162 | "_links" : { 163 | "self" : { 164 | "href" : "http://localhost:8080/cities{?page,size,sort}", 165 | "templated" : true 166 | } 167 | }, 168 | "page" : { 169 | "size" : 20, 170 | "totalElements" : 0, 171 | "totalPages" : 0, 172 | "number" : 0 173 | } 174 | } 175 | ---- 176 | + 177 | Next we'll import some data. 178 | 179 | ==== Importing Data 180 | 181 | . Add this link:src/main/resources/import.sql[import.sql file] to +src/main/resources+. This is a rather large dataset containing all of the postal codes in the United States and its territories. This file will automatically be picked up by Hibernate and imported into the in-memory database. 182 | 183 | . Build the application: 184 | + 185 | [source,bash] 186 | ---- 187 | $ gradle assemble 188 | ---- 189 | 190 | . Run the application: 191 | + 192 | [source,bash] 193 | ---- 194 | $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar 195 | ---- 196 | 197 | . Access the application again using +curl+. Notice the appropriate hypermedia is included for +next+, +previous+, and +self+. You can also select pages and page size by utilizing +?size=n&page=n+ on the URL string. Finally, you can sort the data utilizing +?sort=fieldName+. 198 | + 199 | [source,bash] 200 | ---- 201 | $ curl -i localhost:8080/cities 202 | HTTP/1.1 200 OK 203 | Server: Apache-Coyote/1.1 204 | X-Application-Context: application 205 | Content-Type: application/hal+json 206 | Transfer-Encoding: chunked 207 | Date: Tue, 27 May 2014 19:59:58 GMT 208 | 209 | { 210 | "_links" : { 211 | "next" : { 212 | "href" : "http://localhost:8080/cities?page=1&size=20" 213 | }, 214 | "self" : { 215 | "href" : "http://localhost:8080/cities{?page,size,sort}", 216 | "templated" : true 217 | } 218 | }, 219 | "_embedded" : { 220 | "cities" : [ { 221 | "name" : "HOLTSVILLE", 222 | "county" : "SUFFOLK", 223 | "stateCode" : "NY", 224 | "postalCode" : "00501", 225 | "latitude" : "+40.922326", 226 | "longitude" : "-072.637078", 227 | "_links" : { 228 | "self" : { 229 | "href" : "http://localhost:8080/cities/1" 230 | } 231 | } 232 | }, { 233 | "name" : "HOLTSVILLE", 234 | "county" : "SUFFOLK", 235 | "stateCode" : "NY", 236 | "postalCode" : "00544", 237 | "latitude" : "+40.922326", 238 | "longitude" : "-072.637078", 239 | "_links" : { 240 | "self" : { 241 | "href" : "http://localhost:8080/cities/2" 242 | } 243 | } 244 | }, { 245 | "name" : "ADJUNTAS", 246 | "county" : "ADJUNTAS", 247 | "stateCode" : "PR", 248 | "postalCode" : "00601", 249 | "latitude" : "+18.165273", 250 | "longitude" : "-066.722583", 251 | "_links" : { 252 | "self" : { 253 | "href" : "http://localhost:8080/cities/3" 254 | } 255 | } 256 | }, { 257 | "name" : "AGUADA", 258 | "county" : "AGUADA", 259 | "stateCode" : "PR", 260 | "postalCode" : "00602", 261 | "latitude" : "+18.393103", 262 | "longitude" : "-067.180953", 263 | "_links" : { 264 | "self" : { 265 | "href" : "http://localhost:8080/cities/4" 266 | } 267 | } 268 | }, { 269 | "name" : "AGUADILLA", 270 | "county" : "AGUADILLA", 271 | "stateCode" : "PR", 272 | "postalCode" : "00603", 273 | "latitude" : "+18.455913", 274 | "longitude" : "-067.145780", 275 | "_links" : { 276 | "self" : { 277 | "href" : "http://localhost:8080/cities/5" 278 | } 279 | } 280 | }, { 281 | "name" : "AGUADILLA", 282 | "county" : "AGUADILLA", 283 | "stateCode" : "PR", 284 | "postalCode" : "00604", 285 | "latitude" : "+18.493520", 286 | "longitude" : "-067.135883", 287 | "_links" : { 288 | "self" : { 289 | "href" : "http://localhost:8080/cities/6" 290 | } 291 | } 292 | }, { 293 | "name" : "AGUADILLA", 294 | "county" : "AGUADILLA", 295 | "stateCode" : "PR", 296 | "postalCode" : "00605", 297 | "latitude" : "+18.465162", 298 | "longitude" : "-067.141486", 299 | "_links" : { 300 | "self" : { 301 | "href" : "http://localhost:8080/cities/7" 302 | } 303 | } 304 | }, { 305 | "name" : "MARICAO", 306 | "county" : "MARICAO", 307 | "stateCode" : "PR", 308 | "postalCode" : "00606", 309 | "latitude" : "+18.172947", 310 | "longitude" : "-066.944111", 311 | "_links" : { 312 | "self" : { 313 | "href" : "http://localhost:8080/cities/8" 314 | } 315 | } 316 | }, { 317 | "name" : "ANASCO", 318 | "county" : "ANASCO", 319 | "stateCode" : "PR", 320 | "postalCode" : "00610", 321 | "latitude" : "+18.288685", 322 | "longitude" : "-067.139696", 323 | "_links" : { 324 | "self" : { 325 | "href" : "http://localhost:8080/cities/9" 326 | } 327 | } 328 | }, { 329 | "name" : "ANGELES", 330 | "county" : "UTUADO", 331 | "stateCode" : "PR", 332 | "postalCode" : "00611", 333 | "latitude" : "+18.279531", 334 | "longitude" : "-066.802170", 335 | "_links" : { 336 | "self" : { 337 | "href" : "http://localhost:8080/cities/10" 338 | } 339 | } 340 | }, { 341 | "name" : "ARECIBO", 342 | "county" : "ARECIBO", 343 | "stateCode" : "PR", 344 | "postalCode" : "00612", 345 | "latitude" : "+18.450674", 346 | "longitude" : "-066.698262", 347 | "_links" : { 348 | "self" : { 349 | "href" : "http://localhost:8080/cities/11" 350 | } 351 | } 352 | }, { 353 | "name" : "ARECIBO", 354 | "county" : "ARECIBO", 355 | "stateCode" : "PR", 356 | "postalCode" : "00613", 357 | "latitude" : "+18.458093", 358 | "longitude" : "-066.732732", 359 | "_links" : { 360 | "self" : { 361 | "href" : "http://localhost:8080/cities/12" 362 | } 363 | } 364 | }, { 365 | "name" : "ARECIBO", 366 | "county" : "ARECIBO", 367 | "stateCode" : "PR", 368 | "postalCode" : "00614", 369 | "latitude" : "+18.429675", 370 | "longitude" : "-066.674506", 371 | "_links" : { 372 | "self" : { 373 | "href" : "http://localhost:8080/cities/13" 374 | } 375 | } 376 | }, { 377 | "name" : "BAJADERO", 378 | "county" : "ARECIBO", 379 | "stateCode" : "PR", 380 | "postalCode" : "00616", 381 | "latitude" : "+18.444792", 382 | "longitude" : "-066.640678", 383 | "_links" : { 384 | "self" : { 385 | "href" : "http://localhost:8080/cities/14" 386 | } 387 | } 388 | }, { 389 | "name" : "BARCELONETA", 390 | "county" : "BARCELONETA", 391 | "stateCode" : "PR", 392 | "postalCode" : "00617", 393 | "latitude" : "+18.447092", 394 | "longitude" : "-066.544255", 395 | "_links" : { 396 | "self" : { 397 | "href" : "http://localhost:8080/cities/15" 398 | } 399 | } 400 | }, { 401 | "name" : "BOQUERON", 402 | "county" : "CABO ROJO", 403 | "stateCode" : "PR", 404 | "postalCode" : "00622", 405 | "latitude" : "+17.998531", 406 | "longitude" : "-067.187318", 407 | "_links" : { 408 | "self" : { 409 | "href" : "http://localhost:8080/cities/16" 410 | } 411 | } 412 | }, { 413 | "name" : "CABO ROJO", 414 | "county" : "CABO ROJO", 415 | "stateCode" : "PR", 416 | "postalCode" : "00623", 417 | "latitude" : "+18.062201", 418 | "longitude" : "-067.149541", 419 | "_links" : { 420 | "self" : { 421 | "href" : "http://localhost:8080/cities/17" 422 | } 423 | } 424 | }, { 425 | "name" : "PENUELAS", 426 | "county" : "PENUELAS", 427 | "stateCode" : "PR", 428 | "postalCode" : "00624", 429 | "latitude" : "+18.023535", 430 | "longitude" : "-066.726156", 431 | "_links" : { 432 | "self" : { 433 | "href" : "http://localhost:8080/cities/18" 434 | } 435 | } 436 | }, { 437 | "name" : "CAMUY", 438 | "county" : "CAMUY", 439 | "stateCode" : "PR", 440 | "postalCode" : "00627", 441 | "latitude" : "+18.477891", 442 | "longitude" : "-066.854770", 443 | "_links" : { 444 | "self" : { 445 | "href" : "http://localhost:8080/cities/19" 446 | } 447 | } 448 | }, { 449 | "name" : "CASTANER", 450 | "county" : "LARES", 451 | "stateCode" : "PR", 452 | "postalCode" : "00631", 453 | "latitude" : "+18.269187", 454 | "longitude" : "-066.864993", 455 | "_links" : { 456 | "self" : { 457 | "href" : "http://localhost:8080/cities/20" 458 | } 459 | } 460 | } ] 461 | }, 462 | "page" : { 463 | "size" : 20, 464 | "totalElements" : 42741, 465 | "totalPages" : 2138, 466 | "number" : 0 467 | } 468 | } 469 | ---- 470 | 471 | . Try the following +curl+ statements to see how the application behaves: 472 | + 473 | [source,bash] 474 | ---- 475 | $ curl -i "localhost:8080/cities?size=5" 476 | $ curl -i "localhost:8080/cities?size=5&page=3" 477 | $ curl -i "localhost:8080/cities?sort=postalCode,desc" 478 | ---- 479 | + 480 | Next we'll add searching capabilities. 481 | 482 | ==== Adding Search 483 | 484 | . Let's add some additional finder methods to +CityRepository+: 485 | + 486 | [source,java] 487 | ---- 488 | @RestResource(path = "name", rel = "name") 489 | Page findByNameIgnoreCase(@Param("q") String name, Pageable pageable); 490 | 491 | @RestResource(path = "nameContains", rel = "nameContains") 492 | Page findByNameContainsIgnoreCase(@Param("q") String name, Pageable pageable); 493 | 494 | @RestResource(path = "state", rel = "state") 495 | Page findByStateCodeIgnoreCase(@Param("q") String stateCode, Pageable pageable); 496 | 497 | @RestResource(path = "postalCode", rel = "postalCode") 498 | Page findByPostalCode(@Param("q") String postalCode, Pageable pageable); 499 | ---- 500 | 501 | . Build the application: 502 | + 503 | [source,bash] 504 | ---- 505 | $ gradle assemble 506 | ---- 507 | 508 | . Run the application: 509 | + 510 | [source,bash] 511 | ---- 512 | $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar 513 | ---- 514 | 515 | . Access the application again using +curl+. Notice that hypermedia for a new +search+ endpoint has appeared. 516 | + 517 | [source,bash] 518 | ---- 519 | $ curl -i "localhost:8080/cities" 520 | HTTP/1.1 200 OK 521 | Server: Apache-Coyote/1.1 522 | X-Application-Context: application 523 | Content-Type: application/hal+json 524 | Transfer-Encoding: chunked 525 | Date: Tue, 27 May 2014 20:33:52 GMT 526 | 527 | { 528 | "_links" : { 529 | "next" : { 530 | "href" : "http://localhost:8080/cities?page=1&size=20" 531 | }, 532 | "self" : { 533 | "href" : "http://localhost:8080/cities{?page,size,sort}", 534 | "templated" : true 535 | }, 536 | "search" : { 537 | "href" : "http://localhost:8080/cities/search" 538 | } 539 | }, 540 | (Remainder omitted...) 541 | ---- 542 | 543 | . Access the new +search+ endpoint using +curl+: 544 | + 545 | [source,bash] 546 | ---- 547 | $ curl -i "localhost:8080/cities/search" 548 | HTTP/1.1 200 OK 549 | Server: Apache-Coyote/1.1 550 | X-Application-Context: application 551 | Content-Type: application/hal+json 552 | Transfer-Encoding: chunked 553 | Date: Tue, 27 May 2014 20:38:32 GMT 554 | 555 | { 556 | "_links" : { 557 | "postalCode" : { 558 | "href" : "http://localhost:8080/cities/search/postalCode{?q,page,size,sort}", 559 | "templated" : true 560 | }, 561 | "state" : { 562 | "href" : "http://localhost:8080/cities/search/state{?q,page,size,sort}", 563 | "templated" : true 564 | }, 565 | "name" : { 566 | "href" : "http://localhost:8080/cities/search/name{?q,page,size,sort}", 567 | "templated" : true 568 | }, 569 | "nameContains" : { 570 | "href" : "http://localhost:8080/cities/search/nameContains{?q,page,size,sort}", 571 | "templated" : true 572 | } 573 | } 574 | } 575 | ---- 576 | + 577 | Note that we now have new search endpoints for each of the finders that we added. 578 | 579 | . Try a few of these endpoints. Feel free to substitute your own values for the parameters. 580 | + 581 | [source,bash] 582 | ---- 583 | $ curl -i "http://localhost:8080/cities/search/postalCode?q=75202" 584 | $ curl -i "http://localhost:8080/cities/search/name?q=Boston" 585 | $ curl -i "http://localhost:8080/cities/search/nameContains?q=Fort&size=1" 586 | ---- 587 | + 588 | Next let's take a look at a few of the http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#production-ready[``production ready''] endpoints added by Spring Boot Actuator. 589 | 590 | ==== Using Spring Boot Actuator 591 | 592 | Try out the following endpoints. The output is omitted here because it can be quite large: 593 | 594 | http://localhost:8080/beans:: Dumps all of the beans in the Spring context. 595 | http://localhost:8080/autoconfig:: Dumps all of the auto-configuration performed as part of application bootstrapping. 596 | + 597 | Searching for +DataSource+ will show the ++@Conditional++s causing the embedded DB to be created: 598 | + 599 | [source,javascript] 600 | ---- 601 | "DataSourceAutoConfiguration" : [ { 602 | "condition" : "OnClassCondition", 603 | "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType" 604 | } ], 605 | "DataSourceAutoConfiguration.JdbcTemplateConfiguration" : [ { 606 | "condition" : "DataSourceAutoConfiguration.DatabaseCondition", 607 | "message" : "existing auto database detected" 608 | } ], 609 | "DataSourceAutoConfiguration.JdbcTemplateConfiguration#jdbcTemplate" : [ { 610 | "condition" : "OnBeanCondition", 611 | "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.JdbcOperations; SearchStrategy: all) found no beans" 612 | } ], 613 | "DataSourceAutoConfiguration.JdbcTemplateConfiguration#namedParameterJdbcTemplate" : [ { 614 | "condition" : "OnBeanCondition", 615 | "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; SearchStrategy: all) found no beans" 616 | } ], 617 | "DataSourceAutoConfiguration.TomcatConfiguration" : [ { 618 | "condition" : "DataSourceAutoConfiguration.TomcatDatabaseCondition", 619 | "message" : "found database driver org.hsqldb.jdbcDriver" 620 | }, { 621 | "condition" : "OnBeanCondition", 622 | "message" : "@ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) found no beans" 623 | } ], 624 | "DataSourceTransactionManagerAutoConfiguration" : [ { 625 | "condition" : "OnClassCondition", 626 | "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.core.JdbcTemplate,org.springframework.transaction.PlatformTransactionManager" 627 | } ], 628 | "DataSourceTransactionManagerAutoConfiguration.TransactionManagementConfiguration" : [ { 629 | "condition" : "OnBeanCondition", 630 | "message" : "@ConditionalOnMissingBean (types: org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; SearchStrategy: all) found no beans" 631 | } ], 632 | ---- 633 | 634 | http://localhost:8080/env:: Dumps the application's shell environment as well as all Java system properties. 635 | http://localhost:8080/metrics:: Dumps all metrics currently being collected by Actuator, primarily response time and access counts for endpoints. 636 | http://localhost:8080/mappings:: Dumps all URI request mappings and the controller methods to which they are mapped. 637 | 638 | === Pushing to Cloud Foundry 639 | 640 | . Make sure you have used the http://docs.cloudfoundry.org/devguide/installcf/[+cf+ CLI] to http://docs.cloudfoundry.org/devguide/installcf/whats-new-v6.html#login[log into your Cloud Foundry service]. 641 | 642 | . Create an application manifest in +manifest.yml+: 643 | + 644 | [source,yml] 645 | ---- 646 | --- 647 | applications: 648 | - name: cities 649 | memory: 512M 650 | instances: 1 651 | path: build/libs/cities-0.0.1-SNAPSHOT.jar 652 | timeout: 180 # to give time for the data to import 653 | ---- 654 | 655 | . Push to Cloud Foundry with a random route to prevent collisions: 656 | + 657 | [source,bash] 658 | ---- 659 | $ cf push --random-route 660 | 661 | ... 662 | 663 | 1 of 1 instances running 664 | 665 | App started 666 | 667 | Showing health and status for app cities... 668 | OK 669 | 670 | requested state: started 671 | instances: 1/1 672 | usage: 512M x 1 instances 673 | urls: cities-undeliverable-iatrochemistry.cf.mycloud.com 674 | 675 | state since cpu memory disk 676 | #0 running 2014-05-27 04:15:05 PM 0.0% 433M of 512M 128.9M of 1G 677 | ---- 678 | 679 | . Access the application at the route provided by CF: 680 | + 681 | [source,bash] 682 | ---- 683 | $ curl -i cities-undeliverable-iatrochemistry.cf.mycloud.com/cities 684 | ---- 685 | 686 | ==== Using Spring Cloud 687 | 688 | . At present we're still using the in-memory database. Let's connect to a MySQL database service provided by Cloud Foundry. First we'll create the service instance: 689 | + 690 | [source,bash] 691 | ---- 692 | $ cf create-service p-mysql 100mb-dev cities-db 693 | Creating service cities-db... 694 | OK 695 | ---- 696 | 697 | . Next add the service to your application manifest, which will _bind_ the service to our application on the next push. We'll also add an environment variable to switch on the ``cloud'' profile, 698 | + 699 | [source,yml] 700 | ---- 701 | --- 702 | applications: 703 | - name: cities 704 | memory: 512M 705 | instances: 1 706 | path: build/libs/cities-0.0.1-SNAPSHOT.jar 707 | timeout: 180 708 | services: # Add 709 | - cities-db # these 710 | env: # four 711 | SPRING_PROFILES_ACTIVE: cloud # lines 712 | ---- 713 | + 714 | You can also accomplish the service binding by explicitly binding the service at the command-line: 715 | + 716 | [source,bash] 717 | ---- 718 | $ cf bind-service cities cities-db 719 | Binding service cities-db to app cities... 720 | OK 721 | ---- 722 | 723 | . Next we'll add Spring Cloud and MySQL dependencies to our Gradle build. Comment or remove the +hsqldb+ line add add the following in the +dependencies+ section: 724 | + 725 | [source,groovy] 726 | ---- 727 | dependencies { 728 | // .... 729 | compile("org.springframework.cloud:spring-cloud-spring-service-connector:1.0.0.RELEASE") 730 | compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:1.0.0.RELEASE") 731 | runtime("mysql:mysql-connector-java:5.1.25") 732 | } 733 | ---- 734 | + 735 | Since we've added new dependencies, re-run +gradle idea+ or +gradle eclipse+ to have them added to the IDE classpath. 736 | 737 | . Next, let's create the package +org.example.cities.config+ and create in that package the class +CloudDataSourceConfig+. Add the following code: 738 | + 739 | [source,java] 740 | ---- 741 | @Profile("cloud") 742 | @Configuration 743 | public class CloudDataSourceConfig extends AbstractCloudConfig { 744 | @Bean 745 | public DataSource dataSource() { 746 | return connectionFactory().dataSource(); 747 | } 748 | } 749 | ---- 750 | + 751 | As before, have the IDE import the appropriate dependencies. 752 | + 753 | The +@Profile+ annotation will cause this class (which becomes Spring configuration when annotated as +@Configuration+) to be added to the configuration set because of the +SPRING_PROFILES_ACTIVE+ environment variable we added earlier. You can still run the application locally (with the default profile) using the embedded database. 754 | + 755 | With this code, Spring Cloud will detect a bound service that is compatible with +DataSource+, read the credentials, and then create a +DataSource+ as appropriate (it will throw an exception otherwise). 756 | 757 | . Add the following to +src/main/resources/application.properties+ to cause Hibernate to create the database schema and import data at startup. This is done automatically for embedded databases, not for custom ++DataSource++s. Other Hibernate native properties can be set in a similar fashion: 758 | + 759 | [source,java] 760 | ---- 761 | spring.jpa.hibernate.ddl-auto=create 762 | ---- 763 | 764 | . Build the application: 765 | + 766 | [source,bash] 767 | ---- 768 | $ gradle assemble 769 | ---- 770 | 771 | . Re-push the application: 772 | + 773 | [source,bash] 774 | ---- 775 | $ cf push 776 | ---- 777 | 778 | . Take a look at the +env+ endpoint again to see the service bound in +VCAP_SERVICES+: 779 | + 780 | [source,bash] 781 | ---- 782 | $ curl cities-undeliverable-iatrochemistry.cf.mycloud.com/env 783 | ... 784 | "VCAP_SERVICES" : "{\"p-mysql\":[{\"name\":\"cities-db\",\"label\":\"p-mysql\",\"tags\":[\"mysql\",\"relational\"],\"plan\":\"100mb-dev\",\"credentials\":{\"hostname\":\"192. 785 | 168.0.61\",\"port\":3306,\"name\":\"cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\",\"username\":\"qRouPyXXexyXRRxo\",\"password\":\"JsF1GdLT1mN5WMDS\",\"uri\":\"mysql://qRouPyXXexyXRR 786 | xo:JsF1GdLT1mN5WMDS@192.168.0.61:3306/cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4?reconnect=true\",\"jdbcUrl\":\"jdbc:mysql://qRouPyXXexyXRRxo:JsF1GdLT1mN5WMDS@192.168.0.61:3306/cf_8 787 | 4d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\"}}]}", 788 | ... 789 | ---- 790 | The application is now running against a MySQL database. 791 | 792 | ==== Customizing the +DataSource+ 793 | 794 | . You can customize the database connection that Spring Cloud creates with a few lines of code. Change the +dataSource+ method in +CloudDataSourceConfig+ to add some pooling and connection configuration: 795 | + 796 | [source,java] 797 | ---- 798 | @Bean 799 | public DataSource dataSource() { 800 | PooledServiceConnectorConfig.PoolConfig poolConfig = 801 | new PooledServiceConnectorConfig.PoolConfig(20, 200); 802 | 803 | DataSourceConfig.ConnectionConfig connectionConfig = 804 | new DataSourceConfig.ConnectionConfig("characterEncoding=UTF-8"); 805 | DataSourceConfig serviceConfig = new DataSourceConfig(poolConfig, connectionConfig); 806 | 807 | return connectionFactory().dataSource("cities-db", serviceConfig); 808 | } 809 | ---- 810 | 811 | . Build the application: 812 | + 813 | [source,bash] 814 | ---- 815 | $ gradle assemble 816 | ---- 817 | 818 | . Re-push the application: 819 | + 820 | [source,bash] 821 | ---- 822 | $ cf push 823 | ---- 824 | 825 | == Wrapping It Up 826 | 827 | You now have a fully functional REST microservice backed by a MySQL database running on Cloud Foundry, consisting of four Java classes with no boilerplate code or configuration. 828 | 829 | Spring Boot, Spring Data JPA, Spring Data REST, and Spring Cloud provide the framework and scaffolding, allowing you to write only domain-specific code. Production-ready endpoints are provided by Spring Boot to help manage and introspect into the application. 830 | 831 | Cloud Foundry provides a fast, easy, and efficient platform for deploying and scaling the microservice and connecting to a managed database. 832 | -------------------------------------------------------------------------------- /cities-service/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cities 4 | memory: 512M 5 | instances: 1 6 | path: build/libs/cities-service.jar 7 | timeout: 180 8 | services: 9 | - cities-db 10 | env: 11 | SPRING_PROFILES_ACTIVE: cloud 12 | -------------------------------------------------------------------------------- /cities-service/src/main/java/com/example/cities/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.cities; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 9 | import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; 10 | 11 | @Configuration 12 | @ComponentScan 13 | @EnableAutoConfiguration 14 | @EnableJpaRepositories 15 | @Import(RepositoryRestMvcConfiguration.class) 16 | public class Application { 17 | public static void main(String[] args) { 18 | SpringApplication.run(Application.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cities-service/src/main/java/com/example/cities/config/CloudDataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.config; 2 | 3 | import org.springframework.cloud.config.java.AbstractCloudConfig; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | 8 | import javax.sql.DataSource; 9 | 10 | @Configuration 11 | @Profile("cloud") 12 | public class CloudDataSourceConfig extends AbstractCloudConfig { 13 | @Bean 14 | public DataSource dataSource() { 15 | return connectionFactory().dataSource(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cities-service/src/main/java/com/example/cities/domain/City.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.Id; 7 | import javax.persistence.Table; 8 | import java.io.Serializable; 9 | 10 | @Entity 11 | @Table(name="city") 12 | public class City implements Serializable { 13 | private static final long serialVersionUID = 1L; 14 | 15 | @Id 16 | @GeneratedValue 17 | private long id; 18 | 19 | @Column(nullable = false) 20 | private String name; 21 | 22 | @Column(nullable = false) 23 | private String county; 24 | 25 | @Column(nullable = false) 26 | private String stateCode; 27 | 28 | @Column(nullable = false) 29 | private String postalCode; 30 | 31 | @Column 32 | private String latitude; 33 | 34 | @Column 35 | private String longitude; 36 | 37 | public City() { 38 | } 39 | 40 | public City(String name, String stateCode, String postalCode) { 41 | this.name = name; 42 | this.stateCode = stateCode; 43 | this.postalCode = postalCode; 44 | } 45 | 46 | public String getName() { 47 | return name; 48 | } 49 | 50 | public void setName(String name) { 51 | this.name = name; 52 | } 53 | 54 | public String getPostalCode() { 55 | return postalCode; 56 | } 57 | 58 | public void setPostalCode(String postalCode) { 59 | this.postalCode = postalCode; 60 | } 61 | 62 | public long getId() { 63 | return id; 64 | } 65 | 66 | public void setId(long id) { 67 | this.id = id; 68 | } 69 | 70 | public String getStateCode() { 71 | return stateCode; 72 | } 73 | 74 | public void setStateCode(String stateCode) { 75 | this.stateCode = stateCode; 76 | } 77 | 78 | public String getCounty() { 79 | return county; 80 | } 81 | 82 | public void setCounty(String county) { 83 | this.county = county; 84 | } 85 | 86 | public String getLatitude() { 87 | return latitude; 88 | } 89 | 90 | public void setLatitude(String latitude) { 91 | this.latitude = latitude; 92 | } 93 | 94 | public String getLongitude() { 95 | return longitude; 96 | } 97 | 98 | public void setLongitude(String longitude) { 99 | this.longitude = longitude; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cities-service/src/main/java/com/example/cities/repositories/CityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.repositories; 2 | 3 | import com.example.cities.domain.City; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.repository.PagingAndSortingRepository; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 9 | import org.springframework.data.rest.core.annotation.RestResource; 10 | 11 | @RepositoryRestResource(collectionResourceRel = "cities", path = "cities") 12 | public interface CityRepository extends PagingAndSortingRepository { 13 | 14 | Page findAll(Pageable pageable); 15 | 16 | @RestResource(exported = false) 17 | City findById(Long id); 18 | 19 | @RestResource(path = "name", rel = "name") 20 | Page findByNameIgnoreCase(@Param("q") String name, Pageable pageable); 21 | 22 | @RestResource(path = "nameContains", rel = "nameContains") 23 | Page findByNameContainsIgnoreCase(@Param("q") String name, Pageable pageable); 24 | 25 | @RestResource(path = "state", rel = "state") 26 | Page findByStateCodeIgnoreCase(@Param("q") String stateCode, Pageable pageable); 27 | 28 | @RestResource(path = "postalCode", rel = "postalCode") 29 | Page findByPostalCode(@Param("q") String postalCode, Pageable pageable); 30 | } 31 | -------------------------------------------------------------------------------- /cities-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.jpa.hibernate.ddl-auto=create -------------------------------------------------------------------------------- /cities-service/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cities-ui/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cities-ui 4 | memory: 512M 5 | instances: 1 6 | path: build/libs/cities-ui.jar 7 | services: [ cities-ws ] 8 | env: 9 | SPRING_PROFILES_ACTIVE: cloud 10 | -------------------------------------------------------------------------------- /cities-ui/src/main/java/com/example/cities/AppInitializer.java: -------------------------------------------------------------------------------- 1 | package com.example.cities; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.context.web.SpringBootServletInitializer; 5 | 6 | public class AppInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(Application.class); 11 | } 12 | } -------------------------------------------------------------------------------- /cities-ui/src/main/java/com/example/cities/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.cities; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 8 | 9 | @Configuration 10 | @ComponentScan 11 | @EnableAutoConfiguration 12 | @EnableSpringDataWebSupport 13 | public class Application { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /cities-ui/src/main/java/com/example/cities/config/CloudConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.config; 2 | 3 | import com.example.cities.client.CityRepository; 4 | import org.springframework.cloud.config.java.AbstractCloudConfig; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | @Profile({"cloud","local"}) 11 | public class CloudConfiguration extends AbstractCloudConfig { 12 | @Bean 13 | public CityRepository cityRepository() { 14 | return connectionFactory().service(CityRepository.class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cities-ui/src/main/java/com/example/cities/config/DefaultConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.config; 2 | 3 | import com.example.cities.client.CityRepository; 4 | import com.example.cities.client.CityRepositoryFactory; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | @Profile("default") 11 | public class DefaultConfiguration { 12 | @Bean 13 | public CityRepository cityRepository() { 14 | return new CityRepositoryFactory().create("http://localhost:8080/cities"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cities-ui/src/main/java/com/example/cities/controller/CitiesController.java: -------------------------------------------------------------------------------- 1 | package com.example.cities.controller; 2 | 3 | import com.example.cities.client.CityRepository; 4 | import com.example.cities.client.model.PagedCities; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/cities") 14 | public class CitiesController { 15 | private CityRepository repository; 16 | 17 | @Autowired 18 | public CitiesController(CityRepository repository) { 19 | this.repository = repository; 20 | } 21 | 22 | @RequestMapping(method = RequestMethod.GET) 23 | public PagedCities list(Pageable pageable) { 24 | return repository.findAll(pageable.getPageNumber(), pageable.getPageSize()); 25 | } 26 | 27 | @RequestMapping(value = "/search", method = RequestMethod.GET) 28 | public PagedCities search(@RequestParam("name") String name, Pageable pageable) { 29 | return repository.findByNameContains(name, pageable.getPageNumber(), pageable.getPageSize()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/application.properties -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/404-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/404-icon.png -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/homepage-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/homepage-bg.jpg -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/platform-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/platform-bg.png -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/platform-spring-xd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/platform-spring-xd.png -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/spring-logo-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/spring-logo-platform.png -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/images/spring-logo-xd-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/cities-ui/src/main/resources/static/images/spring-logo-xd-mobile.png -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
Loading...
18 |
19 | 20 | 35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/less/header.less: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-top: 4px solid #6db33f; 3 | background-color: #34302d; 4 | margin-bottom: 0px; 5 | border-bottom: 0; 6 | border-left: 0; 7 | border-right: 0; 8 | } 9 | 10 | .navbar a.navbar-brand { 11 | background: url("../images/spring-logo-platform.png") -1px -1px no-repeat; 12 | margin: 12px 0 6px; 13 | width: 229px; 14 | height: 46px; 15 | display: inline-block; 16 | text-decoration: none; 17 | padding: 0; 18 | } 19 | 20 | .navbar a.navbar-brand span { 21 | display: block; 22 | width: 229px; 23 | height: 46px; 24 | background: url("/images/spring-logo-platform.png") -1px -48px no-repeat; 25 | opacity: 0; 26 | -moz-transition: opacity 0.12s ease-in-out; 27 | -webkit-transition: opacity 0.12s ease-in-out; 28 | -o-transition: opacity 0.12s ease-in-out; 29 | } 30 | 31 | .navbar a:hover.navbar-brand span { 32 | opacity: 1; 33 | } 34 | 35 | .navbar li > a, .navbar-text { 36 | font-family: "montserratregular", sans-serif; 37 | text-shadow: none; 38 | font-size: 14px; 39 | 40 | /* line-height: 14px; */ 41 | padding: 28px 20px; 42 | transition: all 0.15s; 43 | -webkit-transition: all 0.15s; 44 | -moz-transition: all 0.15s; 45 | -o-transition: all 0.15s; 46 | -ms-transition: all 0.15s; 47 | } 48 | 49 | .navbar li > a { 50 | text-transform: uppercase; 51 | } 52 | 53 | .navbar .navbar-text { 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | .navbar li:hover > a { 58 | color: #eeeeee; 59 | background-color: #6db33f; 60 | } 61 | 62 | .navbar-toggle { 63 | border-width: 0; 64 | 65 | .icon-bar + .icon-bar { 66 | margin-top: 3px; 67 | } 68 | .icon-bar { 69 | width: 19px; 70 | height: 3px; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/less/main.less: -------------------------------------------------------------------------------- 1 | //@import "../../webjars/bootstrap/3.2.0/less/bootstrap.less"; 2 | 3 | @spring-green: #6db33f; 4 | @spring-dark-green: #5fa134; 5 | @spring-brown: #34302D; 6 | @spring-grey: #838789; 7 | @spring-light-grey: #f1f1f1; 8 | 9 | @body-bg: @spring-light-grey; 10 | @text-color: @spring-grey; 11 | @link-color: @spring-dark-green; 12 | @link-hover-color: @spring-dark-green; 13 | 14 | @navbar-default-link-color: @spring-light-grey; 15 | @navbar-default-link-active-color: @spring-light-grey; 16 | @navbar-default-link-hover-color: @spring-light-grey; 17 | @navbar-default-link-hover-bg: @spring-green; 18 | @navbar-default-toggle-icon-bar-bg: @spring-light-grey; 19 | @navbar-default-toggle-hover-bg: transparent; 20 | @navbar-default-link-active-bg: @spring-green; 21 | 22 | @border-radius-base: 0; 23 | @border-radius-large: 0; 24 | @border-radius-small: 0; 25 | 26 | @btn-default-color: @spring-light-grey; 27 | @btn-default-bg: @spring-brown; 28 | @btn-default-border: @spring-green; 29 | 30 | @icon-font-path: "../fonts/bootstrap/"; 31 | 32 | @nav-tabs-active-link-hover-color: @spring-light-grey; 33 | @nav-tabs-active-link-hover-bg: @spring-brown; 34 | @nav-tabs-active-link-hover-border-color: @spring-brown; 35 | @nav-tabs-border-color: @spring-brown; 36 | @table-border-color: @spring-brown; 37 | 38 | @import "typography"; 39 | @import "header"; 40 | 41 | .nav, .pagination, .carousel, .panel-title a { cursor: pointer; } 42 | 43 | .table > thead > tr > th { 44 | background-color: lighten(@spring-brown, 3%); 45 | color: @spring-light-grey; 46 | } 47 | 48 | .table-filter { 49 | background-color: @spring-brown; 50 | padding: 9px 12px; 51 | } 52 | 53 | .nav > li > a { 54 | color: @spring-grey; 55 | } 56 | 57 | .btn-default { 58 | border-width: 2px; 59 | transition: border 0.15s; 60 | -webkit-transition: border 0.15s; 61 | -moz-transition: border 0.15s; 62 | -o-transition: border 0.15s; 63 | -ms-transition: border 0.15s; 64 | 65 | &:hover, 66 | &:focus, 67 | &:active, 68 | &.active, 69 | .open .dropdown-toggle& { 70 | background-color: @spring-brown; 71 | border-color: @spring-brown; 72 | } 73 | } 74 | 75 | .container .text-muted { 76 | margin: 20px 0; 77 | } 78 | 79 | code { 80 | font-size: 80%; 81 | } 82 | 83 | .xd-container { 84 | margin-top: 40px; 85 | margin-bottom: 100px; 86 | padding-left: 5px; 87 | padding-right: 5px; 88 | } 89 | 90 | h1 { 91 | margin-bottom: 15px 92 | } 93 | 94 | .index-page--subtitle { 95 | font-size: 16px; 96 | line-height: 24px; 97 | margin: 0 0 30px; 98 | } 99 | 100 | .form-horizontal button.btn-inverse { 101 | margin-left: 32px; 102 | } 103 | 104 | [ng-cloak].splash { 105 | display: block !important; 106 | } 107 | [ng-cloak] { 108 | display: none; 109 | } 110 | 111 | .splash { 112 | background: @spring-green; 113 | color: @spring-brown; 114 | display: none; 115 | } 116 | 117 | .error-page { 118 | margin-top: 100px; 119 | text-align: center; 120 | } 121 | 122 | .error-page .error-title { 123 | font-size: 24px; 124 | line-height: 24px; 125 | margin: 30px 0 0; 126 | } 127 | 128 | table td { 129 | vertical-align: middle !important; 130 | } 131 | 132 | table td .progress { 133 | margin-bottom: 0; 134 | } 135 | 136 | table td.action-column { 137 | width: 1px; 138 | } 139 | 140 | .help-block { 141 | color: lighten(@text-color, 0%); // lighten the text some for contrast 142 | } 143 | 144 | /* Google Maps */ 145 | 146 | .angular-google-map-container { 147 | width: 100%; 148 | height: 300px; 149 | } 150 | 151 | /* fix for Twitter Bootstrap handling of responsive images */ 152 | .angular-google-map img { 153 | max-width: inherit; 154 | } 155 | 156 | .angular-google-map { 157 | display: block; 158 | } 159 | 160 | @import "responsive"; 161 | 162 | 163 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/less/responsive.less: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px) { 2 | .navbar-toggle { 3 | position:absolute; 4 | z-index: 9999; 5 | left:0px; 6 | top:0px; 7 | } 8 | 9 | .navbar a.navbar-brand { 10 | display: block; 11 | margin: 0 auto 0 auto; 12 | width: 148px; 13 | height: 50px; 14 | float: none; 15 | background: url("../images/spring-logo-xd-mobile.png") 0 center no-repeat; 16 | } 17 | 18 | .homepage-billboard .homepage-subtitle { 19 | font-size: 21px; 20 | line-height: 21px; 21 | } 22 | 23 | .navbar a.navbar-brand span { 24 | display: none; 25 | } 26 | 27 | .navbar { 28 | border-top-width: 0; 29 | } 30 | 31 | .xd-container { 32 | margin-top: 20px; 33 | margin-bottom: 30px; 34 | } 35 | 36 | .index-page--subtitle { 37 | margin-top: 10px; 38 | margin-bottom: 30px; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/less/typography.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'varela_roundregular'; 3 | src: url('../fonts/varela_round-webfont.eot'); 4 | src: url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/varela_round-webfont.woff') format('woff'), 6 | url('../fonts/varela_round-webfont.ttf') format('truetype'), 7 | url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'montserratregular'; 14 | src: url('../fonts/montserrat-webfont.eot'); 15 | src: url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'), 16 | url('../fonts/montserrat-webfont.woff') format('woff'), 17 | url('../fonts/montserrat-webfont.ttf') format('truetype'), 18 | url('../fonts/montserrat-webfont.svg#montserratregular') format('svg'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | body, h1, h2, h3, p, input { 24 | margin: 0; 25 | font-weight: 400; 26 | font-family: "varela_roundregular", sans-serif; 27 | color: #34302d; 28 | } 29 | 30 | h1 { 31 | font-size: 24px; 32 | line-height: 30px; 33 | font-family: "montserratregular", sans-serif; 34 | } 35 | 36 | h2 { 37 | font-size: 18px; 38 | font-weight: 700; 39 | line-height: 24px; 40 | margin-bottom: 10px; 41 | font-family: "montserratregular", sans-serif; 42 | } 43 | 44 | h3 { 45 | font-size: 16px; 46 | line-height: 24px; 47 | margin-bottom: 10px; 48 | font-weight: 700; 49 | } 50 | 51 | p { 52 | //font-size: 15px; 53 | //line-height: 24px; 54 | } 55 | 56 | strong { 57 | font-weight: 700; 58 | font-family: "montserratregular", sans-serif; 59 | } 60 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('citiesUiApp', [ 'ngResource', 'ui.router' ]) 4 | .constant('appConfiguration', { 5 | citiesApiUrl: window.location.protocol + '//' + window.location.host + '/proxy' 6 | }) 7 | .config(function () { 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/scripts/controllers/cities-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('citiesUiApp', ['ngResource', 'ui.bootstrap']). 4 | factory('PostalCodes', function ($resource) { 5 | return $resource('cities'); 6 | }). 7 | factory('PostalCodesSearch', function ($resource) { 8 | return $resource('cities/search'); 9 | }). 10 | factory('PostalCode', function ($resource) { 11 | return $resource('cities/:id', {id: '@id'}); 12 | }). 13 | controller('CitiesController', function ($scope, PostalCodes, PostalCodesSearch, PostalCode) { 14 | function list() { 15 | PostalCodes.get({page: $scope.pageNumber - 1, size: $scope.itemsPerPage}).$promise.then(function(pagedCities) { 16 | $scope.pagedCities = pagedCities; 17 | }) 18 | } 19 | 20 | function search() { 21 | PostalCodesSearch.get({name: $scope.searchCity.name, page: $scope.pageNumber - 1, size: $scope.itemsPerPage}).$promise.then(function(pagedCities) { 22 | $scope.pagedCities = pagedCities; 23 | }) 24 | } 25 | 26 | $scope.refresh = function() { 27 | $scope.firstItemIndex = ($scope.itemsPerPage * ($scope.pageNumber - 1)) + 1; 28 | $scope.lastItemIndex = Math.min(($scope.firstItemIndex + $scope.itemsPerPage) - 1, $scope.pagedCities.page.totalElements); 29 | 30 | if ($scope.searchCity.name) 31 | search(); 32 | else 33 | list(); 34 | }; 35 | 36 | $scope.search = function() { 37 | search(); 38 | }; 39 | 40 | $scope.clearSearch = function() { 41 | $scope.searchCity.name = ""; 42 | list(); 43 | }; 44 | 45 | $scope.init = function() { 46 | $scope.pageNumber = 1; 47 | $scope.itemsPerPage = 15; 48 | $scope.maxPageLinks = 15; 49 | $scope.itemsPerPageOptions = [5, 10, 15, 25, 50]; 50 | $scope.searchCity = { 51 | name: "" 52 | }; 53 | $scope.pagedCities = { 54 | "contents": [], 55 | "page": { 56 | "number": 0, 57 | "size": 1, 58 | "totalElements": 1, 59 | "totalPages": 1 60 | } 61 | }; 62 | 63 | $scope.refresh(); 64 | }; 65 | }); 66 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/scripts/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('citiesUiApp') 4 | .config(function ($stateProvider, $urlRouterProvider) { 5 | $urlRouterProvider.otherwise('/cities'); 6 | 7 | $stateProvider.state('cities', { 8 | url: '/cities', 9 | controller: 'CitiesController', 10 | templateUrl: 'views/cities.html' 11 | }); 12 | }); -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/styles/header.css: -------------------------------------------------------------------------------- 1 | .navbar{border-top:4px solid #6db33f;background-color:#34302d;margin-bottom:0px;border-bottom:0;border-left:0;border-right:0;} 2 | .navbar a.navbar-brand{background:url("../images/spring-logo-platform.png") -1px -1px no-repeat;margin:12px 0 6px;width:229px;height:46px;display:inline-block;text-decoration:none;padding:0;} 3 | .navbar a.navbar-brand span{display:block;width:229px;height:46px;background:url("/images/spring-logo-platform.png") -1px -48px no-repeat;opacity:0;-moz-transition:opacity 0.12s ease-in-out;-webkit-transition:opacity 0.12s ease-in-out;-o-transition:opacity 0.12s ease-in-out;} 4 | .navbar a:hover.navbar-brand span{opacity:1;} 5 | .navbar li>a,.navbar-text{font-family:"montserratregular",sans-serif;text-shadow:none;font-size:14px;padding:28px 20px;transition:all 0.15s;-webkit-transition:all 0.15s;-moz-transition:all 0.15s;-o-transition:all 0.15s;-ms-transition:all 0.15s;} 6 | .navbar li>a{text-transform:uppercase;} 7 | .navbar .navbar-text{margin-top:0;margin-bottom:0;} 8 | .navbar li:hover>a{color:#eeeeee;background-color:#6db33f;} 9 | .navbar-toggle{border-width:0;}.navbar-toggle .icon-bar+.icon-bar{margin-top:3px;} 10 | .navbar-toggle .icon-bar{width:19px;height:3px;} 11 | 12 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/styles/main.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'varela_roundregular';src:url('../fonts/varela_round-webfont.eot');src:url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/varela_round-webfont.woff') format('woff'),url('../fonts/varela_round-webfont.ttf') format('truetype'),url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg');font-weight:normal;font-style:normal;}@font-face{font-family:'montserratregular';src:url('../fonts/montserrat-webfont.eot');src:url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/montserrat-webfont.woff') format('woff'),url('../fonts/montserrat-webfont.ttf') format('truetype'),url('../fonts/montserrat-webfont.svg#montserratregular') format('svg');font-weight:normal;font-style:normal;}body,h1,h2,h3,p,input{margin:0;font-weight:400;font-family:"varela_roundregular",sans-serif;color:#34302d;} 2 | h1{font-size:24px;line-height:30px;font-family:"montserratregular",sans-serif;} 3 | h2{font-size:18px;font-weight:700;line-height:24px;margin-bottom:10px;font-family:"montserratregular",sans-serif;} 4 | h3{font-size:16px;line-height:24px;margin-bottom:10px;font-weight:700;} 5 | strong{font-weight:700;font-family:"montserratregular",sans-serif;} 6 | .navbar{border-top:4px solid #6db33f;background-color:#34302d;margin-bottom:0px;border-bottom:0;border-left:0;border-right:0;} 7 | .navbar a.navbar-brand{background:url("../images/spring-logo-platform.png") -1px -1px no-repeat;margin:12px 0 6px;width:229px;height:46px;display:inline-block;text-decoration:none;padding:0;} 8 | .navbar a.navbar-brand span{display:block;width:229px;height:46px;background:url("/images/spring-logo-platform.png") -1px -48px no-repeat;opacity:0;-moz-transition:opacity 0.12s ease-in-out;-webkit-transition:opacity 0.12s ease-in-out;-o-transition:opacity 0.12s ease-in-out;} 9 | .navbar a:hover.navbar-brand span{opacity:1;} 10 | .navbar li>a,.navbar-text{font-family:"montserratregular",sans-serif;text-shadow:none;font-size:14px;padding:28px 20px;transition:all 0.15s;-webkit-transition:all 0.15s;-moz-transition:all 0.15s;-o-transition:all 0.15s;-ms-transition:all 0.15s;} 11 | .navbar li>a{text-transform:uppercase;} 12 | .navbar .navbar-text{margin-top:0;margin-bottom:0;} 13 | .navbar li:hover>a{color:#eeeeee;background-color:#6db33f;} 14 | .navbar-toggle{border-width:0;}.navbar-toggle .icon-bar+.icon-bar{margin-top:3px;} 15 | .navbar-toggle .icon-bar{width:19px;height:3px;} 16 | .nav,.pagination,.carousel,.panel-title a{cursor:pointer;} 17 | .table>thead>tr>th{background-color:#3c3834;color:#f1f1f1;} 18 | .table-filter{background-color:#34302d;padding:9px 12px;} 19 | .nav>li>a{color:#838789;} 20 | .btn-default{border-width:2px;transition:border 0.15s;-webkit-transition:border 0.15s;-moz-transition:border 0.15s;-o-transition:border 0.15s;-ms-transition:border 0.15s;}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-color:#34302d;border-color:#34302d;} 21 | .container .text-muted{margin:20px 0;} 22 | code{font-size:80%;} 23 | .xd-container{margin-top:40px;margin-bottom:100px;padding-left:5px;padding-right:5px;} 24 | h1{margin-bottom:15px;} 25 | .index-page--subtitle{font-size:16px;line-height:24px;margin:0 0 30px;} 26 | .form-horizontal button.btn-inverse{margin-left:32px;} 27 | [ng-cloak].splash{display:block !important;} 28 | [ng-cloak]{display:none;} 29 | .splash{background:#6db33f;color:#34302d;display:none;} 30 | .error-page{margin-top:100px;text-align:center;} 31 | .error-page .error-title{font-size:24px;line-height:24px;margin:30px 0 0;} 32 | table td{vertical-align:middle !important;} 33 | table td .progress{margin-bottom:0;} 34 | table td.action-column{width:1px;} 35 | .help-block{color:#838789;} 36 | .angular-google-map-container{width:100%;height:300px;} 37 | .angular-google-map img{max-width:inherit;} 38 | .angular-google-map{display:block;} 39 | @media (max-width:768px){.navbar-toggle{position:absolute;z-index:9999;left:0px;top:0px;} .navbar a.navbar-brand{display:block;margin:0 auto 0 auto;width:148px;height:50px;float:none;background:url("../images/spring-logo-xd-mobile.png") 0 center no-repeat;} .homepage-billboard .homepage-subtitle{font-size:21px;line-height:21px;} .navbar a.navbar-brand span{display:none;} .navbar{border-top-width:0;} .xd-container{margin-top:20px;margin-bottom:30px;} .index-page--subtitle{margin-top:10px;margin-bottom:30px;}} 40 | 41 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/styles/responsive.css: -------------------------------------------------------------------------------- 1 | @media (max-width:768px){.navbar-toggle{position:absolute;z-index:9999;left:0px;top:0px;} .navbar a.navbar-brand{display:block;margin:0 auto 0 auto;width:148px;height:50px;float:none;background:url("../images/spring-logo-xd-mobile.png") 0 center no-repeat;} .homepage-billboard .homepage-subtitle{font-size:21px;line-height:21px;} .navbar a.navbar-brand span{display:none;} .navbar{border-top-width:0;} .xd-container{margin-top:20px;margin-bottom:30px;} .index-page--subtitle{margin-top:10px;margin-bottom:30px;}} 2 | 3 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/styles/typography.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'varela_roundregular';src:url('../fonts/varela_round-webfont.eot');src:url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/varela_round-webfont.woff') format('woff'),url('../fonts/varela_round-webfont.ttf') format('truetype'),url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg');font-weight:normal;font-style:normal;}@font-face{font-family:'montserratregular';src:url('../fonts/montserrat-webfont.eot');src:url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/montserrat-webfont.woff') format('woff'),url('../fonts/montserrat-webfont.ttf') format('truetype'),url('../fonts/montserrat-webfont.svg#montserratregular') format('svg');font-weight:normal;font-style:normal;}body,h1,h2,h3,p,input{margin:0;font-weight:400;font-family:"varela_roundregular",sans-serif;color:#34302d;} 2 | h1{font-size:24px;line-height:30px;font-family:"montserratregular",sans-serif;} 3 | h2{font-size:18px;font-weight:700;line-height:24px;margin-bottom:10px;font-family:"montserratregular",sans-serif;} 4 | h3{font-size:16px;line-height:24px;margin-bottom:10px;font-weight:700;} 5 | strong{font-weight:700;font-family:"montserratregular",sans-serif;} 6 | 7 | -------------------------------------------------------------------------------- /cities-ui/src/main/resources/static/views/cities.html: -------------------------------------------------------------------------------- 1 |
2 |

Postal Codes

3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 | 16 |
17 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 |
Postal CodeCityCountyState CodeMap
{{city.postalCode}}{{city.name}}{{city.county}}{{city.stateCode}} 38 | 39 | 40 | 41 |
45 | 46 |
47 |

Displaying items {{firstItemIndex}} through {{lastItemIndex}} of {{pagedCities.page.totalElements}}

48 |
49 |
50 | 54 |
55 |
56 | 57 | -------------------------------------------------------------------------------- /docs/components.graffle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ActiveLayerIndex 6 | 0 7 | ApplicationVersion 8 | 9 | com.omnigroup.OmniGraffle.MacAppStore 10 | 139.18 11 | 12 | AutoAdjust 13 | 14 | BackgroundGraphic 15 | 16 | Bounds 17 | {{0, 0}, {576.00000953674316, 733}} 18 | Class 19 | SolidGraphic 20 | ID 21 | 2 22 | Style 23 | 24 | shadow 25 | 26 | Draws 27 | NO 28 | 29 | stroke 30 | 31 | Draws 32 | NO 33 | 34 | 35 | 36 | BaseZoom 37 | 0 38 | CanvasOrigin 39 | {0, 0} 40 | ColumnAlign 41 | 1 42 | ColumnSpacing 43 | 36 44 | CreationDate 45 | 2014-09-15 20:08:08 +0000 46 | Creator 47 | Scott Frederick 48 | DisplayScale 49 | 1 0/72 in = 1 0/72 in 50 | GraphDocumentVersion 51 | 8 52 | GraphicsList 53 | 54 | 55 | Bounds 56 | {{215, 201}, {42, 15}} 57 | Class 58 | ShapedGraphic 59 | Head 60 | 61 | ID 62 | 5 63 | 64 | ID 65 | 27 66 | Rotation 67 | 90 68 | Shape 69 | AdjustableArrow 70 | ShapeData 71 | 72 | ratio 73 | 0.32189163565635681 74 | width 75 | 14.973806381225586 76 | 77 | Style 78 | 79 | fill 80 | 81 | Color 82 | 83 | a 84 | 0.1 85 | b 86 | 0 87 | g 88 | 0 89 | r 90 | 0 91 | 92 | MiddleFraction 93 | 0.70634919404983521 94 | 95 | shadow 96 | 97 | Color 98 | 99 | a 100 | 0.4 101 | b 102 | 0 103 | g 104 | 0 105 | r 106 | 0 107 | 108 | Fuzziness 109 | 0.0 110 | ShadowVector 111 | {0, 2} 112 | 113 | stroke 114 | 115 | Color 116 | 117 | a 118 | 0.75 119 | b 120 | 0 121 | g 122 | 0 123 | r 124 | 0 125 | 126 | 127 | 128 | Tail 129 | 130 | ID 131 | 4 132 | 133 | TextRelativeArea 134 | {{0.125, 0.25}, {0.75, 0.5}} 135 | isConnectedShape 136 | 137 | 138 | 139 | Bounds 140 | {{104, 230}, {264, 72}} 141 | Class 142 | ShapedGraphic 143 | ID 144 | 5 145 | Shape 146 | Rectangle 147 | Style 148 | 149 | fill 150 | 151 | Color 152 | 153 | b 154 | 0.996078 155 | g 156 | 0.576471 157 | r 158 | 0.384314 159 | 160 | 161 | 162 | Text 163 | 164 | Text 165 | {\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf210 166 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 167 | {\colortbl;\red255\green255\blue255;} 168 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 169 | 170 | \f0\fs28 \cf0 cities-service 171 | \fs24 \ 172 | 173 | \fs20 Spring Boot + Spring Data + Spring Cloud Connectors} 174 | 175 | 176 | 177 | Bounds 178 | {{104, 139}, {264, 48}} 179 | Class 180 | ShapedGraphic 181 | ID 182 | 4 183 | Shape 184 | Rectangle 185 | Style 186 | 187 | fill 188 | 189 | Color 190 | 191 | b 192 | 0.239216 193 | g 194 | 0.713726 195 | r 196 | 0.447059 197 | 198 | 199 | 200 | Text 201 | 202 | Text 203 | {\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf210 204 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 205 | {\colortbl;\red255\green255\blue255;} 206 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 207 | 208 | \f0\fs28 \cf0 cities-client 209 | \fs24 \ 210 | 211 | \fs20 Spring Cloud Connectors + Feign} 212 | 213 | 214 | 215 | Bounds 216 | {{104, 67}, {264, 72}} 217 | Class 218 | ShapedGraphic 219 | ID 220 | 1 221 | Shape 222 | Rectangle 223 | Style 224 | 225 | fill 226 | 227 | Color 228 | 229 | b 230 | 0.392157 231 | g 232 | 0.819608 233 | r 234 | 0.996078 235 | 236 | 237 | 238 | Text 239 | 240 | Text 241 | {\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf210 242 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 243 | {\colortbl;\red255\green255\blue255;} 244 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 245 | 246 | \f0\fs28 \cf0 cities-ui 247 | \fs24 \ 248 | 249 | \fs20 Angular.js + Spring Boot + Spring Cloud Connectors} 250 | 251 | 252 | 253 | GridInfo 254 | 255 | GuidesLocked 256 | NO 257 | GuidesVisible 258 | YES 259 | HPages 260 | 1 261 | ImageCounter 262 | 1 263 | KeepToScale 264 | 265 | Layers 266 | 267 | 268 | Lock 269 | NO 270 | Name 271 | Layer 1 272 | Print 273 | YES 274 | View 275 | YES 276 | 277 | 278 | LayoutInfo 279 | 280 | Animate 281 | NO 282 | circoMinDist 283 | 18 284 | circoSeparation 285 | 0.0 286 | layoutEngine 287 | dot 288 | neatoSeparation 289 | 0.0 290 | twopiSeparation 291 | 0.0 292 | 293 | LinksVisible 294 | NO 295 | MagnetsVisible 296 | NO 297 | MasterSheets 298 | 299 | ModificationDate 300 | 2014-09-15 21:41:03 +0000 301 | Modifier 302 | Scott Frederick 303 | NotesVisible 304 | NO 305 | Orientation 306 | 2 307 | OriginVisible 308 | NO 309 | PageBreaks 310 | YES 311 | PrintInfo 312 | 313 | NSBottomMargin 314 | 315 | float 316 | 41 317 | 318 | NSHorizonalPagination 319 | 320 | coded 321 | BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG 322 | 323 | NSLeftMargin 324 | 325 | float 326 | 18 327 | 328 | NSPaperSize 329 | 330 | size 331 | {612.00000953674316, 792} 332 | 333 | NSPrintReverseOrientation 334 | 335 | int 336 | 0 337 | 338 | NSRightMargin 339 | 340 | float 341 | 18 342 | 343 | NSTopMargin 344 | 345 | float 346 | 18 347 | 348 | 349 | PrintOnePage 350 | 351 | ReadOnly 352 | NO 353 | RowAlign 354 | 1 355 | RowSpacing 356 | 36 357 | SheetTitle 358 | Canvas 1 359 | SmartAlignmentGuidesActive 360 | YES 361 | SmartDistanceGuidesActive 362 | YES 363 | UniqueID 364 | 1 365 | UseEntirePage 366 | 367 | VPages 368 | 1 369 | WindowInfo 370 | 371 | CurrentSheet 372 | 0 373 | ExpandedCanvases 374 | 375 | 376 | name 377 | Canvas 1 378 | 379 | 380 | Frame 381 | {{2454, 57}, {711, 872}} 382 | ListView 383 | 384 | OutlineWidth 385 | 142 386 | RightSidebar 387 | 388 | ShowRuler 389 | 390 | Sidebar 391 | 392 | SidebarWidth 393 | 120 394 | VisibleRegion 395 | {{0, 0}, {576, 733}} 396 | Zoom 397 | 1 398 | ZoomValues 399 | 400 | 401 | Canvas 1 402 | 1 403 | 1 404 | 405 | 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /docs/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/docs/components.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/spring-boot-cities/4751cc804fc7c9c486fea18d4e7c0a3206149da6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 18 15:54:26 CDT 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-1.12-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include "cities-service", "cities-client", "cities-ui" 2 | --------------------------------------------------------------------------------