Coverage Summary for Class: CityManager (dev.karlkadak.backend.service)
Class |
Method, %
|
Branch, %
|
Line, %
|
CityManager |
100%
(5/5)
|
100%
(22/22)
|
100%
(43/43)
|
CityManager$1 |
100%
(1/1)
|
100%
(1/1)
|
Total |
100%
(6/6)
|
100%
(22/22)
|
100%
(44/44)
|
package dev.karlkadak.backend.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.karlkadak.backend.entity.City;
import dev.karlkadak.backend.exception.*;
import dev.karlkadak.backend.repository.CityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
/**
* Used for managing the cities and their tracking in the database
* <br>Also logs the management actions using {@link java.util.logging.Logger}
*/
@Service
public class CityManager {
private final CityRepository cityRepository;
private final Logger logger;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final WeatherDataImporter weatherDataImporter;
/**
* API key gathered from application.properties which is used for accessing the
* <a href="https://openweathermap.org/api">OpenWeather API</a>
*/
@Value("${openweather.api.key}")
private String apiKey;
@Autowired
public CityManager(CityRepository cityRepository, Logger logger, RestTemplate restTemplate,
ObjectMapper objectMapper, WeatherDataImporter weatherDataImporter) {
this.cityRepository = cityRepository;
this.logger = logger;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.weatherDataImporter = weatherDataImporter;
}
/**
* Adds a {@link City} object with the given name to the database or enables tracking its weather data in case it
* already exists
*
* @param name name of the city to add / toggle
* @return generated / toggled {@link City} object
*/
public City enableImporting(String name) {
// Retrieve the complete city object
City city = retrieveCompleteCity(name);
boolean cityIsPresent = cityRepository.exists(Example.of(city));
// Throw exception if city is already being tracked
if (cityIsPresent && city.isImportingData()) throw new CityAlreadyBeingTrackedException();
// Enable data tracking if city is present and not being tracked
if (cityIsPresent) city.setImportingData(true);
// Save the city to database (adds it if not present / edits the current instance if present)
// Also gets current weather data
cityRepository.save(city);
weatherDataImporter.fetchAndSave(city);
// Log the action
logger.info(String.format("Enabled tracking for city \"%s\".", city.getName()));
return city;
}
/**
* Disables the weather data tracking for {@link City} object with given ID
*
* @param id ID of the {@link City} object to disable
* @return toggled {@link City} object
*/
public City disableImporting(long id) {
// Retrieve the city object
Optional<City> retrievedCityOptional = cityRepository.findById(id);
if (retrievedCityOptional.isEmpty()) throw new CityNotFoundException(id);
City city = retrievedCityOptional.get();
// Throw exception if tracking for city is already disabled
if (!city.isImportingData()) throw new CityAlreadyNotBeingTrackedException();
// Toggle the tracking
city.setImportingData(false);
// Save the city to database
cityRepository.save(city);
// Log the action
logger.info(String.format("Disabled tracking for city \"%s\".", city.getName()));
return city;
}
/**
* Retrieves a complete {@link City} object using the city name received using the <a
* href="https://openweathermap.org/api">OpenWeather API</a><br> In case such city already exists in the database,
* returns that instance instead
* <br>Needs to be package-private in order to test directly
*
* @param name city name to look up
* @return a {@link City} object linked to the name of the city input, if city is present in database returns that
* instance
*/
City retrieveCompleteCity(String name) {
// Check the name for correct formatting
if (!nameFormattedCorrectly(name)) throw new MalformedCityNameException();
// Initialize variables
JsonNode arrayNode;
String completeName;
double latitude;
double longitude;
String countryCode;
// Perform the API request
try {
String requestUrl = String.format("https://api.openweathermap.org/geo/1.0/direct?q=%s=&limit=1&appid=%s",
name, apiKey);
String jsonResponse = restTemplate.getForObject(requestUrl, String.class);
arrayNode = objectMapper.readTree(jsonResponse);
// Throw exception in case of a processing error or an empty response
if (arrayNode == null || !arrayNode.isArray()) throw new Exception();
} catch (Exception e) {
logger.warning(String.format("API response processing error when retrieving data for city \"%s\".", name));
throw new FailedCityDataImportException();
}
// Throw exception if city is not present
if (arrayNode.isEmpty()) throw new CityNotFoundException();
// Process the API response
try {
Map<String, Object> responseMap = objectMapper.convertValue(arrayNode.get(0), new TypeReference<>() {
});
completeName = (String) responseMap.get("name");
// Return the existing city if present
Optional<City> cityOptional = cityRepository.findByName(completeName);
if (cityOptional.isPresent()) return cityOptional.get();
// Return a new city object
latitude = (double) responseMap.get("lat");
longitude = (double) responseMap.get("lon");
countryCode = (String) responseMap.get("country");
return new City(completeName, latitude, longitude, countryCode);
} catch (Exception e) {
logger.warning(String.format("Error when processing retrieved city data for city \"%s\".", name));
throw new FailedCityDataImportException();
}
}
/**
* Used for preliminary checking of a city name's validity<br> Refuses malformed names
*
* @param name complete name of the city to check
* @return true for acceptable, false for malformed city name
*/
private boolean nameFormattedCorrectly(String name) {
return (!name.isBlank() && name.chars().noneMatch(Character::isDigit));
}
}