├── .gitignore ├── LICENSE ├── README.adoc ├── pom.xml └── src ├── main ├── asciidoc │ └── index.adoc ├── java │ └── com │ │ └── example │ │ └── weather │ │ ├── WeatherApp.java │ │ ├── WeatherAppProperties.java │ │ ├── integration │ │ └── ows │ │ │ ├── Weather.java │ │ │ ├── WeatherEntry.java │ │ │ ├── WeatherForecast.java │ │ │ └── WeatherService.java │ │ └── web │ │ ├── WeatherApiController.java │ │ ├── WeatherSummary.java │ │ └── WeatherSummaryController.java └── resources │ ├── application.properties │ ├── banner.jpg │ ├── static │ ├── css │ │ ├── owfont-regular.css │ │ ├── owfont-regular.min.css │ │ └── style.css │ ├── error │ │ └── 404.html │ ├── favicon.ico │ ├── fonts │ │ ├── SpringOnePlatform.eot │ │ ├── SpringOnePlatform.svg │ │ ├── SpringOnePlatform.ttf │ │ ├── SpringOnePlatform.woff │ │ ├── owfont-regular.eot │ │ ├── owfont-regular.otf │ │ ├── owfont-regular.svg │ │ ├── owfont-regular.ttf │ │ └── owfont-regular.woff │ └── img │ │ ├── background.jpg │ │ └── twitter-logo.png │ └── templates │ └── summary.html └── test ├── java └── com │ └── example │ └── weather │ ├── integration │ └── ows │ │ └── WeatherServiceTest.java │ └── web │ ├── WeatherApiControllerTest.java │ └── WeatherApiDocumentationTest.java └── resources ├── com └── example │ └── weather │ └── integration │ └── ows │ ├── forecast-barcelona.json │ └── weather-barcelona.json └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.ipr 4 | *.iws 5 | 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | 11 | target 12 | 13 | src/main/resources/application-secrets.properties -------------------------------------------------------------------------------- /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 | = Simple weather app 2 | 3 | == Getting started 4 | 5 | To use this app, you need to register an http://openweathermap.org/appid[API Key] on the 6 | http://openweathermap.org/[OpenWeatherApp] service. 7 | 8 | You can create an `application-secrets.properties` in `src/main/resources` and add your 9 | API key there: 10 | 11 | ``` 12 | app.weather.api.key= 13 | ``` 14 | 15 | TIP: The `application-secrets.properties` file is referenced in the `.gitignore` of the 16 | project so you won't commit it by mistake. The `secrets` profile is enabled by default 17 | via the `spring.profiles.active` in the main configuration. 18 | 19 | If you don't like specifying the key in the project directly, you can set an OS 20 | environment property instead, something like: 21 | 22 | ``` 23 | $ EXPORT APP_WEATHER_API_KEY= 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | weather-app 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | Weather App 12 | Sample application for the SpringOne Platform Keynote 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.0.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-mustache 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-cache 42 | 43 | 44 | 45 | com.fasterxml.jackson.datatype 46 | jackson-datatype-jsr310 47 | 48 | 49 | com.github.ben-manes.caffeine 50 | caffeine 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-devtools 56 | true 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-configuration-processor 61 | true 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.springframework.restdocs 71 | spring-restdocs-mockmvc 72 | test 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-maven-plugin 81 | 82 | 83 | 84 | build-info 85 | 86 | 87 | 88 | ${project.parent.version} 89 | 90 | 91 | 92 | 93 | 94 | 95 | pl.project13.maven 96 | git-commit-id-plugin 97 | 98 | 99 | org.asciidoctor 100 | asciidoctor-maven-plugin 101 | 1.5.3 102 | 103 | 104 | generate-docs 105 | prepare-package 106 | 107 | process-asciidoc 108 | 109 | 110 | html 111 | book 112 | 113 | ${project.build.directory}/generated-snippets 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = Weather API 2 | Andy Wilkinson; 3 | :doctype: book 4 | :icons: font 5 | :source-highlighter: highlightjs 6 | :toc: left 7 | :toclevels: 4 8 | :sectlinks: 9 | 10 | 11 | 12 | [[introduction]] 13 | = Introduction 14 | 15 | The Weather API provides access to current weather data and a weather forecast. 16 | 17 | 18 | 19 | [[current-weather]] 20 | == Current weather 21 | 22 | Current weather is available from `api/weather/now/{country}/{city}`. 23 | 24 | 25 | 26 | [[current-weather-request]] 27 | === Request 28 | Two path parameters are used to indicate the location for which weather data is required: 29 | 30 | include::{snippets}/weather/path-parameters.adoc[] 31 | 32 | For example, a curl request for the current weather in Barcelona, Spain looks like this: 33 | 34 | include::{snippets}/weather/curl-request.adoc[] 35 | 36 | 37 | 38 | [[current-weather-response]] 39 | === Response 40 | 41 | The following fields are found in the response: 42 | 43 | include::{snippets}/weather/response-fields.adoc[] 44 | 45 | A typical response looks like this: 46 | 47 | include::{snippets}/weather/http-response.adoc[] 48 | 49 | 50 | 51 | [[weather-forecast]] 52 | == Weather forecast 53 | 54 | A weather forecast is available from `api/weather/forecast/{country}/{city}`. 55 | 56 | 57 | 58 | [[weather-forecast-request]] 59 | === Request 60 | Two path parameters are used to indicate the location for which a weather forecast is 61 | required: 62 | 63 | include::{snippets}/weather-forecast/path-parameters.adoc[] 64 | 65 | For example, a curl request for the weather forecast for Barcelona, Spain looks like this: 66 | 67 | include::{snippets}/weather-forecast/curl-request.adoc[] 68 | 69 | 70 | 71 | [[weather-forecast-response]] 72 | === Response 73 | 74 | The following fields are found in the response: 75 | 76 | include::{snippets}/weather-forecast/response-fields.adoc[] 77 | 78 | A typical response looks like this: 79 | 80 | include::{snippets}/weather-forecast/http-response.adoc[] -------------------------------------------------------------------------------- /src/main/java/com/example/weather/WeatherApp.java: -------------------------------------------------------------------------------- 1 | package com.example.weather; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.cache.annotation.EnableCaching; 7 | 8 | @SpringBootApplication 9 | @EnableConfigurationProperties(WeatherAppProperties.class) 10 | @EnableCaching(proxyTargetClass = true) 11 | public class WeatherApp { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(WeatherApp.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/WeatherAppProperties.java: -------------------------------------------------------------------------------- 1 | package com.example.weather; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import javax.validation.Valid; 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.springframework.boot.context.properties.ConfigurationProperties; 9 | 10 | @ConfigurationProperties("app.weather") 11 | public class WeatherAppProperties { 12 | 13 | @Valid 14 | private final Api api = new Api(); 15 | 16 | /** 17 | * Comma-separated list of locations to display. Each entry should have the 18 | * form "Country/City". 19 | */ 20 | private List locations = Arrays.asList("UK/London", "Russia/Moscow"); 21 | 22 | public Api getApi() { 23 | return this.api; 24 | } 25 | 26 | public List getLocations() { 27 | return this.locations; 28 | } 29 | 30 | public void setLocations(List locations) { 31 | this.locations = locations; 32 | } 33 | 34 | public static class Api { 35 | 36 | /** 37 | * API key of the OpenWeatherMap service. 38 | */ 39 | @NotNull 40 | private String key; 41 | 42 | public String getKey() { 43 | return this.key; 44 | } 45 | 46 | public void setKey(String key) { 47 | this.key = key; 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/integration/ows/Weather.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.integration.ows; 2 | 3 | public class Weather extends WeatherEntry { 4 | 5 | private String name; 6 | 7 | public String getName() { 8 | return this.name; 9 | } 10 | 11 | public void setName(String name) { 12 | this.name = name; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/integration/ows/WeatherEntry.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.integration.ows; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonSetter; 10 | 11 | public class WeatherEntry implements Serializable { 12 | 13 | private Instant timestamp; 14 | 15 | private double temperature; 16 | 17 | private Integer weatherId; 18 | 19 | private String weatherIcon; 20 | 21 | @JsonProperty("timestamp") 22 | public Instant getTimestamp() { 23 | return this.timestamp; 24 | } 25 | 26 | @JsonSetter("dt") 27 | public void setTimestamp(long unixTime) { 28 | this.timestamp = Instant.ofEpochMilli(unixTime * 1000); 29 | } 30 | 31 | /** 32 | * Return the temperature in Kelvin (K). 33 | */ 34 | public double getTemperature() { 35 | return this.temperature; 36 | } 37 | 38 | public void setTemperature(double temperature) { 39 | this.temperature = temperature; 40 | } 41 | 42 | @JsonProperty("main") 43 | public void setMain(Map main) { 44 | setTemperature(Double.parseDouble(main.get("temp").toString())); 45 | } 46 | 47 | public Integer getWeatherId() { 48 | return this.weatherId; 49 | } 50 | 51 | public void setWeatherId(Integer weatherId) { 52 | this.weatherId = weatherId; 53 | } 54 | 55 | public String getWeatherIcon() { 56 | return this.weatherIcon; 57 | } 58 | 59 | public void setWeatherIcon(String weatherIcon) { 60 | this.weatherIcon = weatherIcon; 61 | } 62 | 63 | @JsonProperty("weather") 64 | public void setWeather(List> weatherEntries) { 65 | Map weather = weatherEntries.get(0); 66 | setWeatherId((Integer) weather.get("id")); 67 | setWeatherIcon((String) weather.get("icon")); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/integration/ows/WeatherForecast.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.integration.ows; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonSetter; 10 | 11 | public class WeatherForecast implements Serializable { 12 | 13 | private String name; 14 | 15 | private List entries = new ArrayList<>(); 16 | 17 | public String getName() { 18 | return this.name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | @JsonProperty("entries") 26 | public List getEntries() { 27 | return this.entries; 28 | } 29 | 30 | @JsonSetter("list") 31 | public void setEntries(List entries) { 32 | this.entries = entries; 33 | } 34 | 35 | @JsonProperty("city") 36 | public void setCity(Map city) { 37 | setName(city.get("name").toString()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/integration/ows/WeatherService.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.integration.ows; 2 | 3 | import java.net.URI; 4 | 5 | import com.example.weather.WeatherAppProperties; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import org.springframework.boot.web.client.RestTemplateBuilder; 10 | import org.springframework.cache.annotation.Cacheable; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.http.RequestEntity; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.web.client.RestTemplate; 16 | import org.springframework.web.util.UriTemplate; 17 | 18 | @Service 19 | public class WeatherService { 20 | 21 | private static final String WEATHER_URL = 22 | "http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={key}"; 23 | 24 | private static final String FORECAST_URL = 25 | "http://api.openweathermap.org/data/2.5/forecast?q={city},{country}&APPID={key}"; 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(WeatherService.class); 28 | 29 | private final RestTemplate restTemplate; 30 | 31 | private final String apiKey; 32 | 33 | public WeatherService(RestTemplateBuilder restTemplateBuilder, 34 | WeatherAppProperties properties) { 35 | this.restTemplate = restTemplateBuilder.build(); 36 | this.apiKey = properties.getApi().getKey(); 37 | } 38 | 39 | @Cacheable("weather") 40 | public Weather getWeather(String country, String city) { 41 | logger.info("Requesting current weather for {}/{}", country, city); 42 | URI url = new UriTemplate(WEATHER_URL).expand(city, country, this.apiKey); 43 | return invoke(url, Weather.class); 44 | } 45 | 46 | @Cacheable("forecast") 47 | public WeatherForecast getWeatherForecast(String country, String city) { 48 | logger.info("Requesting weather forecast for {}/{}", country, city); 49 | URI url = new UriTemplate(FORECAST_URL).expand(city, country, this.apiKey); 50 | return invoke(url, WeatherForecast.class); 51 | } 52 | 53 | private T invoke(URI url, Class responseType) { 54 | RequestEntity request = RequestEntity.get(url) 55 | .accept(MediaType.APPLICATION_JSON).build(); 56 | ResponseEntity exchange = this.restTemplate 57 | .exchange(request, responseType); 58 | return exchange.getBody(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/web/WeatherApiController.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.web; 2 | 3 | import com.example.weather.integration.ows.Weather; 4 | import com.example.weather.integration.ows.WeatherForecast; 5 | import com.example.weather.integration.ows.WeatherService; 6 | 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/api/weather") 13 | public class WeatherApiController { 14 | 15 | private final WeatherService weatherService; 16 | 17 | public WeatherApiController(WeatherService weatherService) { 18 | this.weatherService = weatherService; 19 | } 20 | 21 | @RequestMapping("/now/{country}/{city}") 22 | public Weather getWeather(@PathVariable String country, 23 | @PathVariable String city) { 24 | return this.weatherService.getWeather(country, city); 25 | } 26 | 27 | @RequestMapping("/weekly/{country}/{city}") 28 | public WeatherForecast getWeatherForecast(@PathVariable String country, 29 | @PathVariable String city) { 30 | return this.weatherService.getWeatherForecast(country, city); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/web/WeatherSummary.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.web; 2 | 3 | import com.example.weather.integration.ows.Weather; 4 | 5 | class WeatherSummary { 6 | 7 | private final String country; 8 | 9 | private final String city; 10 | 11 | private final Integer code; 12 | 13 | private final String icon; 14 | 15 | private final double temperature; 16 | 17 | WeatherSummary(String country, String city, Weather weather) { 18 | this.country = country; 19 | this.city = city; 20 | this.code = weather.getWeatherId(); 21 | this.icon = weather.getWeatherIcon(); 22 | this.temperature = weather.getTemperature(); 23 | } 24 | 25 | public String getCountry() { 26 | return this.country; 27 | } 28 | 29 | public String getCity() { 30 | return this.city; 31 | } 32 | 33 | public Integer getCode() { 34 | return this.code; 35 | } 36 | 37 | public String getIcon() { 38 | return this.icon; 39 | } 40 | 41 | public String getFahrenheitTemperature() { 42 | double fahrenheitTemp = (this.temperature * 1.8) - 459.67; 43 | return String.format("%4.2f", fahrenheitTemp); 44 | } 45 | 46 | public String getCelsiusTemperature() { 47 | double celsiusTemp = this.temperature - 273.15; 48 | return String.format("%4.2f", celsiusTemp); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/example/weather/web/WeatherSummaryController.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.web; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import com.example.weather.WeatherAppProperties; 9 | import com.example.weather.integration.ows.Weather; 10 | import com.example.weather.integration.ows.WeatherService; 11 | 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.servlet.ModelAndView; 16 | 17 | @Controller 18 | @RequestMapping("/") 19 | public class WeatherSummaryController { 20 | 21 | private final WeatherService weatherService; 22 | 23 | private final WeatherAppProperties properties; 24 | 25 | public WeatherSummaryController(WeatherService weatherService, WeatherAppProperties properties) { 26 | this.weatherService = weatherService; 27 | this.properties = properties; 28 | } 29 | 30 | @RequestMapping(method = RequestMethod.GET) 31 | public ModelAndView conferenceWeather() { 32 | Map model = new LinkedHashMap<>(); 33 | model.put("summary", getSummary()); 34 | return new ModelAndView("summary", model); 35 | } 36 | 37 | private Object getSummary() { 38 | List summary = new ArrayList<>(); 39 | for (String location : this.properties.getLocations()) { 40 | String country = location.split("/")[0]; 41 | String city = location.split("/")[1]; 42 | Weather weather = this.weatherService.getWeather(country, city); 43 | summary.add(createWeatherSummary(country, city, weather)); 44 | } 45 | return summary; 46 | } 47 | 48 | 49 | 50 | private WeatherSummary createWeatherSummary(String country, String city, 51 | Weather weather) { 52 | // cough cough 53 | if ("Las Vegas".equals(city)) { 54 | weather.setWeatherId(666); 55 | } 56 | return new WeatherSummary(country, city, weather); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=secrets 2 | 3 | spring.jackson.serialization.write_dates_as_timestamps=false 4 | 5 | app.weather.locations=Belgium/Brussels,USA/Las Vegas 6 | 7 | spring.cache.caffeine.spec=recordStats,maximumSize=500,expireAfterWrite=600s -------------------------------------------------------------------------------- /src/main/resources/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/banner.jpg -------------------------------------------------------------------------------- /src/main/resources/static/css/owfont-regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * owfont-regular 1.0.0 by Deniz Fuchidzhiev - http://websygen.com 3 | * License - font: SIL OFL 1.1, css: MIT License 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'owfont'; 9 | src: url('../fonts/owfont-regular.eot?v=1.0.0'); 10 | src: url('../fonts/owfont-regular.eot?#iefix&v=1.0.0') format('embedded-opentype'), 11 | url('../fonts/owfont-regular.woff') format('woff'), 12 | url('../fonts/owfont-regular.ttf') format('truetype'), 13 | url('../fonts/owfont-regular.svg#owf-regular') format('svg'); 14 | font-weight: normal; 15 | font-style: normal; 16 | } 17 | .owf { 18 | display: inline-block; 19 | font: normal normal normal 14px/1 owfont; 20 | font-size: inherit; 21 | text-rendering: auto; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | transform: translate(0, 0); 25 | } 26 | /* makes the font 33% larger relative to the icon container */ 27 | .owf-lg { 28 | font-size: 1.33333333em; 29 | line-height: 0.75em; 30 | vertical-align: -15%; 31 | } 32 | .owf-2x { 33 | font-size: 2em; 34 | } 35 | .owf-3x { 36 | font-size: 3em; 37 | } 38 | .owf-4x { 39 | font-size: 4em; 40 | } 41 | .owf-5x { 42 | font-size: 5em; 43 | } 44 | .owf-fw { 45 | width: 1.28571429em; 46 | text-align: center; 47 | } 48 | .owf-ul { 49 | padding-left: 0; 50 | margin-left: 2.14285714em; 51 | list-style-type: none; 52 | } 53 | .owf-ul > li { 54 | position: relative; 55 | } 56 | .owf-li { 57 | position: absolute; 58 | left: -2.14285714em; 59 | width: 2.14285714em; 60 | top: 0.14285714em; 61 | text-align: center; 62 | } 63 | .owf-li.owf-lg { 64 | left: -1.85714286em; 65 | } 66 | .owf-border { 67 | padding: .2em .25em .15em; 68 | border: solid 0.08em #eeeeee; 69 | border-radius: .1em; 70 | } 71 | .owf-pull-right { 72 | float: right; 73 | } 74 | .owf-pull-left { 75 | float: left; 76 | } 77 | .owf.owf-pull-left { 78 | margin-right: .3em; 79 | } 80 | .owf.owf-pull-right { 81 | margin-left: .3em; 82 | } 83 | 84 | /* owfont uses the Unicode Private Use Area (PUA) to ensure screen 85 | readers do not read off random characters that represent icons */ 86 | 87 | /* Weather Condition Codes */ 88 | 89 | /* Thunderstorm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 90 | 91 | /* thunderstorm with light rain */ 92 | .owf-200:before, 93 | .owf-200-d:before, 94 | .owf-200-n:before { 95 | content: "\EB28"; 96 | } 97 | /* thunderstorm with rain */ 98 | .owf-201:before, 99 | .owf-201-d:before, 100 | .owf-201-n:before { 101 | content: "\EB29"; 102 | } 103 | /* thunderstorm with heavy rain */ 104 | .owf-202:before, 105 | .owf-202-d:before, 106 | .owf-202-n:before { 107 | content: "\EB2A"; 108 | } 109 | /* light thunderstorm */ 110 | .owf-210:before, 111 | .owf-210-d:before, 112 | .owf-210-n:before { 113 | content: "\EB32"; 114 | } 115 | /* thunderstorm */ 116 | .owf-211:before, 117 | .owf-211-d:before, 118 | .owf-211-n:before { 119 | content: "\EB33"; 120 | } 121 | /* heavy thunderstorm */ 122 | .owf-212:before, 123 | .owf-212-d:before, 124 | .owf-212-n:before { 125 | content: "\EB34"; 126 | } 127 | /* ragged thunderstorm */ 128 | .owf-221:before, 129 | .owf-221-d:before, 130 | .owf-221-n:before { 131 | content: "\EB3D"; 132 | } 133 | /* thunderstorm with light drizzle */ 134 | .owf-230:before, 135 | .owf-230-d:before, 136 | .owf-230-n:before { 137 | content: "\EB46"; 138 | } 139 | /* thunderstorm with drizzle */ 140 | .owf-231:before, 141 | .owf-231-d:before, 142 | .owf-231-n:before { 143 | content: "\EB47"; 144 | } 145 | /* thunderstorm with heavy drizzle */ 146 | .owf-232:before, 147 | .owf-232-d:before, 148 | .owf-232-n:before { 149 | content: "\EB48"; 150 | } 151 | 152 | /* Drizzle - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 153 | 154 | /* light intensity drizzle */ 155 | .owf-300:before, 156 | .owf-300-d:before, 157 | .owf-300-n:before { 158 | content: "\EB8C"; 159 | } 160 | /* drizzle */ 161 | .owf-301:before, 162 | .owf-301-d:before, 163 | .owf-301-n:before { 164 | content: "\EB8D"; 165 | } 166 | /* heavy intensity drizzle */ 167 | .owf-302:before, 168 | .owf-302-d:before, 169 | .owf-302-n:before { 170 | content: "\EB8E"; 171 | } 172 | /* light intensity drizzle rain */ 173 | .owf-310:before, 174 | .owf-310-d:before, 175 | .owf-310-n:before { 176 | content: "\EB96"; 177 | } 178 | /* drizzle rain */ 179 | .owf-311:before, 180 | .owf-311-d:before, 181 | .owf-311-n:before { 182 | content: "\EB97"; 183 | } 184 | /* heavy intensity drizzle rain */ 185 | .owf-312:before, 186 | .owf-312-d:before, 187 | .owf-312-n:before { 188 | content: "\EB98"; 189 | } 190 | /* shower rain and drizzle */ 191 | .owf-313:before, 192 | .owf-313-d:before, 193 | .owf-313-n:before { 194 | content: "\EB99"; 195 | } 196 | /* heavy shower rain and drizzle*/ 197 | .owf-314:before, 198 | .owf-314-d:before, 199 | .owf-314-n:before { 200 | content: "\EB9A"; 201 | } 202 | /* shower drizzle */ 203 | .owf-321:before, 204 | .owf-321-d:before, 205 | .owf-321-n:before { 206 | content: "\EBA1"; 207 | } 208 | 209 | /* Rain - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 210 | 211 | /* light rain */ 212 | .owf-500:before, 213 | .owf-500-d:before, 214 | .owf-500-n:before { 215 | content: "\EC54"; 216 | } 217 | /* moderate rain */ 218 | .owf-501:before, 219 | .owf-501-d:before, 220 | .owf-501-n:before { 221 | content: "\EC55"; 222 | } 223 | /* heavy intensity rain */ 224 | .owf-502:before, 225 | .owf-502-d:before, 226 | .owf-502-n:before { 227 | content: "\EC56"; 228 | } 229 | /* very heavy rain */ 230 | .owf-503:before, 231 | .owf-503-d:before, 232 | .owf-503-n:before { 233 | content: "\EC57"; 234 | } 235 | /* extreme rain */ 236 | .owf-504:before, 237 | .owf-504-d:before, 238 | .owf-504-n:before { 239 | content: "\EC58"; 240 | } 241 | /* freezing rain */ 242 | .owf-511:before, 243 | .owf-511-d:before, 244 | .owf-511-n:before { 245 | content: "\EC5F"; 246 | } 247 | /* light intensity shower rain */ 248 | .owf-520:before, 249 | .owf-520-d:before, 250 | .owf-520-n:before { 251 | content: "\EC68"; 252 | } 253 | /* shower rain */ 254 | .owf-521:before, 255 | .owf-521-d:before, 256 | .owf-521-n:before { 257 | content: "\EC69"; 258 | } 259 | /* heavy intensity shower rain */ 260 | .owf-522:before, 261 | .owf-522-d:before, 262 | .owf-522-n:before { 263 | content: "\EC6A"; 264 | } 265 | /* ragged shower rain */ 266 | .owf-531:before, 267 | .owf-531-d:before, 268 | .owf-531-n:before { 269 | content: "\EC73"; 270 | } 271 | 272 | /* Snow - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 273 | 274 | /* light snow */ 275 | .owf-600:before, 276 | .owf-600-d:before, 277 | .owf-600-n:before { 278 | content: "\ECB8"; 279 | } 280 | /* snow */ 281 | .owf-601:before, 282 | .owf-601-d:before, 283 | .owf-601-n:before { 284 | content: "\ECB9"; 285 | } 286 | /* heavy snow */ 287 | .owf-602:before, 288 | .owf-602-d:before, 289 | .owf-602-n:before { 290 | content: "\ECBA"; 291 | } 292 | /* sleet */ 293 | .owf-611:before, 294 | .owf-611-d:before, 295 | .owf-611-n:before { 296 | content: "\ECC3"; 297 | } 298 | /* shower sleet */ 299 | .owf-612:before, 300 | .owf-612-d:before, 301 | .owf-612-n:before { 302 | content: "\ECC4"; 303 | } 304 | /* light rain and snow */ 305 | .owf-615:before, 306 | .owf-615-d:before, 307 | .owf-615-n:before { 308 | content: "\ECC7"; 309 | } 310 | /* rain and snow */ 311 | .owf-616:before, 312 | .owf-616-d:before, 313 | .owf-616-n:before { 314 | content: "\ECC8"; 315 | } 316 | /* light shower snow */ 317 | .owf-620:before, 318 | .owf-620-d:before, 319 | .owf-620-n:before { 320 | content: "\ECCC"; 321 | } 322 | /* shower snow */ 323 | .owf-621:before, 324 | .owf-621-d:before, 325 | .owf-621-n:before { 326 | content: "\ECCD"; 327 | } 328 | /* heavy shower snow */ 329 | .owf-622:before, 330 | .owf-622-d:before, 331 | .owf-622-n:before { 332 | content: "\ECCE"; 333 | } 334 | 335 | /* Atmosphere - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 336 | 337 | /* mist */ 338 | .owf-701:before, 339 | .owf-701-d:before, 340 | .owf-701-n:before { 341 | content: "\ED1D"; 342 | } 343 | /* smoke */ 344 | .owf-711:before, 345 | .owf-711-d:before, 346 | .owf-711-n:before { 347 | content: "\ED27"; 348 | } 349 | /* haze */ 350 | .owf-721:before, 351 | .owf-721-d:before, 352 | .owf-721-n:before { 353 | content: "\ED31"; 354 | } 355 | /* Sand/Dust Whirls */ 356 | .owf-731:before, 357 | .owf-731-d:before, 358 | .owf-731-n:before { 359 | content: "\ED3B"; 360 | } 361 | /* Fog */ 362 | .owf-741:before, 363 | .owf-741-d:before, 364 | .owf-741-n:before { 365 | content: "\ED45"; 366 | } 367 | /* sand */ 368 | .owf-751:before, 369 | .owf-751-d:before, 370 | .owf-751-n:before { 371 | content: "\ED4F"; 372 | } 373 | /* dust */ 374 | .owf-761:before, 375 | .owf-761-d:before, 376 | .owf-761-n:before { 377 | content: "\ED59"; 378 | } 379 | /* VOLCANIC ASH */ 380 | .owf-762:before, 381 | .owf-762-d:before, 382 | .owf-762-n:before { 383 | content: "\ED5A"; 384 | } 385 | /* SQUALLS */ 386 | .owf-771:before, 387 | .owf-771-d:before, 388 | .owf-771-n:before { 389 | content: "\ED63"; 390 | } 391 | /* TORNADO */ 392 | .owf-781:before, 393 | .owf-781-d:before, 394 | .owf-781-n:before { 395 | content: "\ED6D"; 396 | } 397 | 398 | /* Clouds - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 399 | 400 | /* sky is clear */ /* Calm */ 401 | .owf-800:before, 402 | .owf-800-d:before, 403 | .owf-951:before, 404 | .owf-951-d:before { 405 | content: "\ED80"; 406 | } 407 | .owf-800-n:before, 408 | .owf-951-n:before { 409 | content: "\F168"; 410 | } 411 | /* few clouds */ 412 | .owf-801:before, 413 | .owf-801-d:before { 414 | content: "\ED81"; 415 | } 416 | .owf-801-n:before { 417 | content: "\F169"; 418 | } 419 | /* scattered clouds */ 420 | .owf-802:before, 421 | .owf-802-d:before { 422 | content: "\ED82"; 423 | } 424 | .owf-802-n:before { 425 | content: "\F16A"; 426 | } 427 | /* broken clouds */ 428 | .owf-803:before, 429 | .owf-803-d:before, 430 | .owf-803-n:before { 431 | content: "\ED83"; 432 | } 433 | /* overcast clouds */ 434 | .owf-804:before, 435 | .owf-804-d:before, 436 | .owf-804-n:before { 437 | content: "\ED84"; 438 | } 439 | 440 | /* Extreme - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 441 | 442 | /* tornado */ 443 | .owf-900:before, 444 | .owf-900-d:before, 445 | .owf-900-n:before { 446 | content: "\EDE4"; 447 | } 448 | /* tropical storm */ 449 | .owf-901:before, 450 | .owf-901-d:before, 451 | .owf-901-n:before { 452 | content: "\EDE5"; 453 | } 454 | /* hurricane */ 455 | .owf-902:before, 456 | .owf-902-d:before, 457 | .owf-902-n:before { 458 | content: "\EDE6"; 459 | } 460 | /* cold */ 461 | .owf-903:before, 462 | .owf-903-d:before, 463 | .owf-903-n:before { 464 | content: "\EDE7"; 465 | } 466 | /* hot */ 467 | .owf-904:before, 468 | .owf-904-d:before, 469 | .owf-904-n:before { 470 | content: "\EDE8"; 471 | } 472 | /* windy */ 473 | .owf-905:before, 474 | .owf-905-d:before, 475 | .owf-905-n:before { 476 | content: "\EDE9"; 477 | } 478 | /* hail */ 479 | .owf-906:before, 480 | .owf-906-d:before, 481 | .owf-906-n:before { 482 | content: "\EDEA"; 483 | } 484 | 485 | /* Additional - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 486 | 487 | /* Setting */ 488 | .owf-950:before, 489 | .owf-950-d:before, 490 | .owf-950-n:before { 491 | content: "\EE16"; 492 | } 493 | /* Light breeze */ 494 | .owf-952:before, 495 | .owf-952-d:before, 496 | .owf-952-n:before { 497 | content: "\EE18"; 498 | } 499 | /* Gentle Breeze */ 500 | .owf-953:before, 501 | .owf-953-d:before, 502 | .owf-953-n:before { 503 | content: "\EE19"; 504 | } 505 | /* Moderate breeze */ 506 | .owf-954:before, 507 | .owf-954-d:before, 508 | .owf-954-n:before { 509 | content: "\EE1A"; 510 | } 511 | /* Fresh Breeze */ 512 | .owf-955:before, 513 | .owf-955-d:before, 514 | .owf-955-n:before { 515 | content: "\EE1B"; 516 | } 517 | /* Strong Breeze */ 518 | .owf-956:before, 519 | .owf-956-d:before, 520 | .owf-956-n:before { 521 | content: "\EE1C"; 522 | } 523 | /* High wind, near gale */ 524 | .owf-957:before, 525 | .owf-957-d:before, 526 | .owf-957-n:before { 527 | content: "\EE1D"; 528 | } 529 | /* Gale */ 530 | .owf-958:before, 531 | .owf-958-d:before, 532 | .owf-958-n:before { 533 | content: "\EE1E"; 534 | } 535 | /* Severe Gale */ 536 | .owf-959:before, 537 | .owf-959-d:before, 538 | .owf-959-n:before { 539 | content: "\EE1F"; 540 | } 541 | /* Storm */ 542 | .owf-960:before, 543 | .owf-960-d:before, 544 | .owf-960-n:before { 545 | content: "\EE20"; 546 | } 547 | /* Violent Storm */ 548 | .owf-961:before, 549 | .owf-961-d:before, 550 | .owf-961-n:before { 551 | content: "\EE21"; 552 | } 553 | /* Hurricane */ 554 | .owf-962:before, 555 | .owf-962-d:before, 556 | .owf-962-n:before { 557 | content: "\EE22"; 558 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/owfont-regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * owfont-regular 1.0.0 by Deniz Fuchidzhiev - http://websygen.com 3 | * License - font: SIL OFL 1.1, css: MIT License 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face{font-family:owfont;src:url(../fonts/owfont-regular.eot?v=1.0.0);src:url(../fonts/owfont-regular.eot?#iefix&v=1.0.0) format('embedded-opentype'),url(../fonts/owfont-regular.woff) format('woff'),url(../fonts/owfont-regular.ttf) format('truetype'),url(../fonts/owfont-regular.svg#owf-regular) format('svg');font-weight:400;font-style:normal}.owf{display:inline-block;font:normal normal normal 14px/1 owfont;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0,0)}.owf-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.owf-2x{font-size:2em}.owf-3x{font-size:3em}.owf-4x{font-size:4em}.owf-5x{font-size:5em}.owf-fw{width:1.28571429em;text-align:center}.owf-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.owf-ul>li{position:relative}.owf-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.owf-li.owf-lg{left:-1.85714286em}.owf-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.owf-pull-right{float:right}.owf-pull-left{float:left}.owf.owf-pull-left{margin-right:.3em}.owf.owf-pull-right{margin-left:.3em}.owf-200-d:before,.owf-200-n:before,.owf-200:before{content:"\EB28"}.owf-201-d:before,.owf-201-n:before,.owf-201:before{content:"\EB29"}.owf-202-d:before,.owf-202-n:before,.owf-202:before{content:"\EB2A"}.owf-210-d:before,.owf-210-n:before,.owf-210:before{content:"\EB32"}.owf-211-d:before,.owf-211-n:before,.owf-211:before{content:"\EB33"}.owf-212-d:before,.owf-212-n:before,.owf-212:before{content:"\EB34"}.owf-221-d:before,.owf-221-n:before,.owf-221:before{content:"\EB3D"}.owf-230-d:before,.owf-230-n:before,.owf-230:before{content:"\EB46"}.owf-231-d:before,.owf-231-n:before,.owf-231:before{content:"\EB47"}.owf-232-d:before,.owf-232-n:before,.owf-232:before{content:"\EB48"}.owf-300-d:before,.owf-300-n:before,.owf-300:before{content:"\EB8C"}.owf-301-d:before,.owf-301-n:before,.owf-301:before{content:"\EB8D"}.owf-302-d:before,.owf-302-n:before,.owf-302:before{content:"\EB8E"}.owf-310-d:before,.owf-310-n:before,.owf-310:before{content:"\EB96"}.owf-311-d:before,.owf-311-n:before,.owf-311:before{content:"\EB97"}.owf-312-d:before,.owf-312-n:before,.owf-312:before{content:"\EB98"}.owf-313-d:before,.owf-313-n:before,.owf-313:before{content:"\EB99"}.owf-314-d:before,.owf-314-n:before,.owf-314:before{content:"\EB9A"}.owf-321-d:before,.owf-321-n:before,.owf-321:before{content:"\EBA1"}.owf-500-d:before,.owf-500-n:before,.owf-500:before{content:"\EC54"}.owf-501-d:before,.owf-501-n:before,.owf-501:before{content:"\EC55"}.owf-502-d:before,.owf-502-n:before,.owf-502:before{content:"\EC56"}.owf-503-d:before,.owf-503-n:before,.owf-503:before{content:"\EC57"}.owf-504-d:before,.owf-504-n:before,.owf-504:before{content:"\EC58"}.owf-511-d:before,.owf-511-n:before,.owf-511:before{content:"\EC5F"}.owf-520-d:before,.owf-520-n:before,.owf-520:before{content:"\EC68"}.owf-521-d:before,.owf-521-n:before,.owf-521:before{content:"\EC69"}.owf-522-d:before,.owf-522-n:before,.owf-522:before{content:"\EC6A"}.owf-531-d:before,.owf-531-n:before,.owf-531:before{content:"\EC73"}.owf-600-d:before,.owf-600-n:before,.owf-600:before{content:"\ECB8"}.owf-601-d:before,.owf-601-n:before,.owf-601:before{content:"\ECB9"}.owf-602-d:before,.owf-602-n:before,.owf-602:before{content:"\ECBA"}.owf-611-d:before,.owf-611-n:before,.owf-611:before{content:"\ECC3"}.owf-612-d:before,.owf-612-n:before,.owf-612:before{content:"\ECC4"}.owf-615-d:before,.owf-615-n:before,.owf-615:before{content:"\ECC7"}.owf-616-d:before,.owf-616-n:before,.owf-616:before{content:"\ECC8"}.owf-620-d:before,.owf-620-n:before,.owf-620:before{content:"\ECCC"}.owf-621-d:before,.owf-621-n:before,.owf-621:before{content:"\ECCD"}.owf-622-d:before,.owf-622-n:before,.owf-622:before{content:"\ECCE"}.owf-701-d:before,.owf-701-n:before,.owf-701:before{content:"\ED1D"}.owf-711-d:before,.owf-711-n:before,.owf-711:before{content:"\ED27"}.owf-721-d:before,.owf-721-n:before,.owf-721:before{content:"\ED31"}.owf-731-d:before,.owf-731-n:before,.owf-731:before{content:"\ED3B"}.owf-741-d:before,.owf-741-n:before,.owf-741:before{content:"\ED45"}.owf-751-d:before,.owf-751-n:before,.owf-751:before{content:"\ED4F"}.owf-761-d:before,.owf-761-n:before,.owf-761:before{content:"\ED59"}.owf-762-d:before,.owf-762-n:before,.owf-762:before{content:"\ED5A"}.owf-771-d:before,.owf-771-n:before,.owf-771:before{content:"\ED63"}.owf-781-d:before,.owf-781-n:before,.owf-781:before{content:"\ED6D"}.owf-800-d:before,.owf-800:before,.owf-951-d:before,.owf-951:before{content:"\ED80"}.owf-800-n:before,.owf-951-n:before{content:"\F168"}.owf-801-d:before,.owf-801:before{content:"\ED81"}.owf-801-n:before{content:"\F169"}.owf-802-d:before,.owf-802:before{content:"\ED82"}.owf-802-n:before{content:"\F16A"}.owf-803-d:before,.owf-803-n:before,.owf-803:before{content:"\ED83"}.owf-804-d:before,.owf-804-n:before,.owf-804:before{content:"\ED84"}.owf-900-d:before,.owf-900-n:before,.owf-900:before{content:"\EDE4"}.owf-901-d:before,.owf-901-n:before,.owf-901:before{content:"\EDE5"}.owf-902-d:before,.owf-902-n:before,.owf-902:before{content:"\EDE6"}.owf-903-d:before,.owf-903-n:before,.owf-903:before{content:"\EDE7"}.owf-904-d:before,.owf-904-n:before,.owf-904:before{content:"\EDE8"}.owf-905-d:before,.owf-905-n:before,.owf-905:before{content:"\EDE9"}.owf-906-d:before,.owf-906-n:before,.owf-906:before{content:"\EDEA"}.owf-950-d:before,.owf-950-n:before,.owf-950:before{content:"\EE16"}.owf-952-d:before,.owf-952-n:before,.owf-952:before{content:"\EE18"}.owf-953-d:before,.owf-953-n:before,.owf-953:before{content:"\EE19"}.owf-954-d:before,.owf-954-n:before,.owf-954:before{content:"\EE1A"}.owf-955-d:before,.owf-955-n:before,.owf-955:before{content:"\EE1B"}.owf-956-d:before,.owf-956-n:before,.owf-956:before{content:"\EE1C"}.owf-957-d:before,.owf-957-n:before,.owf-957:before{content:"\EE1D"}.owf-958-d:before,.owf-958-n:before,.owf-958:before{content:"\EE1E"}.owf-959-d:before,.owf-959-n:before,.owf-959:before{content:"\EE1F"}.owf-960-d:before,.owf-960-n:before,.owf-960:before{content:"\EE20"}.owf-961-d:before,.owf-961-n:before,.owf-961:before{content:"\EE21"}.owf-962-d:before,.owf-962-n:before,.owf-962:before{content:"\EE22"} -------------------------------------------------------------------------------- /src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | body { 6 | font-family: arial, sans-serif; 7 | background: url("../img/background.jpg") no-repeat #0e4f79; 8 | background-size: cover; 9 | } 10 | #twitter { 11 | position: fixed; 12 | top: 0px; 13 | right: 0px; 14 | padding: 0.6em 3em; 15 | background: url("../img/twitter-logo.png") no-repeat #929292; 16 | background-size: contain; 17 | } 18 | #twitter a { 19 | color: white; 20 | text-shadow: 2px 2px black; 21 | text-decoration: none; 22 | } 23 | h1 { 24 | font-size: 60px; 25 | padding: 20px 20px 0px 20px; 26 | color: white; 27 | text-shadow: 2px 2px black; 28 | } 29 | h2 { 30 | font-size: 35px; 31 | padding-bottom: 10px; 32 | } 33 | li { 34 | list-style: none; 35 | display: block; 36 | background-color: rgba(255,255,255,.8); 37 | padding: 20px; 38 | margin: 20px; 39 | } 40 | ul li div i { 41 | display: block; 42 | float: left; 43 | padding-right: 20px; 44 | } 45 | div.temp { 46 | font-size: 30px; 47 | float: left; 48 | padding: 10px 100px 0px 0px; 49 | } 50 | div.temp sup { 51 | font-size: 15px; 52 | } 53 | 54 | /* Generated by Glyphter (http://www.glyphter.com) */ 55 | @font-face { 56 | font-family: 'SpringOnePlatform'; 57 | src: url('../fonts/SpringOnePlatform.eot'); 58 | src: url('../fonts/SpringOnePlatform.eot?#iefix') format('embedded-opentype'), 59 | url('../fonts/SpringOnePlatform.woff') format('woff'), 60 | url('../fonts/SpringOnePlatform.ttf') format('truetype'), 61 | url('../fonts/SpringOnePlatform.svg#SpringOnePlatform') format('svg'); 62 | font-weight: normal; 63 | font-style: normal; 64 | } 65 | /* SIZZLING */ 66 | .owf.owf-666:before, 67 | .owf.owf-666-d:before, 68 | .owf.owf-666-n:before { 69 | display: inline-block; 70 | font-family: 'SpringOnePlatform'; 71 | font-style: normal; 72 | font-weight: normal; 73 | line-height: 1; 74 | -webkit-font-smoothing: antialiased; 75 | -moz-osx-font-smoothing: grayscale; 76 | content: "\0041"; 77 | } -------------------------------------------------------------------------------- /src/main/resources/static/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ooops, page not found 6 | 16 | 17 | 18 |
19 |
20 |

¯\_(ツ)_/¯

21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/fonts/SpringOnePlatform.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/SpringOnePlatform.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/SpringOnePlatform.svg: -------------------------------------------------------------------------------- 1 | Generated by Glyphter 2 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/SpringOnePlatform.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/SpringOnePlatform.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/SpringOnePlatform.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/SpringOnePlatform.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/owfont-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/owfont-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/owfont-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/owfont-regular.otf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/owfont-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20110222 at Wed Feb 11 17:55:33 2015 6 | By www-data 7 | 8 | 9 | 10 | 23 | 26 | 29 | 31 | 33 | 35 | 38 | 42 | 46 | 49 | 52 | 55 | 59 | 62 | 65 | 68 | 71 | 74 | 77 | 80 | 83 | 87 | 91 | 95 | 99 | 102 | 105 | 108 | 111 | 115 | 120 | 123 | 127 | 132 | 137 | 141 | 146 | 152 | 156 | 160 | 164 | 168 | 173 | 180 | 189 | 193 | 195 | 197 | 201 | 204 | 208 | 211 | 214 | 218 | 223 | 225 | 227 | 230 | 233 | 237 | 241 | 244 | 246 | 248 | 251 | 271 | 275 | 278 | 286 | 289 | 293 | 295 | 297 | 300 | 305 | 310 | 317 | 326 | 337 | 345 | 353 | 361 | 370 | 373 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/owfont-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/owfont-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/owfont-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/fonts/owfont-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/img/background.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/twitter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringOnePlatform2016/weather-app/3b2f134f629ad5f50ec0e91d54f3b8abe20852c6/src/main/resources/static/img/twitter-logo.png -------------------------------------------------------------------------------- /src/main/resources/templates/summary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Weather Summary 8 | 9 | 10 | 11 |

Weather Summary

12 |
    13 | {{#summary}} 14 |
  • 15 |

    {{city}}, {{country}}

    16 |
    17 | 18 |
    {{fahrenheitTemperature}}°F / {{celsiusTemperature}}°C
    19 |
    20 |
    21 |
  • 22 | {{/summary}} 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/test/java/com/example/weather/integration/ows/WeatherServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.integration.ows; 2 | 3 | import org.assertj.core.data.Offset; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; 9 | import org.springframework.core.io.ClassPathResource; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.context.TestPropertySource; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.client.MockRestServiceServer; 14 | 15 | import static org.assertj.core.api.Assertions.*; 16 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; 17 | import static org.springframework.test.web.client.response.MockRestResponseCreators.*; 18 | 19 | @RunWith(SpringRunner.class) 20 | @RestClientTest(WeatherService.class) 21 | @TestPropertySource(properties = "app.weather.api.key=test-ABC") 22 | public class WeatherServiceTest { 23 | 24 | private static final String URL = "http://api.openweathermap.org/data/2.5/"; 25 | 26 | @Autowired 27 | private WeatherService weatherService; 28 | 29 | @Autowired 30 | private MockRestServiceServer server; 31 | 32 | @Test 33 | public void getWeather() { 34 | this.server.expect( 35 | requestTo(URL + "weather?q=barcelona,es&APPID=test-ABC")) 36 | .andRespond(withSuccess( 37 | new ClassPathResource("weather-barcelona.json", getClass()), 38 | MediaType.APPLICATION_JSON)); 39 | Weather forecast = this.weatherService.getWeather("es", "barcelona"); 40 | assertThat(forecast.getName()).isEqualTo("Barcelona"); 41 | assertThat(forecast.getTemperature()).isEqualTo(286.72, Offset.offset(0.1)); 42 | assertThat(forecast.getWeatherId()).isEqualTo(800); 43 | assertThat(forecast.getWeatherIcon()).isEqualTo("01d"); 44 | this.server.verify(); 45 | } 46 | 47 | @Test 48 | public void getWeatherForecast() { 49 | this.server.expect( 50 | requestTo(URL + "forecast?q=barcelona,es&APPID=test-ABC")) 51 | .andRespond(withSuccess( 52 | new ClassPathResource("forecast-barcelona.json", getClass()), 53 | MediaType.APPLICATION_JSON)); 54 | this.weatherService.getWeatherForecast("es", "barcelona"); 55 | this.server.verify(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/example/weather/web/WeatherApiControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.web; 2 | 3 | import java.time.Instant; 4 | 5 | import com.example.weather.integration.ows.Weather; 6 | import com.example.weather.integration.ows.WeatherEntry; 7 | import com.example.weather.integration.ows.WeatherForecast; 8 | import com.example.weather.integration.ows.WeatherService; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 14 | import org.springframework.boot.test.mock.mockito.MockBean; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.mockito.BDDMockito.given; 20 | import static org.mockito.Mockito.verify; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | 25 | @RunWith(SpringRunner.class) 26 | @WebMvcTest(WeatherApiController.class) 27 | public class WeatherApiControllerTest { 28 | 29 | @MockBean 30 | private WeatherService weatherService; 31 | 32 | @Autowired 33 | private MockMvc mvc; 34 | 35 | @Test 36 | public void weather() throws Exception { 37 | Weather weather = new Weather(); 38 | weather.setName("London"); 39 | setWeatherEntry(weather, 286.72, 800, "01d", Instant.ofEpochSecond(1234)); 40 | given(this.weatherService.getWeather("uk", "london")).willReturn(weather); 41 | this.mvc.perform(get("/api/weather/now/uk/london")) 42 | .andExpect(status().isOk()) 43 | .andExpect(jsonPath("$.name", is("London"))) 44 | .andExpect(jsonPath("$.temperature", is(286.72))) 45 | .andExpect(jsonPath("$.weatherId", is(800))) 46 | .andExpect(jsonPath("$.weatherIcon", is("01d"))) 47 | .andExpect(jsonPath("$.timestamp", is("1970-01-01T00:20:34Z"))); 48 | verify(this.weatherService).getWeather("uk", "london"); 49 | } 50 | 51 | @Test 52 | public void weatherForecast() throws Exception { 53 | WeatherForecast forecast = new WeatherForecast(); 54 | forecast.setName("Brussels"); 55 | forecast.getEntries().add(createWeatherEntry(285.45, 600, "02d", Instant.ofEpochSecond(1234))); 56 | forecast.getEntries().add(createWeatherEntry(294.45, 800, "01d", Instant.ofEpochSecond(5678))); 57 | given(this.weatherService.getWeatherForecast("be", "brussels")).willReturn(forecast); 58 | this.mvc.perform(get("/api/weather/weekly/be/brussels")) 59 | .andExpect(status().isOk()) 60 | .andExpect(jsonPath("$.name", is("Brussels"))) 61 | .andExpect(jsonPath("$.entries[0].temperature", is(285.45))) 62 | .andExpect(jsonPath("$.entries[1].temperature", is(294.45))); 63 | verify(this.weatherService).getWeatherForecast("be", "brussels"); 64 | } 65 | 66 | private static WeatherEntry createWeatherEntry(double temperature, int id, String icon, 67 | Instant timestamp) { 68 | WeatherEntry entry = new WeatherEntry(); 69 | setWeatherEntry(entry, temperature, id, icon, timestamp); 70 | return entry; 71 | } 72 | 73 | private static void setWeatherEntry(WeatherEntry entry, double temperature, int id, String icon, 74 | Instant timestamp) { 75 | entry.setTemperature(temperature); 76 | entry.setWeatherId(id); 77 | entry.setWeatherIcon(icon); 78 | entry.setTimestamp(timestamp.getEpochSecond()); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/example/weather/web/WeatherApiDocumentationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.weather.web; 2 | 3 | import static org.mockito.BDDMockito.given; 4 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; 5 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; 6 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 7 | import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 8 | import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; 9 | import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 11 | 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; 16 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 17 | import org.springframework.boot.test.mock.mockito.MockBean; 18 | import org.springframework.core.io.ClassPathResource; 19 | import org.springframework.restdocs.payload.FieldDescriptor; 20 | import org.springframework.test.context.TestPropertySource; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | 24 | import com.example.weather.integration.ows.Weather; 25 | import com.example.weather.integration.ows.WeatherForecast; 26 | import com.example.weather.integration.ows.WeatherService; 27 | import com.fasterxml.jackson.databind.ObjectMapper; 28 | 29 | @RunWith(SpringRunner.class) 30 | @WebMvcTest 31 | @AutoConfigureRestDocs(outputDir = "target/generated-snippets") 32 | @TestPropertySource(properties="spring.jackson.serialization.indent-output:true") 33 | public class WeatherApiDocumentationTest { 34 | 35 | @Autowired 36 | private MockMvc mockMvc; 37 | 38 | @MockBean 39 | private WeatherService weatherService; 40 | 41 | @Autowired 42 | private ObjectMapper objectMapper; 43 | 44 | private final FieldDescriptor[] weatherFields = new FieldDescriptor[] { 45 | fieldWithPath("weatherId").description("ID of the weather conditions"), 46 | fieldWithPath("weatherIcon").description("ID of the weather icon"), 47 | fieldWithPath("temperature").description("Temperature in Kelvin"), 48 | fieldWithPath("timestamp").description("ISO-8601 timestamp of the weather data")}; 49 | 50 | @Test 51 | public void weather() throws Exception { 52 | Weather weather = this.objectMapper.readValue( 53 | new ClassPathResource("/com/example/weather/integration/ows/weather-barcelona.json").getURL(), 54 | Weather.class); 55 | given(this.weatherService.getWeather("spring", "barcelona")).willReturn(weather); 56 | given(this.weatherService.getWeather("spain", "barcelona")).willReturn(weather); 57 | this.mockMvc.perform(get("/api/weather/now/{country}/{city}", "spain", "barcelona")) 58 | .andExpect(status().isOk()) 59 | .andDo(document("weather", 60 | pathParameters( 61 | parameterWithName("country").description("Name of the country"), 62 | parameterWithName("city").description("Name of the city")), 63 | responseFields( 64 | fieldWithPath("name").description("Name of the weather data")) 65 | .and(this.weatherFields))); 66 | } 67 | 68 | @Test 69 | public void weatherForecast() throws Exception { 70 | WeatherForecast weatherForecast = this.objectMapper.readValue( 71 | new ClassPathResource("/com/example/weather/integration/ows/forecast-barcelona.json").getURL(), 72 | WeatherForecast.class); 73 | given(this.weatherService.getWeatherForecast("spain", "barcelona")).willReturn(weatherForecast); 74 | given(this.weatherService.getWeatherForecast("spain", "barcelona")).willReturn(weatherForecast); 75 | this.mockMvc.perform(get("/api/weather/weekly/{country}/{city}", "spain", "barcelona")) 76 | .andExpect(status().isOk()) 77 | .andDo(document("weather-forecast", 78 | pathParameters( 79 | parameterWithName("country").description("Name of the country"), 80 | parameterWithName("city").description("Name of the city")), 81 | responseFields( 82 | fieldWithPath("name").description("Name of the weather forecast"), 83 | fieldWithPath("entries").description("Array of weather data")) 84 | .andWithPrefix("entries[].", this.weatherFields))); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/resources/com/example/weather/integration/ows/forecast-barcelona.json: -------------------------------------------------------------------------------- 1 | { 2 | "city": { 3 | "id": 3128760, 4 | "name": "Barcelona", 5 | "coord": { 6 | "lon": 2.15899, 7 | "lat": 41.38879 8 | }, 9 | "country": "ES", 10 | "population": 0, 11 | "sys": { 12 | "population": 0 13 | } 14 | }, 15 | "cod": "200", 16 | "message": 0.0032, 17 | "cnt": 40, 18 | "list": [ 19 | { 20 | "dt": 1460646000, 21 | "main": { 22 | "temp": 291.52, 23 | "temp_min": 290.019, 24 | "temp_max": 291.52, 25 | "pressure": 1018.74, 26 | "sea_level": 1026.88, 27 | "grnd_level": 1018.74, 28 | "humidity": 80, 29 | "temp_kf": 1.5 30 | }, 31 | "weather": [ 32 | { 33 | "id": 800, 34 | "main": "Clear", 35 | "description": "clear sky", 36 | "icon": "01d" 37 | } 38 | ], 39 | "clouds": { 40 | "all": 0 41 | }, 42 | "wind": { 43 | "speed": 6.33, 44 | "deg": 207.001 45 | }, 46 | "sys": { 47 | "pod": "d" 48 | }, 49 | "dt_txt": "2016-04-14 15:00:00" 50 | }, 51 | { 52 | "dt": 1460656800, 53 | "main": { 54 | "temp": 290.64, 55 | "temp_min": 289.225, 56 | "temp_max": 290.64, 57 | "pressure": 1018.4, 58 | "sea_level": 1026.61, 59 | "grnd_level": 1018.4, 60 | "humidity": 84, 61 | "temp_kf": 1.42 62 | }, 63 | "weather": [ 64 | { 65 | "id": 800, 66 | "main": "Clear", 67 | "description": "clear sky", 68 | "icon": "01d" 69 | } 70 | ], 71 | "clouds": { 72 | "all": 0 73 | }, 74 | "wind": { 75 | "speed": 5.44, 76 | "deg": 219.502 77 | }, 78 | "sys": { 79 | "pod": "d" 80 | }, 81 | "dt_txt": "2016-04-14 18:00:00" 82 | }, 83 | { 84 | "dt": 1460667600, 85 | "main": { 86 | "temp": 289.41, 87 | "temp_min": 288.07, 88 | "temp_max": 289.41, 89 | "pressure": 1019.09, 90 | "sea_level": 1027.46, 91 | "grnd_level": 1019.09, 92 | "humidity": 91, 93 | "temp_kf": 1.34 94 | }, 95 | "weather": [ 96 | { 97 | "id": 800, 98 | "main": "Clear", 99 | "description": "clear sky", 100 | "icon": "01n" 101 | } 102 | ], 103 | "clouds": { 104 | "all": 0 105 | }, 106 | "wind": { 107 | "speed": 5.15, 108 | "deg": 236.504 109 | }, 110 | "sys": { 111 | "pod": "n" 112 | }, 113 | "dt_txt": "2016-04-14 21:00:00" 114 | }, 115 | { 116 | "dt": 1460678400, 117 | "main": { 118 | "temp": 288.76, 119 | "temp_min": 287.501, 120 | "temp_max": 288.76, 121 | "pressure": 1018.94, 122 | "sea_level": 1027.37, 123 | "grnd_level": 1018.94, 124 | "humidity": 96, 125 | "temp_kf": 1.26 126 | }, 127 | "weather": [ 128 | { 129 | "id": 800, 130 | "main": "Clear", 131 | "description": "clear sky", 132 | "icon": "01n" 133 | } 134 | ], 135 | "clouds": { 136 | "all": 0 137 | }, 138 | "wind": { 139 | "speed": 4.02, 140 | "deg": 250 141 | }, 142 | "sys": { 143 | "pod": "n" 144 | }, 145 | "dt_txt": "2016-04-15 00:00:00" 146 | }, 147 | { 148 | "dt": 1460689200, 149 | "main": { 150 | "temp": 287.71, 151 | "temp_min": 286.527, 152 | "temp_max": 287.71, 153 | "pressure": 1018.07, 154 | "sea_level": 1026.56, 155 | "grnd_level": 1018.07, 156 | "humidity": 100, 157 | "temp_kf": 1.18 158 | }, 159 | "weather": [ 160 | { 161 | "id": 800, 162 | "main": "Clear", 163 | "description": "clear sky", 164 | "icon": "01n" 165 | } 166 | ], 167 | "clouds": { 168 | "all": 0 169 | }, 170 | "wind": { 171 | "speed": 3.11, 172 | "deg": 239.001 173 | }, 174 | "sys": { 175 | "pod": "n" 176 | }, 177 | "dt_txt": "2016-04-15 03:00:00" 178 | }, 179 | { 180 | "dt": 1460700000, 181 | "main": { 182 | "temp": 287.22, 183 | "temp_min": 286.121, 184 | "temp_max": 287.22, 185 | "pressure": 1018.17, 186 | "sea_level": 1026.69, 187 | "grnd_level": 1018.17, 188 | "humidity": 100, 189 | "temp_kf": 1.1 190 | }, 191 | "weather": [ 192 | { 193 | "id": 800, 194 | "main": "Clear", 195 | "description": "clear sky", 196 | "icon": "01d" 197 | } 198 | ], 199 | "clouds": { 200 | "all": 0 201 | }, 202 | "wind": { 203 | "speed": 2.85, 204 | "deg": 229 205 | }, 206 | "sys": { 207 | "pod": "d" 208 | }, 209 | "dt_txt": "2016-04-15 06:00:00" 210 | }, 211 | { 212 | "dt": 1460710800, 213 | "main": { 214 | "temp": 290, 215 | "temp_min": 288.974, 216 | "temp_max": 290, 217 | "pressure": 1018.74, 218 | "sea_level": 1027.1, 219 | "grnd_level": 1018.74, 220 | "humidity": 88, 221 | "temp_kf": 1.03 222 | }, 223 | "weather": [ 224 | { 225 | "id": 800, 226 | "main": "Clear", 227 | "description": "clear sky", 228 | "icon": "01d" 229 | } 230 | ], 231 | "clouds": { 232 | "all": 0 233 | }, 234 | "wind": { 235 | "speed": 2.11, 236 | "deg": 211 237 | }, 238 | "sys": { 239 | "pod": "d" 240 | }, 241 | "dt_txt": "2016-04-15 09:00:00" 242 | }, 243 | { 244 | "dt": 1460721600, 245 | "main": { 246 | "temp": 290.32, 247 | "temp_min": 289.372, 248 | "temp_max": 290.32, 249 | "pressure": 1018.32, 250 | "sea_level": 1026.45, 251 | "grnd_level": 1018.32, 252 | "humidity": 86, 253 | "temp_kf": 0.95 254 | }, 255 | "weather": [ 256 | { 257 | "id": 802, 258 | "main": "Clouds", 259 | "description": "scattered clouds", 260 | "icon": "03d" 261 | } 262 | ], 263 | "clouds": { 264 | "all": 32 265 | }, 266 | "wind": { 267 | "speed": 3.66, 268 | "deg": 171.001 269 | }, 270 | "sys": { 271 | "pod": "d" 272 | }, 273 | "dt_txt": "2016-04-15 12:00:00" 274 | }, 275 | { 276 | "dt": 1460732400, 277 | "main": { 278 | "temp": 289.96, 279 | "temp_min": 289.097, 280 | "temp_max": 289.96, 281 | "pressure": 1016.62, 282 | "sea_level": 1024.8, 283 | "grnd_level": 1016.62, 284 | "humidity": 85, 285 | "temp_kf": 0.87 286 | }, 287 | "weather": [ 288 | { 289 | "id": 802, 290 | "main": "Clouds", 291 | "description": "scattered clouds", 292 | "icon": "03d" 293 | } 294 | ], 295 | "clouds": { 296 | "all": 48 297 | }, 298 | "wind": { 299 | "speed": 2.57, 300 | "deg": 143.501 301 | }, 302 | "sys": { 303 | "pod": "d" 304 | }, 305 | "dt_txt": "2016-04-15 15:00:00" 306 | }, 307 | { 308 | "dt": 1460743200, 309 | "main": { 310 | "temp": 289.3, 311 | "temp_min": 288.512, 312 | "temp_max": 289.3, 313 | "pressure": 1015.35, 314 | "sea_level": 1023.64, 315 | "grnd_level": 1015.35, 316 | "humidity": 88, 317 | "temp_kf": 0.79 318 | }, 319 | "weather": [ 320 | { 321 | "id": 802, 322 | "main": "Clouds", 323 | "description": "scattered clouds", 324 | "icon": "03d" 325 | } 326 | ], 327 | "clouds": { 328 | "all": 36 329 | }, 330 | "wind": { 331 | "speed": 0.76, 332 | "deg": 129.503 333 | }, 334 | "sys": { 335 | "pod": "d" 336 | }, 337 | "dt_txt": "2016-04-15 18:00:00" 338 | }, 339 | { 340 | "dt": 1460754000, 341 | "main": { 342 | "temp": 287.38, 343 | "temp_min": 286.669, 344 | "temp_max": 287.38, 345 | "pressure": 1015.54, 346 | "sea_level": 1023.98, 347 | "grnd_level": 1015.54, 348 | "humidity": 100, 349 | "temp_kf": 0.71 350 | }, 351 | "weather": [ 352 | { 353 | "id": 800, 354 | "main": "Clear", 355 | "description": "clear sky", 356 | "icon": "01n" 357 | } 358 | ], 359 | "clouds": { 360 | "all": 0 361 | }, 362 | "wind": { 363 | "speed": 0.52, 364 | "deg": 211.001 365 | }, 366 | "sys": { 367 | "pod": "n" 368 | }, 369 | "dt_txt": "2016-04-15 21:00:00" 370 | }, 371 | { 372 | "dt": 1460764800, 373 | "main": { 374 | "temp": 286.85, 375 | "temp_min": 286.217, 376 | "temp_max": 286.85, 377 | "pressure": 1014.98, 378 | "sea_level": 1023.43, 379 | "grnd_level": 1014.98, 380 | "humidity": 100, 381 | "temp_kf": 0.63 382 | }, 383 | "weather": [ 384 | { 385 | "id": 800, 386 | "main": "Clear", 387 | "description": "clear sky", 388 | "icon": "01n" 389 | } 390 | ], 391 | "clouds": { 392 | "all": 0 393 | }, 394 | "wind": { 395 | "speed": 2.21, 396 | "deg": 236.004 397 | }, 398 | "sys": { 399 | "pod": "n" 400 | }, 401 | "dt_txt": "2016-04-16 00:00:00" 402 | }, 403 | { 404 | "dt": 1460775600, 405 | "main": { 406 | "temp": 286.58, 407 | "temp_min": 286.024, 408 | "temp_max": 286.58, 409 | "pressure": 1014.3, 410 | "sea_level": 1022.77, 411 | "grnd_level": 1014.3, 412 | "humidity": 100, 413 | "temp_kf": 0.55 414 | }, 415 | "weather": [ 416 | { 417 | "id": 800, 418 | "main": "Clear", 419 | "description": "clear sky", 420 | "icon": "01n" 421 | } 422 | ], 423 | "clouds": { 424 | "all": 0 425 | }, 426 | "wind": { 427 | "speed": 3.86, 428 | "deg": 243.002 429 | }, 430 | "sys": { 431 | "pod": "n" 432 | }, 433 | "dt_txt": "2016-04-16 03:00:00" 434 | }, 435 | { 436 | "dt": 1460786400, 437 | "main": { 438 | "temp": 286.59, 439 | "temp_min": 286.114, 440 | "temp_max": 286.59, 441 | "pressure": 1014.37, 442 | "sea_level": 1023.02, 443 | "grnd_level": 1014.37, 444 | "humidity": 100, 445 | "temp_kf": 0.47 446 | }, 447 | "weather": [ 448 | { 449 | "id": 800, 450 | "main": "Clear", 451 | "description": "clear sky", 452 | "icon": "01d" 453 | } 454 | ], 455 | "clouds": { 456 | "all": 0 457 | }, 458 | "wind": { 459 | "speed": 3.82, 460 | "deg": 251.501 461 | }, 462 | "sys": { 463 | "pod": "d" 464 | }, 465 | "dt_txt": "2016-04-16 06:00:00" 466 | }, 467 | { 468 | "dt": 1460797200, 469 | "main": { 470 | "temp": 290.98, 471 | "temp_min": 290.589, 472 | "temp_max": 290.98, 473 | "pressure": 1015.86, 474 | "sea_level": 1024.22, 475 | "grnd_level": 1015.86, 476 | "humidity": 79, 477 | "temp_kf": 0.39 478 | }, 479 | "weather": [ 480 | { 481 | "id": 800, 482 | "main": "Clear", 483 | "description": "clear sky", 484 | "icon": "01d" 485 | } 486 | ], 487 | "clouds": { 488 | "all": 0 489 | }, 490 | "wind": { 491 | "speed": 3.51, 492 | "deg": 239.001 493 | }, 494 | "sys": { 495 | "pod": "d" 496 | }, 497 | "dt_txt": "2016-04-16 09:00:00" 498 | }, 499 | { 500 | "dt": 1460808000, 501 | "main": { 502 | "temp": 291.91, 503 | "temp_min": 291.596, 504 | "temp_max": 291.91, 505 | "pressure": 1015.65, 506 | "sea_level": 1023.98, 507 | "grnd_level": 1015.65, 508 | "humidity": 75, 509 | "temp_kf": 0.32 510 | }, 511 | "weather": [ 512 | { 513 | "id": 800, 514 | "main": "Clear", 515 | "description": "clear sky", 516 | "icon": "01d" 517 | } 518 | ], 519 | "clouds": { 520 | "all": 0 521 | }, 522 | "wind": { 523 | "speed": 6.16, 524 | "deg": 212.001 525 | }, 526 | "sys": { 527 | "pod": "d" 528 | }, 529 | "dt_txt": "2016-04-16 12:00:00" 530 | }, 531 | { 532 | "dt": 1460818800, 533 | "main": { 534 | "temp": 291.14, 535 | "temp_min": 290.904, 536 | "temp_max": 291.14, 537 | "pressure": 1014.78, 538 | "sea_level": 1022.99, 539 | "grnd_level": 1014.78, 540 | "humidity": 77, 541 | "temp_kf": 0.24 542 | }, 543 | "weather": [ 544 | { 545 | "id": 800, 546 | "main": "Clear", 547 | "description": "clear sky", 548 | "icon": "01d" 549 | } 550 | ], 551 | "clouds": { 552 | "all": 0 553 | }, 554 | "wind": { 555 | "speed": 7.01, 556 | "deg": 205.002 557 | }, 558 | "sys": { 559 | "pod": "d" 560 | }, 561 | "dt_txt": "2016-04-16 15:00:00" 562 | }, 563 | { 564 | "dt": 1460829600, 565 | "main": { 566 | "temp": 290.14, 567 | "temp_min": 289.978, 568 | "temp_max": 290.14, 569 | "pressure": 1014.85, 570 | "sea_level": 1023.08, 571 | "grnd_level": 1014.85, 572 | "humidity": 81, 573 | "temp_kf": 0.16 574 | }, 575 | "weather": [ 576 | { 577 | "id": 801, 578 | "main": "Clouds", 579 | "description": "few clouds", 580 | "icon": "02d" 581 | } 582 | ], 583 | "clouds": { 584 | "all": 12 585 | }, 586 | "wind": { 587 | "speed": 5.36, 588 | "deg": 220.503 589 | }, 590 | "sys": { 591 | "pod": "d" 592 | }, 593 | "dt_txt": "2016-04-16 18:00:00" 594 | }, 595 | { 596 | "dt": 1460840400, 597 | "main": { 598 | "temp": 289.1, 599 | "temp_min": 289.017, 600 | "temp_max": 289.1, 601 | "pressure": 1016.52, 602 | "sea_level": 1024.89, 603 | "grnd_level": 1016.52, 604 | "humidity": 88, 605 | "temp_kf": 0.08 606 | }, 607 | "weather": [ 608 | { 609 | "id": 802, 610 | "main": "Clouds", 611 | "description": "scattered clouds", 612 | "icon": "03n" 613 | } 614 | ], 615 | "clouds": { 616 | "all": 32 617 | }, 618 | "wind": { 619 | "speed": 5.62, 620 | "deg": 235.002 621 | }, 622 | "sys": { 623 | "pod": "n" 624 | }, 625 | "dt_txt": "2016-04-16 21:00:00" 626 | }, 627 | { 628 | "dt": 1460851200, 629 | "main": { 630 | "temp": 288.555, 631 | "temp_min": 288.555, 632 | "temp_max": 288.555, 633 | "pressure": 1017.02, 634 | "sea_level": 1025.38, 635 | "grnd_level": 1017.02, 636 | "humidity": 91, 637 | "temp_kf": 0 638 | }, 639 | "weather": [ 640 | { 641 | "id": 800, 642 | "main": "Clear", 643 | "description": "clear sky", 644 | "icon": "02n" 645 | } 646 | ], 647 | "clouds": { 648 | "all": 8 649 | }, 650 | "wind": { 651 | "speed": 4.93, 652 | "deg": 238.002 653 | }, 654 | "sys": { 655 | "pod": "n" 656 | }, 657 | "dt_txt": "2016-04-17 00:00:00" 658 | }, 659 | { 660 | "dt": 1460862000, 661 | "main": { 662 | "temp": 287.672, 663 | "temp_min": 287.672, 664 | "temp_max": 287.672, 665 | "pressure": 1016.37, 666 | "sea_level": 1024.9, 667 | "grnd_level": 1016.37, 668 | "humidity": 96, 669 | "temp_kf": 0 670 | }, 671 | "weather": [ 672 | { 673 | "id": 802, 674 | "main": "Clouds", 675 | "description": "scattered clouds", 676 | "icon": "03n" 677 | } 678 | ], 679 | "clouds": { 680 | "all": 44 681 | }, 682 | "wind": { 683 | "speed": 3.01, 684 | "deg": 236.501 685 | }, 686 | "sys": { 687 | "pod": "n" 688 | }, 689 | "dt_txt": "2016-04-17 03:00:00" 690 | }, 691 | { 692 | "dt": 1460872800, 693 | "main": { 694 | "temp": 287.647, 695 | "temp_min": 287.647, 696 | "temp_max": 287.647, 697 | "pressure": 1016.45, 698 | "sea_level": 1024.99, 699 | "grnd_level": 1016.45, 700 | "humidity": 96, 701 | "temp_kf": 0 702 | }, 703 | "weather": [ 704 | { 705 | "id": 803, 706 | "main": "Clouds", 707 | "description": "broken clouds", 708 | "icon": "04d" 709 | } 710 | ], 711 | "clouds": { 712 | "all": 80 713 | }, 714 | "wind": { 715 | "speed": 1.31, 716 | "deg": 237 717 | }, 718 | "sys": { 719 | "pod": "d" 720 | }, 721 | "dt_txt": "2016-04-17 06:00:00" 722 | }, 723 | { 724 | "dt": 1460883600, 725 | "main": { 726 | "temp": 289.96, 727 | "temp_min": 289.96, 728 | "temp_max": 289.96, 729 | "pressure": 1018.73, 730 | "sea_level": 1026.87, 731 | "grnd_level": 1018.73, 732 | "humidity": 83, 733 | "temp_kf": 0 734 | }, 735 | "weather": [ 736 | { 737 | "id": 801, 738 | "main": "Clouds", 739 | "description": "few clouds", 740 | "icon": "02d" 741 | } 742 | ], 743 | "clouds": { 744 | "all": 20 745 | }, 746 | "wind": { 747 | "speed": 3.21, 748 | "deg": 60.5035 749 | }, 750 | "sys": { 751 | "pod": "d" 752 | }, 753 | "dt_txt": "2016-04-17 09:00:00" 754 | }, 755 | { 756 | "dt": 1460894400, 757 | "main": { 758 | "temp": 289.717, 759 | "temp_min": 289.717, 760 | "temp_max": 289.717, 761 | "pressure": 1019.52, 762 | "sea_level": 1027.7, 763 | "grnd_level": 1019.52, 764 | "humidity": 85, 765 | "temp_kf": 0 766 | }, 767 | "weather": [ 768 | { 769 | "id": 802, 770 | "main": "Clouds", 771 | "description": "scattered clouds", 772 | "icon": "03d" 773 | } 774 | ], 775 | "clouds": { 776 | "all": 36 777 | }, 778 | "wind": { 779 | "speed": 5.27, 780 | "deg": 115.509 781 | }, 782 | "sys": { 783 | "pod": "d" 784 | }, 785 | "dt_txt": "2016-04-17 12:00:00" 786 | }, 787 | { 788 | "dt": 1460905200, 789 | "main": { 790 | "temp": 289.174, 791 | "temp_min": 289.174, 792 | "temp_max": 289.174, 793 | "pressure": 1018.51, 794 | "sea_level": 1026.55, 795 | "grnd_level": 1018.51, 796 | "humidity": 88, 797 | "temp_kf": 0 798 | }, 799 | "weather": [ 800 | { 801 | "id": 801, 802 | "main": "Clouds", 803 | "description": "few clouds", 804 | "icon": "02d" 805 | } 806 | ], 807 | "clouds": { 808 | "all": 12 809 | }, 810 | "wind": { 811 | "speed": 3.87, 812 | "deg": 104 813 | }, 814 | "sys": { 815 | "pod": "d" 816 | }, 817 | "dt_txt": "2016-04-17 15:00:00" 818 | }, 819 | { 820 | "dt": 1460916000, 821 | "main": { 822 | "temp": 288.506, 823 | "temp_min": 288.506, 824 | "temp_max": 288.506, 825 | "pressure": 1018.16, 826 | "sea_level": 1026.38, 827 | "grnd_level": 1018.16, 828 | "humidity": 91, 829 | "temp_kf": 0 830 | }, 831 | "weather": [ 832 | { 833 | "id": 801, 834 | "main": "Clouds", 835 | "description": "few clouds", 836 | "icon": "02d" 837 | } 838 | ], 839 | "clouds": { 840 | "all": 12 841 | }, 842 | "wind": { 843 | "speed": 0.86, 844 | "deg": 64.0006 845 | }, 846 | "sys": { 847 | "pod": "d" 848 | }, 849 | "dt_txt": "2016-04-17 18:00:00" 850 | }, 851 | { 852 | "dt": 1460926800, 853 | "main": { 854 | "temp": 287.136, 855 | "temp_min": 287.136, 856 | "temp_max": 287.136, 857 | "pressure": 1019.35, 858 | "sea_level": 1027.84, 859 | "grnd_level": 1019.35, 860 | "humidity": 100, 861 | "temp_kf": 0 862 | }, 863 | "weather": [ 864 | { 865 | "id": 802, 866 | "main": "Clouds", 867 | "description": "scattered clouds", 868 | "icon": "03n" 869 | } 870 | ], 871 | "clouds": { 872 | "all": 44 873 | }, 874 | "wind": { 875 | "speed": 1.47, 876 | "deg": 211.501 877 | }, 878 | "sys": { 879 | "pod": "n" 880 | }, 881 | "dt_txt": "2016-04-17 21:00:00" 882 | }, 883 | { 884 | "dt": 1460937600, 885 | "main": { 886 | "temp": 286.706, 887 | "temp_min": 286.706, 888 | "temp_max": 286.706, 889 | "pressure": 1019.76, 890 | "sea_level": 1028.28, 891 | "grnd_level": 1019.76, 892 | "humidity": 100, 893 | "temp_kf": 0 894 | }, 895 | "weather": [ 896 | { 897 | "id": 802, 898 | "main": "Clouds", 899 | "description": "scattered clouds", 900 | "icon": "03n" 901 | } 902 | ], 903 | "clouds": { 904 | "all": 36 905 | }, 906 | "wind": { 907 | "speed": 3.21, 908 | "deg": 249.002 909 | }, 910 | "sys": { 911 | "pod": "n" 912 | }, 913 | "dt_txt": "2016-04-18 00:00:00" 914 | }, 915 | { 916 | "dt": 1460948400, 917 | "main": { 918 | "temp": 287.606, 919 | "temp_min": 287.606, 920 | "temp_max": 287.606, 921 | "pressure": 1019.53, 922 | "sea_level": 1027.93, 923 | "grnd_level": 1019.53, 924 | "humidity": 97, 925 | "temp_kf": 0 926 | }, 927 | "weather": [ 928 | { 929 | "id": 803, 930 | "main": "Clouds", 931 | "description": "broken clouds", 932 | "icon": "04n" 933 | } 934 | ], 935 | "clouds": { 936 | "all": 76 937 | }, 938 | "wind": { 939 | "speed": 3.16, 940 | "deg": 277 941 | }, 942 | "sys": { 943 | "pod": "n" 944 | }, 945 | "dt_txt": "2016-04-18 03:00:00" 946 | }, 947 | { 948 | "dt": 1460959200, 949 | "main": { 950 | "temp": 288.083, 951 | "temp_min": 288.083, 952 | "temp_max": 288.083, 953 | "pressure": 1020.16, 954 | "sea_level": 1028.65, 955 | "grnd_level": 1020.16, 956 | "humidity": 93, 957 | "temp_kf": 0 958 | }, 959 | "weather": [ 960 | { 961 | "id": 804, 962 | "main": "Clouds", 963 | "description": "overcast clouds", 964 | "icon": "04d" 965 | } 966 | ], 967 | "clouds": { 968 | "all": 92 969 | }, 970 | "wind": { 971 | "speed": 3.26, 972 | "deg": 259.001 973 | }, 974 | "sys": { 975 | "pod": "d" 976 | }, 977 | "dt_txt": "2016-04-18 06:00:00" 978 | }, 979 | { 980 | "dt": 1460970000, 981 | "main": { 982 | "temp": 290.099, 983 | "temp_min": 290.099, 984 | "temp_max": 290.099, 985 | "pressure": 1022.18, 986 | "sea_level": 1030.6, 987 | "grnd_level": 1022.18, 988 | "humidity": 82, 989 | "temp_kf": 0 990 | }, 991 | "weather": [ 992 | { 993 | "id": 803, 994 | "main": "Clouds", 995 | "description": "broken clouds", 996 | "icon": "04d" 997 | } 998 | ], 999 | "clouds": { 1000 | "all": 76 1001 | }, 1002 | "wind": { 1003 | "speed": 3.41, 1004 | "deg": 228 1005 | }, 1006 | "sys": { 1007 | "pod": "d" 1008 | }, 1009 | "dt_txt": "2016-04-18 09:00:00" 1010 | }, 1011 | { 1012 | "dt": 1460980800, 1013 | "main": { 1014 | "temp": 290.38, 1015 | "temp_min": 290.38, 1016 | "temp_max": 290.38, 1017 | "pressure": 1022.78, 1018 | "sea_level": 1031.11, 1019 | "grnd_level": 1022.78, 1020 | "humidity": 81, 1021 | "temp_kf": 0 1022 | }, 1023 | "weather": [ 1024 | { 1025 | "id": 800, 1026 | "main": "Clear", 1027 | "description": "clear sky", 1028 | "icon": "01d" 1029 | } 1030 | ], 1031 | "clouds": { 1032 | "all": 0 1033 | }, 1034 | "wind": { 1035 | "speed": 5.32, 1036 | "deg": 204 1037 | }, 1038 | "sys": { 1039 | "pod": "d" 1040 | }, 1041 | "dt_txt": "2016-04-18 12:00:00" 1042 | }, 1043 | { 1044 | "dt": 1460991600, 1045 | "main": { 1046 | "temp": 290.314, 1047 | "temp_min": 290.314, 1048 | "temp_max": 290.314, 1049 | "pressure": 1021.73, 1050 | "sea_level": 1030.01, 1051 | "grnd_level": 1021.73, 1052 | "humidity": 79, 1053 | "temp_kf": 0 1054 | }, 1055 | "weather": [ 1056 | { 1057 | "id": 800, 1058 | "main": "Clear", 1059 | "description": "clear sky", 1060 | "icon": "01d" 1061 | } 1062 | ], 1063 | "clouds": { 1064 | "all": 0 1065 | }, 1066 | "wind": { 1067 | "speed": 4.46, 1068 | "deg": 206.504 1069 | }, 1070 | "sys": { 1071 | "pod": "d" 1072 | }, 1073 | "dt_txt": "2016-04-18 15:00:00" 1074 | }, 1075 | { 1076 | "dt": 1461002400, 1077 | "main": { 1078 | "temp": 289.557, 1079 | "temp_min": 289.557, 1080 | "temp_max": 289.557, 1081 | "pressure": 1021.67, 1082 | "sea_level": 1030.13, 1083 | "grnd_level": 1021.67, 1084 | "humidity": 82, 1085 | "temp_kf": 0 1086 | }, 1087 | "weather": [ 1088 | { 1089 | "id": 800, 1090 | "main": "Clear", 1091 | "description": "clear sky", 1092 | "icon": "02d" 1093 | } 1094 | ], 1095 | "clouds": { 1096 | "all": 8 1097 | }, 1098 | "wind": { 1099 | "speed": 5.11, 1100 | "deg": 211.501 1101 | }, 1102 | "sys": { 1103 | "pod": "d" 1104 | }, 1105 | "dt_txt": "2016-04-18 18:00:00" 1106 | }, 1107 | { 1108 | "dt": 1461013200, 1109 | "main": { 1110 | "temp": 287.28, 1111 | "temp_min": 287.28, 1112 | "temp_max": 287.28, 1113 | "pressure": 1023.31, 1114 | "sea_level": 1032.04, 1115 | "grnd_level": 1023.31, 1116 | "humidity": 95, 1117 | "temp_kf": 0 1118 | }, 1119 | "weather": [ 1120 | { 1121 | "id": 800, 1122 | "main": "Clear", 1123 | "description": "clear sky", 1124 | "icon": "01n" 1125 | } 1126 | ], 1127 | "clouds": { 1128 | "all": 0 1129 | }, 1130 | "wind": { 1131 | "speed": 5.71, 1132 | "deg": 223.001 1133 | }, 1134 | "sys": { 1135 | "pod": "n" 1136 | }, 1137 | "dt_txt": "2016-04-18 21:00:00" 1138 | }, 1139 | { 1140 | "dt": 1461024000, 1141 | "main": { 1142 | "temp": 286.566, 1143 | "temp_min": 286.566, 1144 | "temp_max": 286.566, 1145 | "pressure": 1024.09, 1146 | "sea_level": 1032.7, 1147 | "grnd_level": 1024.09, 1148 | "humidity": 100, 1149 | "temp_kf": 0 1150 | }, 1151 | "weather": [ 1152 | { 1153 | "id": 800, 1154 | "main": "Clear", 1155 | "description": "clear sky", 1156 | "icon": "01n" 1157 | } 1158 | ], 1159 | "clouds": { 1160 | "all": 0 1161 | }, 1162 | "wind": { 1163 | "speed": 3.4, 1164 | "deg": 244.509 1165 | }, 1166 | "sys": { 1167 | "pod": "n" 1168 | }, 1169 | "dt_txt": "2016-04-19 00:00:00" 1170 | }, 1171 | { 1172 | "dt": 1461034800, 1173 | "main": { 1174 | "temp": 285.747, 1175 | "temp_min": 285.747, 1176 | "temp_max": 285.747, 1177 | "pressure": 1024.11, 1178 | "sea_level": 1032.63, 1179 | "grnd_level": 1024.11, 1180 | "humidity": 100, 1181 | "temp_kf": 0 1182 | }, 1183 | "weather": [ 1184 | { 1185 | "id": 800, 1186 | "main": "Clear", 1187 | "description": "clear sky", 1188 | "icon": "01n" 1189 | } 1190 | ], 1191 | "clouds": { 1192 | "all": 0 1193 | }, 1194 | "wind": { 1195 | "speed": 2.23, 1196 | "deg": 36.5023 1197 | }, 1198 | "sys": { 1199 | "pod": "n" 1200 | }, 1201 | "dt_txt": "2016-04-19 03:00:00" 1202 | }, 1203 | { 1204 | "dt": 1461045600, 1205 | "main": { 1206 | "temp": 286.219, 1207 | "temp_min": 286.219, 1208 | "temp_max": 286.219, 1209 | "pressure": 1024.12, 1210 | "sea_level": 1032.66, 1211 | "grnd_level": 1024.12, 1212 | "humidity": 100, 1213 | "temp_kf": 0 1214 | }, 1215 | "weather": [ 1216 | { 1217 | "id": 802, 1218 | "main": "Clouds", 1219 | "description": "scattered clouds", 1220 | "icon": "03d" 1221 | } 1222 | ], 1223 | "clouds": { 1224 | "all": 44 1225 | }, 1226 | "wind": { 1227 | "speed": 3.22, 1228 | "deg": 38.0002 1229 | }, 1230 | "sys": { 1231 | "pod": "d" 1232 | }, 1233 | "dt_txt": "2016-04-19 06:00:00" 1234 | }, 1235 | { 1236 | "dt": 1461056400, 1237 | "main": { 1238 | "temp": 289.678, 1239 | "temp_min": 289.678, 1240 | "temp_max": 289.678, 1241 | "pressure": 1024.97, 1242 | "sea_level": 1033.35, 1243 | "grnd_level": 1024.97, 1244 | "humidity": 83, 1245 | "temp_kf": 0 1246 | }, 1247 | "weather": [ 1248 | { 1249 | "id": 801, 1250 | "main": "Clouds", 1251 | "description": "few clouds", 1252 | "icon": "02d" 1253 | } 1254 | ], 1255 | "clouds": { 1256 | "all": 24 1257 | }, 1258 | "wind": { 1259 | "speed": 2.56, 1260 | "deg": 49.0002 1261 | }, 1262 | "sys": { 1263 | "pod": "d" 1264 | }, 1265 | "dt_txt": "2016-04-19 09:00:00" 1266 | }, 1267 | { 1268 | "dt": 1461067200, 1269 | "main": { 1270 | "temp": 290.422, 1271 | "temp_min": 290.422, 1272 | "temp_max": 290.422, 1273 | "pressure": 1024.44, 1274 | "sea_level": 1032.83, 1275 | "grnd_level": 1024.44, 1276 | "humidity": 79, 1277 | "temp_kf": 0 1278 | }, 1279 | "weather": [ 1280 | { 1281 | "id": 801, 1282 | "main": "Clouds", 1283 | "description": "few clouds", 1284 | "icon": "02d" 1285 | } 1286 | ], 1287 | "clouds": { 1288 | "all": 24 1289 | }, 1290 | "wind": { 1291 | "speed": 5.46, 1292 | "deg": 79.0035 1293 | }, 1294 | "sys": { 1295 | "pod": "d" 1296 | }, 1297 | "dt_txt": "2016-04-19 12:00:00" 1298 | } 1299 | ] 1300 | } 1301 | -------------------------------------------------------------------------------- /src/test/resources/com/example/weather/integration/ows/weather-barcelona.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": 2.16, 4 | "lat": 41.39 5 | }, 6 | "weather": [ 7 | { 8 | "id": 800, 9 | "main": "Clear", 10 | "description": "clear sky", 11 | "icon": "01d" 12 | } 13 | ], 14 | "base": "cmc stations", 15 | "main": { 16 | "temp": 286.72, 17 | "pressure": 1012, 18 | "humidity": 62, 19 | "temp_min": 282.59, 20 | "temp_max": 290.37 21 | }, 22 | "wind": { 23 | "speed": 5.1, 24 | "deg": 310 25 | }, 26 | "clouds": { 27 | "all": 0 28 | }, 29 | "dt": 1461484642, 30 | "sys": { 31 | "type": 1, 32 | "id": 5470, 33 | "message": 0.0105, 34 | "country": "ES", 35 | "sunrise": 1461473841, 36 | "sunset": 1461523325 37 | }, 38 | "id": 6544100, 39 | "name": "Barcelona", 40 | "cod": 200 41 | } -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------