Real-Time Weather Dashboard: Java Spring Boot Application with External API Integration

Introduction

A Weather Dashboard application that displays current weather conditions, forecasts, and historical data by integrating with external weather APIs. This comprehensive application demonstrates REST API consumption, data caching, error handling, and modern web UI development in Java.

Features

  • Current Weather - Real-time weather conditions for any location
  • 5-Day Forecast - Extended weather predictions
  • Historical Data - Past weather information
  • Location Search - City, coordinates, or auto-detection
  • Multiple Data Sources - Fallback API support
  • Responsive UI - Mobile-friendly dashboard
  • Caching - Performance optimization
  • Error Handling - Graceful degradation

Architecture

Weather Dashboard Architecture:
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Frontend      │◄──►│   Spring Boot    │◄──►│  Weather APIs   │
│   (Thymeleaf)   │    │   Application    │    │ (OpenWeather,   │
│                 │    │                  │    │  WeatherAPI)    │
└─────────────────┘    └──────────────────┘    └─────────────────┘
│
▼
┌─────────────────┐
│   Cache & DB    │
│   (Redis/DB)    │
└─────────────────┘

Dependencies

Maven Dependencies

<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- External APIs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Development -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Configuration

1. Application Properties

# application.yml
spring:
application:
name: weather-dashboard
# Thymeleaf Configuration
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
# Cache Configuration
cache:
type: redis
redis:
time-to-live: 300000 # 5 minutes
# Redis Configuration
data:
redis:
host: localhost
port: 6379
password: 
timeout: 2000
# Jackson Configuration
jackson:
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
# Weather API Configuration
weather:
api:
# OpenWeatherMap Configuration
openweather:
base-url: https://api.openweathermap.org/data/2.5
api-key: ${OPENWEATHER_API_KEY:demo_key}
timeout: 5000
retry:
max-attempts: 3
backoff-delay: 1000
# WeatherAPI.com Configuration (Fallback)
weatherapi:
base-url: http://api.weatherapi.com/v1
api-key: ${WEATHERAPI_KEY:demo_key}
timeout: 5000
# Primary provider (openweather or weatherapi)
primary-provider: openweather
# Application Settings
cache:
enabled: true
ttl-minutes: 10
features:
forecast-enabled: true
historical-enabled: false
location-autodetect: true
# Server Configuration
server:
port: 8080
servlet:
context-path: /weather

2. Cache Configuration

package com.weather.dashboard.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
}
}

Domain Models

1. Weather Data Models

package com.weather.dashboard.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public class WeatherData {
private Coordinates coord;
private List<Weather> weather;
private String base;
private Main main;
private Integer visibility;
private Wind wind;
private Clouds clouds;
private Rain rain;
private Snow snow;
private Long dt;
private Sys sys;
private Integer timezone;
private Long id;
private String name;
private Integer cod;
// Getters and Setters
public Coordinates getCoord() { return coord; }
public void setCoord(Coordinates coord) { this.coord = coord; }
public List<Weather> getWeather() { return weather; }
public void setWeather(List<Weather> weather) { this.weather = weather; }
public String getBase() { return base; }
public void setBase(String base) { this.base = base; }
public Main getMain() { return main; }
public void setMain(Main main) { this.main = main; }
public Integer getVisibility() { return visibility; }
public void setVisibility(Integer visibility) { this.visibility = visibility; }
public Wind getWind() { return wind; }
public void setWind(Wind wind) { this.wind = wind; }
public Clouds getClouds() { return clouds; }
public void setClouds(Clouds clouds) { this.clouds = clouds; }
public Rain getRain() { return rain; }
public void setRain(Rain rain) { this.rain = rain; }
public Snow getSnow() { return snow; }
public void setSnow(Snow snow) { this.snow = snow; }
public Long getDt() { return dt; }
public void setDt(Long dt) { this.dt = dt; }
public Sys getSys() { return sys; }
public void setSys(Sys sys) { this.sys = sys; }
public Integer getTimezone() { return timezone; }
public void setTimezone(Integer timezone) { this.timezone = timezone; }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getCod() { return cod; }
public void setCod(Integer cod) { this.cod = cod; }
// Helper methods
public LocalDateTime getTimestamp() {
return dt != null ? LocalDateTime.ofEpochSecond(dt, 0, java.time.ZoneOffset.UTC) : null;
}
public String getWeatherDescription() {
return weather != null && !weather.isEmpty() ? weather.get(0).getDescription() : null;
}
public String getWeatherIcon() {
return weather != null && !weather.isEmpty() ? weather.get(0).getIcon() : null;
}
// Inner classes
public static class Coordinates {
private Double lon;
private Double lat;
public Double getLon() { return lon; }
public void setLon(Double lon) { this.lon = lon; }
public Double getLat() { return lat; }
public void setLat(Double lat) { this.lat = lat; }
}
public static class Weather {
private Integer id;
private String main;
private String description;
private String icon;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getMain() { return main; }
public void setMain(String main) { this.main = main; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getIcon() { return icon; }
public void setIcon(String icon) { this.icon = icon; }
}
public static class Main {
private Double temp;
private Double feelsLike;
private Double tempMin;
private Double tempMax;
private Integer pressure;
private Integer humidity;
private Integer seaLevel;
private Integer grndLevel;
@JsonProperty("temp")
public Double getTemp() { return temp; }
public void setTemp(Double temp) { this.temp = temp; }
@JsonProperty("feels_like")
public Double getFeelsLike() { return feelsLike; }
public void setFeelsLike(Double feelsLike) { this.feelsLike = feelsLike; }
@JsonProperty("temp_min")
public Double getTempMin() { return tempMin; }
public void setTempMin(Double tempMin) { this.tempMin = tempMin; }
@JsonProperty("temp_max")
public Double getTempMax() { return tempMax; }
public void setTempMax(Double tempMax) { this.tempMax = tempMax; }
public Integer getPressure() { return pressure; }
public void setPressure(Integer pressure) { this.pressure = pressure; }
public Integer getHumidity() { return humidity; }
public void setHumidity(Integer humidity) { this.humidity = humidity; }
@JsonProperty("sea_level")
public Integer getSeaLevel() { return seaLevel; }
public void setSeaLevel(Integer seaLevel) { this.seaLevel = seaLevel; }
@JsonProperty("grnd_level")
public Integer getGrndLevel() { return grndLevel; }
public void setGrndLevel(Integer grndLevel) { this.grndLevel = grndLevel; }
}
public static class Wind {
private Double speed;
private Integer deg;
private Double gust;
public Double getSpeed() { return speed; }
public void setSpeed(Double speed) { this.speed = speed; }
public Integer getDeg() { return deg; }
public void setDeg(Integer deg) { this.deg = deg; }
public Double getGust() { return gust; }
public void setGust(Double gust) { this.gust = gust; }
}
public static class Clouds {
private Integer all;
public Integer getAll() { return all; }
public void setAll(Integer all) { this.all = all; }
}
public static class Rain {
@JsonProperty("1h")
private Double oneHour;
@JsonProperty("3h")
private Double threeHours;
public Double getOneHour() { return oneHour; }
public void setOneHour(Double oneHour) { this.oneHour = oneHour; }
public Double getThreeHours() { return threeHours; }
public void setThreeHours(Double threeHours) { this.threeHours = threeHours; }
}
public static class Snow {
@JsonProperty("1h")
private Double oneHour;
@JsonProperty("3h")
private Double threeHours;
public Double getOneHour() { return oneHour; }
public void setOneHour(Double oneHour) { this.oneHour = oneHour; }
public Double getThreeHours() { return threeHours; }
public void setThreeHours(Double threeHours) { this.threeHours = threeHours; }
}
public static class Sys {
private Integer type;
private Long id;
private String country;
private Long sunrise;
private Long sunset;
public Integer getType() { return type; }
public void setType(Integer type) { this.type = type; }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public Long getSunrise() { return sunrise; }
public void setSunrise(Long sunrise) { this.sunrise = sunrise; }
public Long getSunset() { return sunset; }
public void setSunset(Long sunset) { this.sunset = sunset; }
}
}

2. Forecast Models

package com.weather.dashboard.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.List;
public class WeatherForecast {
private String cod;
private Integer message;
private Integer cnt;
private List<ForecastItem> list;
private City city;
// Getters and Setters
public String getCod() { return cod; }
public void setCod(String cod) { this.cod = cod; }
public Integer getMessage() { return message; }
public void setMessage(Integer message) { this.message = message; }
public Integer getCnt() { return cnt; }
public void setCnt(Integer cnt) { this.cnt = cnt; }
public List<ForecastItem> getList() { return list; }
public void setList(List<ForecastItem> list) { this.list = list; }
public City getCity() { return city; }
public void setCity(City city) { this.city = city; }
public static class ForecastItem {
private Long dt;
private Main main;
private List<Weather> weather;
private Clouds clouds;
private Wind wind;
private Integer visibility;
private Double pop;
private Rain rain;
private Snow snow;
private Sys sys;
private String dtTxt;
public Long getDt() { return dt; }
public void setDt(Long dt) { this.dt = dt; }
public Main getMain() { return main; }
public void setMain(Main main) { this.main = main; }
public List<Weather> getWeather() { return weather; }
public void setWeather(List<Weather> weather) { this.weather = weather; }
public Clouds getClouds() { return clouds; }
public void setClouds(Clouds clouds) { this.clouds = clouds; }
public Wind getWind() { return wind; }
public void setWind(Wind wind) { this.wind = wind; }
public Integer getVisibility() { return visibility; }
public void setVisibility(Integer visibility) { this.visibility = visibility; }
public Double getPop() { return pop; }
public void setPop(Double pop) { this.pop = pop; }
public Rain getRain() { return rain; }
public void setRain(Rain rain) { this.rain = rain; }
public Snow getSnow() { return snow; }
public void setSnow(Snow snow) { this.snow = snow; }
public Sys getSys() { return sys; }
public void setSys(Sys sys) { this.sys = sys; }
@JsonProperty("dt_txt")
public String getDtTxt() { return dtTxt; }
public void setDtTxt(String dtTxt) { this.dtTxt = dtTxt; }
public LocalDateTime getDateTime() {
return dt != null ? LocalDateTime.ofEpochSecond(dt, 0, java.time.ZoneOffset.UTC) : null;
}
}
public static class City {
private Long id;
private String name;
private Coordinates coord;
private String country;
private Integer population;
private Integer timezone;
private Long sunrise;
private Long sunset;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Coordinates getCoord() { return coord; }
public void setCoord(Coordinates coord) { this.coord = coord; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public Integer getPopulation() { return population; }
public void setPopulation(Integer population) { this.population = population; }
public Integer getTimezone() { return timezone; }
public void setTimezone(Integer timezone) { this.timezone = timezone; }
public Long getSunrise() { return sunrise; }
public void setSunrise(Long sunrise) { this.sunrise = sunrise; }
public Long getSunset() { return sunset; }
public void setSunset(Long sunset) { this.sunset = sunset; }
}
// Reuse inner classes from WeatherData
public static class Coordinates extends WeatherData.Coordinates {}
public static class Weather extends WeatherData.Weather {}
public static class Main extends WeatherData.Main {}
public static class Wind extends WeatherData.Wind {}
public static class Clouds extends WeatherData.Clouds {}
public static class Rain extends WeatherData.Rain {}
public static class Snow extends WeatherData.Snow {}
public static class Sys extends WeatherData.Sys {}
}

3. API Response Models

package com.weather.dashboard.model;
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
// Constructors
public ApiResponse() {}
public ApiResponse(boolean success, T data) {
this.success = success;
this.data = data;
}
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public ApiResponse(boolean success, String error) {
this.success = success;
this.error = error;
}
// Static factory methods
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String error) {
return new ApiResponse<>(false, error);
}
// Getters and Setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
}
package com.weather.dashboard.model;
import java.util.List;
public class WeatherDashboard {
private WeatherData currentWeather;
private WeatherForecast forecast;
private Location location;
private List<WeatherAlert> alerts;
// Constructors
public WeatherDashboard() {}
public WeatherDashboard(WeatherData currentWeather, WeatherForecast forecast, Location location) {
this.currentWeather = currentWeather;
this.forecast = forecast;
this.location = location;
}
// Getters and Setters
public WeatherData getCurrentWeather() { return currentWeather; }
public void setCurrentWeather(WeatherData currentWeather) { this.currentWeather = currentWeather; }
public WeatherForecast getForecast() { return forecast; }
public void setForecast(WeatherForecast forecast) { this.forecast = forecast; }
public Location getLocation() { return location; }
public void setLocation(Location location) { this.location = location; }
public List<WeatherAlert> getAlerts() { return alerts; }
public void setAlerts(List<WeatherAlert> alerts) { this.alerts = alerts; }
}
package com.weather.dashboard.model;
public class Location {
private String name;
private String country;
private Double lat;
private Double lon;
private String timezone;
// Constructors
public Location() {}
public Location(String name, String country, Double lat, Double lon) {
this.name = name;
this.country = country;
this.lat = lat;
this.lon = lon;
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public Double getLat() { return lat; }
public void setLat(Double lat) { this.lat = lat; }
public Double getLon() { return lon; }
public void setLon(Double lon) { this.lon = lon; }
public String getTimezone() { return timezone; }
public void setTimezone(String timezone) { this.timezone = timezone; }
public String getDisplayName() {
return name + (country != null ? ", " + country : "");
}
}
package com.weather.dashboard.model;
import java.time.LocalDateTime;
public class WeatherAlert {
private String senderName;
private String event;
private LocalDateTime start;
private LocalDateTime end;
private String description;
private String severity;
// Getters and Setters
public String getSenderName() { return senderName; }
public void setSenderName(String senderName) { this.senderName = senderName; }
public String getEvent() { return event; }
public void setEvent(String event) { this.event = event; }
public LocalDateTime getStart() { return start; }
public void setStart(LocalDateTime start) { this.start = start; }
public LocalDateTime getEnd() { return end; }
public void setEnd(LocalDateTime end) { this.end = end; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
}

Weather Service Implementation

1. Weather Service Interface

package com.weather.dashboard.service;
import com.weather.dashboard.model.WeatherData;
import com.weather.dashboard.model.WeatherForecast;
public interface WeatherService {
WeatherData getCurrentWeather(String location);
WeatherData getCurrentWeather(Double lat, Double lon);
WeatherForecast getWeatherForecast(String location);
WeatherForecast getWeatherForecast(Double lat, Double lon);
boolean isServiceAvailable();
String getServiceName();
}

2. OpenWeatherMap Service

package com.weather.dashboard.service.impl;
import com.weather.dashboard.model.WeatherData;
import com.weather.dashboard.model.WeatherForecast;
import com.weather.dashboard.service.WeatherService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Optional;
@Service("openWeatherService")
public class OpenWeatherMapService implements WeatherService {
private final RestTemplate restTemplate;
@Value("${weather.api.openweather.base-url}")
private String baseUrl;
@Value("${weather.api.openweather.api-key}")
private String apiKey;
@Value("${weather.api.openweather.timeout:5000}")
private int timeout;
public OpenWeatherMapService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
@Cacheable(value = "currentWeather", key = "#location")
public WeatherData getCurrentWeather(String location) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/weather")
.queryParam("q", location)
.queryParam("appid", apiKey)
.queryParam("units", "metric")
.queryParam("lang", "en")
.toUriString();
try {
ResponseEntity<WeatherData> response = restTemplate.getForEntity(url, WeatherData.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
}
} catch (Exception e) {
throw new WeatherServiceException("Failed to fetch current weather for: " + location, e);
}
throw new WeatherServiceException("No weather data found for: " + location);
}
@Override
@Cacheable(value = "currentWeather", key = "T(java.util.Objects).hash(#lat, #lon)")
public WeatherData getCurrentWeather(Double lat, Double lon) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/weather")
.queryParam("lat", lat)
.queryParam("lon", lon)
.queryParam("appid", apiKey)
.queryParam("units", "metric")
.queryParam("lang", "en")
.toUriString();
try {
ResponseEntity<WeatherData> response = restTemplate.getForEntity(url, WeatherData.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
}
} catch (Exception e) {
throw new WeatherServiceException(
String.format("Failed to fetch current weather for coordinates: lat=%s, lon=%s", lat, lon), e);
}
throw new WeatherServiceException(
String.format("No weather data found for coordinates: lat=%s, lon=%s", lat, lon));
}
@Override
@Cacheable(value = "weatherForecast", key = "#location")
public WeatherForecast getWeatherForecast(String location) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/forecast")
.queryParam("q", location)
.queryParam("appid", apiKey)
.queryParam("units", "metric")
.queryParam("lang", "en")
.queryParam("cnt", 40) // 5 days * 8 intervals per day
.toUriString();
try {
ResponseEntity<WeatherForecast> response = restTemplate.getForEntity(url, WeatherForecast.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
}
} catch (Exception e) {
throw new WeatherServiceException("Failed to fetch weather forecast for: " + location, e);
}
throw new WeatherServiceException("No forecast data found for: " + location);
}
@Override
@Cacheable(value = "weatherForecast", key = "T(java.util.Objects).hash(#lat, #lon)")
public WeatherForecast getWeatherForecast(Double lat, Double lon) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/forecast")
.queryParam("lat", lat)
.queryParam("lon", lon)
.queryParam("appid", apiKey)
.queryParam("units", "metric")
.queryParam("lang", "en")
.queryParam("cnt", 40)
.toUriString();
try {
ResponseEntity<WeatherForecast> response = restTemplate.getForEntity(url, WeatherForecast.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
}
} catch (Exception e) {
throw new WeatherServiceException(
String.format("Failed to fetch weather forecast for coordinates: lat=%s, lon=%s", lat, lon), e);
}
throw new WeatherServiceException(
String.format("No forecast data found for coordinates: lat=%s, lon=%s", lat, lon));
}
@Override
public boolean isServiceAvailable() {
try {
// Try to fetch weather for a known location (London)
getCurrentWeather("London");
return true;
} catch (Exception e) {
return false;
}
}
@Override
public String getServiceName() {
return "OpenWeatherMap";
}
public static class WeatherServiceException extends RuntimeException {
public WeatherServiceException(String message) {
super(message);
}
public WeatherServiceException(String message, Throwable cause) {
super(message, cause);
}
}
}

3. WeatherAPI.com Service (Fallback)

package com.weather.dashboard.service.impl;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.weather.dashboard.model.WeatherData;
import com.weather.dashboard.model.WeatherForecast;
import com.weather.dashboard.service.WeatherService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Service("weatherApiService")
public class WeatherApiService implements WeatherService {
private final RestTemplate restTemplate;
@Value("${weather.api.weatherapi.base-url}")
private String baseUrl;
@Value("${weather.api.weatherapi.api-key}")
private String apiKey;
public WeatherApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
@Cacheable(value = "currentWeather", key = "#location")
public WeatherData getCurrentWeather(String location) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/current.json")
.queryParam("key", apiKey)
.queryParam("q", location)
.queryParam("aqi", "no")
.toUriString();
try {
ResponseEntity<WeatherApiResponse> response = restTemplate.getForEntity(url, WeatherApiResponse.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return convertToStandardFormat(response.getBody());
}
} catch (Exception e) {
throw new WeatherServiceException("Failed to fetch current weather from WeatherAPI for: " + location, e);
}
throw new WeatherServiceException("No weather data found from WeatherAPI for: " + location);
}
@Override
public WeatherData getCurrentWeather(Double lat, Double lon) {
String coordinates = lat + "," + lon;
return getCurrentWeather(coordinates);
}
@Override
@Cacheable(value = "weatherForecast", key = "#location")
public WeatherForecast getWeatherForecast(String location) {
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/forecast.json")
.queryParam("key", apiKey)
.queryParam("q", location)
.queryParam("days", 5)
.queryParam("aqi", "no")
.queryParam("alerts", "no")
.toUriString();
try {
// Implementation would convert WeatherAPI response to standard format
// Similar to current weather conversion
throw new UnsupportedOperationException("WeatherAPI forecast not implemented in this example");
} catch (Exception e) {
throw new WeatherServiceException("Failed to fetch forecast from WeatherAPI for: " + location, e);
}
}
@Override
public WeatherForecast getWeatherForecast(Double lat, Double lon) {
String coordinates = lat + "," + lon;
return getWeatherForecast(coordinates);
}
@Override
public boolean isServiceAvailable() {
try {
getCurrentWeather("London");
return true;
} catch (Exception e) {
return false;
}
}
@Override
public String getServiceName() {
return "WeatherAPI.com";
}
private WeatherData convertToStandardFormat(WeatherApiResponse apiResponse) {
WeatherData weatherData = new WeatherData();
// Convert WeatherAPI response to standard WeatherData format
if (apiResponse.location != null) {
weatherData.setName(apiResponse.location.name);
WeatherData.Coordinates coords = new WeatherData.Coordinates();
coords.setLat(apiResponse.location.lat);
coords.setLon(apiResponse.location.lon);
weatherData.setCoord(coords);
}
if (apiResponse.current != null) {
WeatherData.Main main = new WeatherData.Main();
main.setTemp(apiResponse.current.tempC);
main.setFeelsLike(apiResponse.current.feelsLikeC);
main.setHumidity(apiResponse.current.humidity);
main.setPressure(apiResponse.current.pressureMb);
weatherData.setMain(main);
WeatherData.Weather weather = new WeatherData.Weather();
weather.setDescription(apiResponse.current.condition.text);
weather.setIcon(apiResponse.current.condition.icon);
weatherData.setWeather(java.util.List.of(weather));
WeatherData.Wind wind = new WeatherData.Wind();
wind.setSpeed(apiResponse.current.windKph / 3.6); // Convert kph to m/s
wind.setDeg(apiResponse.current.windDegree);
weatherData.setWind(wind);
}
return weatherData;
}
// WeatherAPI.com specific response classes
private static class WeatherApiResponse {
public Location location;
public Current current;
}
private static class Location {
public String name;
public String region;
public String country;
public Double lat;
public Double lon;
public String localtime;
}
private static class Current {
@JsonProperty("temp_c")
public Double tempC;
@JsonProperty("feelslike_c")
public Double feelsLikeC;
public Integer humidity;
@JsonProperty("pressure_mb")
public Integer pressureMb;
public Condition condition;
@JsonProperty("wind_kph")
public Double windKph;
@JsonProperty("wind_degree")
public Integer windDegree;
}
private static class Condition {
public String text;
public String icon;
}
}

4. Aggregated Weather Service

package com.weather.dashboard.service;
import com.weather.dashboard.model.WeatherData;
import com.weather.dashboard.model.WeatherForecast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class AggregatedWeatherService {
private static final Logger logger = LoggerFactory.getLogger(AggregatedWeatherService.class);
private final List<WeatherService> weatherServices;
private final String primaryProvider;
public AggregatedWeatherService(
@Qualifier("openWeatherService") WeatherService openWeatherService,
@Qualifier("weatherApiService") WeatherService weatherApiService,
@Value("${weather.api.primary-provider:openweather}") String primaryProvider) {
this.weatherServices = new ArrayList<>();
this.weatherServices.add(openWeatherService);
this.weatherServices.add(weatherApiService);
this.primaryProvider = primaryProvider;
}
public WeatherData getCurrentWeather(String location) {
for (WeatherService service : getServicesInPriorityOrder()) {
try {
if (service.isServiceAvailable()) {
logger.info("Using {} for current weather data", service.getServiceName());
return service.getCurrentWeather(location);
}
} catch (Exception e) {
logger.warn("Failed to get current weather from {}: {}", 
service.getServiceName(), e.getMessage());
}
}
throw new RuntimeException("All weather services are unavailable");
}
public WeatherData getCurrentWeather(Double lat, Double lon) {
for (WeatherService service : getServicesInPriorityOrder()) {
try {
if (service.isServiceAvailable()) {
logger.info("Using {} for current weather data", service.getServiceName());
return service.getCurrentWeather(lat, lon);
}
} catch (Exception e) {
logger.warn("Failed to get current weather from {}: {}", 
service.getServiceName(), e.getMessage());
}
}
throw new RuntimeException("All weather services are unavailable");
}
public WeatherForecast getWeatherForecast(String location) {
for (WeatherService service : getServicesInPriorityOrder()) {
try {
if (service.isServiceAvailable()) {
logger.info("Using {} for weather forecast", service.getServiceName());
return service.getWeatherForecast(location);
}
} catch (Exception e) {
logger.warn("Failed to get forecast from {}: {}", 
service.getServiceName(), e.getMessage());
}
}
throw new RuntimeException("All weather services are unavailable for forecast");
}
public WeatherForecast getWeatherForecast(Double lat, Double lon) {
for (WeatherService service : getServicesInPriorityOrder()) {
try {
if (service.isServiceAvailable()) {
logger.info("Using {} for weather forecast", service.getServiceName());
return service.getWeatherForecast(lat, lon);
}
} catch (Exception e) {
logger.warn("Failed to get forecast from {}: {}", 
service.getServiceName(), e.getMessage());
}
}
throw new RuntimeException("All weather services are unavailable for forecast");
}
private List<WeatherService> getServicesInPriorityOrder() {
List<WeatherService> orderedServices = new ArrayList<>();
// Add primary provider first
for (WeatherService service : weatherServices) {
if (service.getServiceName().toLowerCase().contains(primaryProvider.toLowerCase())) {
orderedServices.add(0, service);
} else {
orderedServices.add(service);
}
}
return orderedServices;
}
public List<String> getAvailableServices() {
List<String> available = new ArrayList<>();
for (WeatherService service : weatherServices) {
if (service.isServiceAvailable()) {
available.add(service.getServiceName());
}
}
return available;
}
}

REST Controller

1. Weather Dashboard Controller

package com.weather.dashboard.controller;
import com.weather.dashboard.model.*;
import com.weather.dashboard.service.AggregatedWeatherService;
import com.weather.dashboard.service.LocationService;
import jakarta.validation.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/dashboard")
@Validated
public class WeatherDashboardController {
private static final Logger logger = LoggerFactory.getLogger(WeatherDashboardController.class);
private final AggregatedWeatherService weatherService;
private final LocationService locationService;
public WeatherDashboardController(AggregatedWeatherService weatherService, 
LocationService locationService) {
this.weatherService = weatherService;
this.locationService = locationService;
}
@GetMapping
public String showDashboard(@RequestParam(required = false) String location, Model model) {
try {
// Default location if none provided
if (location == null || location.trim().isEmpty()) {
location = "London";
}
WeatherData currentWeather = weatherService.getCurrentWeather(location);
WeatherForecast forecast = weatherService.getWeatherForecast(location);
Location locationInfo = new Location(
currentWeather.getName(),
currentWeather.getSys() != null ? currentWeather.getSys().getCountry() : null,
currentWeather.getCoord() != null ? currentWeather.getCoord().getLat() : null,
currentWeather.getCoord() != null ? currentWeather.getCoord().getLon() : null
);
WeatherDashboard dashboard = new WeatherDashboard(currentWeather, forecast, locationInfo);
model.addAttribute("dashboard", dashboard);
model.addAttribute("availableServices", weatherService.getAvailableServices());
model.addAttribute("searchLocation", location);
} catch (Exception e) {
logger.error("Failed to load weather dashboard for location: {}", location, e);
model.addAttribute("error", "Failed to load weather data: " + e.getMessage());
}
return "dashboard";
}
@GetMapping("/search")
public String searchLocation(@RequestParam @NotBlank String q, Model model) {
try {
List<Location> locations = locationService.searchLocations(q);
model.addAttribute("locations", locations);
model.addAttribute("searchQuery", q);
} catch (Exception e) {
logger.error("Failed to search locations for query: {}", q, e);
model.addAttribute("error", "Location search failed: " + e.getMessage());
}
return "location-results";
}
// REST API endpoints for AJAX calls
@GetMapping("/api/current")
@ResponseBody
public ResponseEntity<ApiResponse<WeatherData>> getCurrentWeatherApi(
@RequestParam String location) {
try {
WeatherData weatherData = weatherService.getCurrentWeather(location);
return ResponseEntity.ok(ApiResponse.success(weatherData));
} catch (Exception e) {
logger.error("API error for current weather: {}", location, e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("Failed to get current weather: " + e.getMessage()));
}
}
@GetMapping("/api/forecast")
@ResponseBody
public ResponseEntity<ApiResponse<WeatherForecast>> getForecastApi(
@RequestParam String location) {
try {
WeatherForecast forecast = weatherService.getWeatherForecast(location);
return ResponseEntity.ok(ApiResponse.success(forecast));
} catch (Exception e) {
logger.error("API error for forecast: {}", location, e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("Failed to get forecast: " + e.getMessage()));
}
}
@GetMapping("/api/location")
@ResponseBody
public ResponseEntity<ApiResponse<Location>> getLocationByCoords(
@RequestParam Double lat, @RequestParam Double lon) {
try {
WeatherData weatherData = weatherService.getCurrentWeather(lat, lon);
Location location = new Location(
weatherData.getName(),
weatherData.getSys() != null ? weatherData.getSys().getCountry() : null,
lat,
lon
);
return ResponseEntity.ok(ApiResponse.success(location));
} catch (Exception e) {
logger.error("API error for location by coords: lat={}, lon={}", lat, lon, e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("Failed to get location: " + e.getMessage()));
}
}
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
logger.error("Dashboard error", e);
model.addAttribute("error", "An error occurred: " + e.getMessage());
return "error";
}
}

Location Service

1. Location Service Implementation

package com.weather.dashboard.service;
import com.weather.dashboard.model.Location;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class LocationService {
private final RestTemplate restTemplate;
@Value("${weather.api.openweather.base-url}")
private String openWeatherBaseUrl;
@Value("${weather.api.openweather.api-key}")
private String openWeatherApiKey;
public LocationService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<Location> searchLocations(String query) {
String url = UriComponentsBuilder.fromHttpUrl(openWeatherBaseUrl + "/find")
.queryParam("q", query)
.queryParam("appid", openWeatherApiKey)
.queryParam("type", "like")
.queryParam("mode", "json")
.queryParam("cnt", 10)
.toUriString();
try {
Map response = restTemplate.getForObject(url, Map.class);
return parseLocationResponse(response);
} catch (Exception e) {
throw new RuntimeException("Failed to search locations for: " + query, e);
}
}
@SuppressWarnings("unchecked")
private List<Location> parseLocationResponse(Map response) {
List<Location> locations = new ArrayList<>();
if (response != null && response.containsKey("list")) {
List<Map<String, Object>> locationList = (List<Map<String, Object>>) response.get("list");
for (Map<String, Object> locationData : locationList) {
Location location = new Location();
location.setName((String) locationData.get("name"));
Map<String, Object> sys = (Map<String, Object>) locationData.get("sys");
if (sys != null) {
location.setCountry((String) sys.get("country"));
}
Map<String, Object> coord = (Map<String, Object>) locationData.get("coord");
if (coord != null) {
location.setLat((Double) coord.get("lat"));
location.setLon((Double) coord.get("lon"));
}
locations.add(location);
}
}
return locations;
}
public Location getLocationByCoords(Double lat, Double lon) {
String url = UriComponentsBuilder.fromHttpUrl(openWeatherBaseUrl + "/weather")
.queryParam("lat", lat)
.queryParam("lon", lon)
.queryParam("appid", openWeatherApiKey)
.toUriString();
try {
Map response = restTemplate.getForObject(url, Map.class);
return parseSingleLocation(response);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to get location for coordinates: lat=%s, lon=%s", lat, lon), e);
}
}
@SuppressWarnings("unchecked")
private Location parseSingleLocation(Map response) {
if (response == null) {
return null;
}
Location location = new Location();
location.setName((String) response.get("name"));
Map<String, Object> sys = (Map<String, Object>) response.get("sys");
if (sys != null) {
location.setCountry((String) sys.get("country"));
}
Map<String, Object> coord = (Map<String, Object>) response.get("coord");
if (coord != null) {
location.setLat((Double) coord.get("lat"));
location.setLon((Double) coord.get("lon"));
}
return location;
}
}

Utility Services

1. Weather Data Utility

package com.weather.dashboard.util;
import com.weather.dashboard.model.WeatherData;
public class WeatherUtils {
private WeatherUtils() {
// Utility class
}
public static String getTemperatureColor(double temperature) {
if (temperature < 0) return "#4A90E2";    // Very Cold - Blue
if (temperature < 10) return "#7ED321";   // Cold - Light Green
if (temperature < 20) return "#F5A623";   // Cool - Orange
if (temperature < 30) return "#FF6B6B";   // Warm - Red
return "#D0021B";                         // Hot - Dark Red
}
public static String getWeatherIconUrl(String iconCode) {
return "https://openweathermap.org/img/wn/" + iconCode + "@2x.png";
}
public static String getWindDirection(Integer degrees) {
if (degrees == null) return "N/A";
String[] directions = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", 
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"};
int index = (int) Math.round(degrees / 22.5) % 16;
return directions[index];
}
public static String formatTemperature(Double temp) {
if (temp == null) return "N/A";
return String.format("%.1f°C", temp);
}
public static String formatHumidity(Integer humidity) {
if (humidity == null) return "N/A";
return humidity + "%";
}
public static String formatPressure(Integer pressure) {
if (pressure == null) return "N/A";
return pressure + " hPa";
}
public static String formatWindSpeed(Double speed) {
if (speed == null) return "N/A";
return String.format("%.1f m/s", speed);
}
public static String getWeatherConditionClass(String mainCondition) {
if (mainCondition == null) return "default";
return switch (mainCondition.toLowerCase()) {
case "clear" -> "sunny";
case "clouds" -> "cloudy";
case "rain", "drizzle" -> "rainy";
case "snow" -> "snowy";
case "thunderstorm" -> "stormy";
case "mist", "fog", "haze" -> "foggy";
default -> "default";
};
}
}

Frontend Templates

1. Main Dashboard Template

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Dashboard</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2980b9;
--success-color: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--light-bg: #f8f9fa;
--dark-text: #2c3e50;
}
body {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.weather-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.current-weather {
text-align: center;
padding: 2rem;
}
.temperature {
font-size: 4rem;
font-weight: 300;
color: var(--dark-text);
}
.weather-icon {
width: 120px;
height: 120px;
}
.weather-condition {
font-size: 1.5rem;
color: var(--dark-text);
margin: 1rem 0;
}
.weather-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.detail-card {
background: white;
padding: 1rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.detail-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-color);
}
.detail-label {
font-size: 0.9rem;
color: #666;
margin-top: 0.5rem;
}
.forecast-container {
margin-top: 2rem;
}
.forecast-item {
text-align: center;
padding: 1rem;
border-radius: 12px;
transition: transform 0.2s;
}
.forecast-item:hover {
transform: translateY(-5px);
background: rgba(255, 255, 255, 0.8);
}
.search-box {
max-width: 500px;
margin: 0 auto 2rem;
}
.location-name {
font-size: 2rem;
font-weight: 600;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.last-updated {
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container py-4">
<!-- Header -->
<div class="row mb-4">
<div class="col-12 text-center">
<h1 class="location-name" th:text="${dashboard?.location?.displayName ?: 'Weather Dashboard'}">
Weather Dashboard
</h1>
<div class="last-updated" th:if="${dashboard?.currentWeather?.timestamp}">
Last updated: <span th:text="${#temporals.format(dashboard.currentWeather.timestamp, 'MMM dd, yyyy HH:mm')}"></span>
</div>
</div>
</div>
<!-- Search Box -->
<div class="row mb-4">
<div class="col-12">
<div class="search-box">
<form th:action="@{/dashboard}" method="get" class="d-flex">
<input type="text" 
name="location" 
class="form-control form-control-lg" 
placeholder="Enter city name..."
th:value="${searchLocation ?: ''}"
required>
<button type="submit" class="btn btn-primary btn-lg ms-2">
<i class="fas fa-search"></i> Search
</button>
</form>
</div>
</div>
</div>
<!-- Error Alert -->
<div th:if="${error}" class="row mb-4">
<div class="col-12">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<span th:text="${error}"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
</div>
<!-- Current Weather -->
<div th:if="${dashboard?.currentWeather}" class="row mb-4">
<div class="col-12">
<div class="weather-card current-weather">
<div class="row align-items-center">
<div class="col-md-6">
<div class="temperature" 
th:text="${#numbers.formatDecimal(dashboard.currentWeather.main.temp, 1, 1)} + '°C'">
</div>
<div class="weather-condition" th:text="${dashboard.currentWeather.weatherDescription}">
</div>
<div class="feels-like">
Feels like: <span th:text="${#numbers.formatDecimal(dashboard.currentWeather.main.feelsLike, 1, 1)} + '°C'"></span>
</div>
</div>
<div class="col-md-6">
<img th:src="${@weatherUtils.getWeatherIconUrl(dashboard.currentWeather.weatherIcon)}" 
alt="Weather Icon" 
class="weather-icon">
</div>
</div>
<!-- Weather Details -->
<div class="weather-details">
<div class="detail-card">
<div class="detail-value" th:text="${dashboard.currentWeather.main.humidity} + '%'"></div>
<div class="detail-label">Humidity</div>
</div>
<div class="detail-card">
<div class="detail-value" th:text="${dashboard.currentWeather.main.pressure} + ' hPa'"></div>
<div class="detail-label">Pressure</div>
</div>
<div class="detail-card">
<div class="detail-value" th:text="${#numbers.formatDecimal(dashboard.currentWeather.wind.speed, 1, 1)} + ' m/s'"></div>
<div class="detail-label">Wind Speed</div>
</div>
<div class="detail-card">
<div class="detail-value" th:text="${@weatherUtils.getWindDirection(dashboard.currentWeather.wind.deg)}"></div>
<div class="detail-label">Wind Direction</div>
</div>
<div class="detail-card" th:if="${dashboard.currentWeather.visibility}">
<div class="detail-value" th:text="${#numbers.formatInteger(dashboard.currentWeather.visibility / 1000, 1)} + ' km'"></div>
<div class="detail-label">Visibility</div>
</div>
</div>
</div>
</div>
</div>
<!-- Forecast -->
<div th:if="${dashboard?.forecast}" class="row">
<div class="col-12">
<div class="weather-card forecast-container p-4">
<h3 class="mb-4">5-Day Forecast</h3>
<div class="row">
<div th:each="forecastItem : ${dashboard.forecast.list}" 
th:if="${#strings.substring(forecastItem.dtTxt, 11, 16) == '12:00'}"
class="col-lg-2 col-md-4 col-sm-6 mb-3">
<div class="forecast-item">
<div class="forecast-date" th:text="${#temporals.format(forecastItem.dateTime, 'EEE, MMM dd')}">
</div>
<img th:src="${@weatherUtils.getWeatherIconUrl(forecastItem.weather[0].icon)}" 
alt="Weather Icon" 
class="img-fluid my-2" style="width: 60px; height: 60px;">
<div class="forecast-temp">
<span th:text="${#numbers.formatDecimal(forecastItem.main.tempMax, 1, 1)} + '°'"></span> /
<span th:text="${#numbers.formatDecimal(forecastItem.main.tempMin, 1, 1)} + '°'"></span>
</div>
<div class="forecast-condition" th:text="${forecastItem.weather[0].description}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Service Status -->
<div th:if="${availableServices}" class="row mt-4">
<div class="col-12">
<div class="weather-card p-3">
<small class="text-muted">
Data provided by: 
<span th:each="service, iter : ${availableServices}" 
th:text="${service} + (${iter.last} ? '' : ', ')">
</span>
</small>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JavaScript -->
<script>
// Auto-refresh every 10 minutes
setInterval(() => {
window.location.reload();
}, 10 * 60 * 1000);
// Add smooth animations
document.addEventListener('DOMContentLoaded', function() {
const cards = document.querySelectorAll('.weather-card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'all 0.5s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
});
</script>
</body>
</html>

Configuration Classes

1. Rest Template Configuration

package com.weather.dashboard.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Configuration
public class RestTemplateConfig {
@Value("${weather.api.openweather.timeout:5000}")
private int timeout;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofMillis(timeout));
factory.setReadTimeout(Duration.ofMillis(timeout));
return builder
.requestFactory(() -> factory)
.build();
}
}

2. Utility Bean Configuration

package com.weather.dashboard.config;
import com.weather.dashboard.util.WeatherUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UtilityConfig {
@Bean
public WeatherUtils weatherUtils() {
return new WeatherUtils();
}
}

Application Main Class

package com.weather.dashboard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class WeatherDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(WeatherDashboardApplication.class, args);
}
}

Testing

1. Weather Service Test

package com.weather.dashboard.service;
import com.weather.dashboard.model.WeatherData;
import com.weather.dashboard.service.impl.OpenWeatherMapService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.client.RestTemplate;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OpenWeatherMapServiceTest {
@Mock
private RestTemplate restTemplate;
@InjectMocks
private OpenWeatherMapService weatherService;
@Test
void testGetCurrentWeather_Success() {
// Given
WeatherData expectedWeather = new WeatherData();
expectedWeather.setName("London");
when(restTemplate.getForEntity(anyString(), eq(WeatherData.class)))
.thenReturn(org.springframework.http.ResponseEntity.ok(expectedWeather));
// When
WeatherData result = weatherService.getCurrentWeather("London");
// Then
assertNotNull(result);
assertEquals("London", result.getName());
}
}

Conclusion

This Weather Dashboard application demonstrates:

Key Features Implemented

  • Multi-provider weather data integration with fallback support
  • Real-time current weather and 5-day forecasts
  • Responsive web interface with modern UI/UX
  • Intelligent caching for performance optimization
  • Comprehensive error handling and graceful degradation
  • RESTful API endpoints for external consumption
  • Location search and auto-detection capabilities

Technical Highlights

  • Spring Boot for rapid application development
  • Thymeleaf for server-side templating
  • Redis caching for improved performance
  • REST API consumption with proper error handling
  • Configuration externalization for different environments
  • Comprehensive testing strategy

Extensibility

The application is designed to be easily extensible:

  • Add new weather data providers
  • Implement historical weather data
  • Add user preferences and favorites
  • Include weather maps and radar data
  • Add notification system for severe weather

This provides a solid foundation for a production-ready weather dashboard that can scale and adapt to changing requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper