Added starter model.

This commit is contained in:
Andrew Lalis 2023-01-20 18:16:08 +01:00
parent 7b6610a01a
commit 4f7f17f6b0
18 changed files with 497 additions and 41 deletions

View File

@ -30,3 +30,5 @@ build/
### VS Code ###
.vscode/
.sample_data

View File

@ -0,0 +1,30 @@
package nl.andrewlalis.gymboard_api.controller;
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
import nl.andrewlalis.gymboard_api.service.GymService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Controller for accessing a particular gym.
*/
@RestController
@RequestMapping(path = "/gyms/{countryCode}/{cityCode}/{gymName}")
public class GymController {
private final GymService gymService;
public GymController(GymService gymService) {
this.gymService = gymService;
}
@GetMapping
public GymResponse getGym(
@PathVariable String countryCode,
@PathVariable String cityCode,
@PathVariable String gymName
) {
return gymService.getGym(countryCode, cityCode, gymName);
}
}

View File

@ -0,0 +1,33 @@
package nl.andrewlalis.gymboard_api.controller.dto;
import nl.andrewlalis.gymboard_api.model.Gym;
import java.time.format.DateTimeFormatter;
public record GymResponse (
String countryCode,
String countryName,
String cityShortName,
String cityName,
String createdAt,
String displayName,
String websiteUrl,
double locationLatitude,
double locationLongitude,
String streetAddress
) {
public GymResponse(Gym gym) {
this(
gym.getCity().getCountry().getCode(),
gym.getCity().getCountry().getName(),
gym.getCity().getShortName(),
gym.getCity().getName(),
gym.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
gym.getDisplayName(),
gym.getWebsiteUrl(),
gym.getLocation().getLatitude().doubleValue(),
gym.getLocation().getLongitude().doubleValue(),
gym.getStreetAddress()
);
}
}

View File

@ -0,0 +1,3 @@
package nl.andrewlalis.gymboard_api.controller.dto;
public record RawGymId(String countryCode, String cityCode, String gymName) {}

View File

@ -0,0 +1,10 @@
package nl.andrewlalis.gymboard_api.dao;
import nl.andrewlalis.gymboard_api.model.City;
import nl.andrewlalis.gymboard_api.model.CityId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CityRepository extends JpaRepository<City, CityId> {
}

View File

@ -0,0 +1,9 @@
package nl.andrewlalis.gymboard_api.dao;
import nl.andrewlalis.gymboard_api.model.Country;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CountryRepository extends JpaRepository<Country, String> {
}

View File

@ -0,0 +1,18 @@
package nl.andrewlalis.gymboard_api.dao;
import nl.andrewlalis.gymboard_api.model.Gym;
import nl.andrewlalis.gymboard_api.model.GymId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface GymRepository extends JpaRepository<Gym, GymId> {
@Query("SELECT g FROM Gym g " +
"WHERE g.id.shortName = :gym AND " +
"g.id.city.id.shortName = :city AND " +
"g.id.city.id.country.code = :country")
Optional<Gym> findByRawId(String gym, String city, String country);
}

View File

@ -0,0 +1,9 @@
package nl.andrewlalis.gymboard_api.dao.exercise;
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ExerciseRepository extends JpaRepository<Exercise, String> {
}

View File

@ -0,0 +1,32 @@
package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.*;
@Entity
@Table(name = "city")
public class City {
@EmbeddedId
private CityId id;
@Column(nullable = false)
private String name;
public City() {}
public City(String shortName, String name, Country country) {
this.id = new CityId(shortName, country);
this.name = name;
}
public String getShortName() {
return id.getShortName();
}
public String getName() {
return name;
}
public Country getCountry() {
return id.getCountry();
}
}

View File

@ -0,0 +1,46 @@
package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
public class CityId implements Serializable {
@Column(nullable = false, length = 127)
private String shortName;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Country country;
public CityId() {}
public CityId(String shortName, Country country) {
this.shortName = shortName;
this.country = country;
}
public String getShortName() {
return shortName;
}
public Country getCountry() {
return country;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CityId cityId = (CityId) o;
return shortName.equals(cityId.shortName) && country.equals(cityId.country);
}
@Override
public int hashCode() {
return Objects.hash(shortName, country);
}
}

View File

@ -0,0 +1,32 @@
package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "country")
public class Country {
@Id
@Column(nullable = false, length = 2, unique = true)
private String code;
@Column(nullable = false, unique = true)
private String name;
public Country() {}
public Country(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,45 @@
package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Objects;
@Embeddable
public class GeoPoint implements Serializable {
@Column(nullable = false, precision = 9, scale = 6)
private BigDecimal latitude;
@Column(nullable = false, precision = 9, scale = 6)
private BigDecimal longitude;
public GeoPoint() {}
public GeoPoint(BigDecimal latitude, BigDecimal longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public BigDecimal getLatitude() {
return latitude;
}
public BigDecimal getLongitude() {
return longitude;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GeoPoint geoPoint = (GeoPoint) o;
return geoPoint.latitude.equals(this.latitude) &&
geoPoint.longitude.equals(this.longitude);
}
@Override
public int hashCode() {
return Objects.hash(latitude, longitude);
}
}

View File

@ -1,12 +1,13 @@
package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
/**
* Represents a single gym location, at which local leaderboards are held.
*/
@Entity
@Table(name = "gym")
public class Gym {
@ -16,23 +17,57 @@ public class Gym {
@CreationTimestamp
private LocalDateTime createdAt;
@Column(nullable = false, length = 127)
private String displayName;
@Column(length = 1024)
private String websiteUrl;
@Embedded
private GeoPoint location;
@Column(nullable = false, length = 1024)
private String streetAddress;
public Gym() {}
public Gym(City city, String shortName, String displayName, String websiteUrl, GeoPoint location, String streetAddress) {
this.id = new GymId(shortName, city);
this.displayName = displayName;
this.websiteUrl = websiteUrl;
this.location = location;
this.streetAddress = streetAddress;
}
public GymId getId() {
return id;
}
public String getName() {
return id.getName();
public City getCity() {
return id.getCity();
}
public String getCityId() {
return id.getCityId();
}
public String getCountryId() {
return id.getCountryId();
public String getShortName() {
return id.getShortName();
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public String getDisplayName() {
return displayName;
}
public String getWebsiteUrl() {
return websiteUrl;
}
public GeoPoint getLocation() {
return location;
}
public String getStreetAddress() {
return streetAddress;
}
}

View File

@ -2,6 +2,8 @@ package nl.andrewlalis.gymboard_api.model;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import java.io.Serializable;
import java.util.Objects;
@ -12,45 +14,35 @@ import java.util.Objects;
@Embeddable
public class GymId implements Serializable {
@Column(nullable = false, length = 127)
private String name;
@Column(nullable = false, length = 127)
private String cityId;
@Column(nullable = false, length = 2)
private String countryId;
private String shortName;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private City city;
public GymId() {}
public GymId(String name, String cityId, String countryId) {
this.name = name;
this.cityId = cityId;
this.countryId = countryId;
public GymId(String shortName, City city) {
this.shortName = shortName;
this.city = city;
}
public String getName() {
return name;
public String getShortName() {
return shortName;
}
public String getCityId() {
return cityId;
}
public String getCountryId() {
return countryId;
public City getCity() {
return city;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof GymId gymId) {
return getName().equals(gymId.getName()) &&
getCityId().equals(gymId.getCityId()) &&
getCountryId().equals(gymId.getCountryId());
}
return false;
if (!(o instanceof GymId gymId)) return false;
return getShortName().equals(gymId.getShortName()) && getCity().equals(gymId.getCity());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getCityId(), getCountryId());
return Objects.hash(getShortName(), getCity());
}
}

View File

@ -0,0 +1,91 @@
package nl.andrewlalis.gymboard_api.model;
import nl.andrewlalis.gymboard_api.dao.CityRepository;
import nl.andrewlalis.gymboard_api.dao.CountryRepository;
import nl.andrewlalis.gymboard_api.dao.GymRepository;
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
@Component
public class SampleDataLoader implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger log = LoggerFactory.getLogger(SampleDataLoader.class);
private final CountryRepository countryRepository;
private final CityRepository cityRepository;
private final GymRepository gymRepository;
private final ExerciseRepository exerciseRepository;
public SampleDataLoader(
CountryRepository countryRepository,
CityRepository cityRepository,
GymRepository gymRepository,
ExerciseRepository exerciseRepository
) {
this.countryRepository = countryRepository;
this.cityRepository = cityRepository;
this.gymRepository = gymRepository;
this.exerciseRepository = exerciseRepository;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Path markerFile = Path.of(".sample_data");
if (Files.exists(markerFile)) return;
log.info("Generating sample data.");
generateSampleData();
try {
Files.writeString(markerFile, "Yes");
} catch (IOException e) {
e.printStackTrace();
}
}
@Transactional
protected void generateSampleData() {
Exercise benchPress = exerciseRepository.save(new Exercise("barbell-bench-press", "Barbell Bench Press"));
Exercise squat = exerciseRepository.save(new Exercise("barbell-squat", "Barbell Squat"));
Exercise deadlift = exerciseRepository.save(new Exercise("deadlift", "Deadlift"));
Country nl = countryRepository.save(new Country("nl", "Netherlands"));
City groningen = cityRepository.save(new City("groningen", "Groningen", nl));
Gym g1 = gymRepository.save(new Gym(
groningen,
"trainmore-munnekeholm",
"Trainmore Munnekeholm",
"https://trainmore.nl/clubs/munnekeholm/",
new GeoPoint(new BigDecimal("53.215939"), new BigDecimal("6.561549")),
"Munnekeholm 1, 9711 JA Groningen"
));
Gym g2 = gymRepository.save(new Gym(
groningen,
"trainmore-oude-ebbinge",
"Trainmore Oude Ebbinge Non-Stop",
"https://trainmore.nl/clubs/oude-ebbinge/",
new GeoPoint(new BigDecimal("53.220900"), new BigDecimal("6.565976")),
"Oude Ebbingestraat 54-58, 9712 HL Groningen"
));
Country us = countryRepository.save(new Country("us", "United States"));
City tampa = cityRepository.save(new City("tampa", "Tampa", us));
Gym g3 = gymRepository.save(new Gym(
tampa,
"powerhouse-gym",
"Powerhouse Gym Athletic Club",
"http://www.pgathleticclub.com/",
new GeoPoint(new BigDecimal("27.997223"), new BigDecimal("-82.496237")),
"3251-A W Hillsborough Ave, Tampa, FL 33614, United States"
));
}
}

View File

@ -0,0 +1,35 @@
package nl.andrewlalis.gymboard_api.model.exercise;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* An exercise for which attempts can be submitted, and lifts are tracked.
*/
@Entity
@Table(name = "exercise")
public class Exercise {
@Id
@Column(nullable = false, length = 127)
private String shortname;
@Column(nullable = false, unique = true)
private String name;
public Exercise() {}
public Exercise(String shortname, String name) {
this.shortname = shortname;
this.name = name;
}
public String getShortname() {
return shortname;
}
public String getName() {
return name;
}
}

View File

@ -1,12 +1,14 @@
package nl.andrewlalis.gymboard_api.model;
package nl.andrewlalis.gymboard_api.model.exercise;
import jakarta.persistence.*;
import nl.andrewlalis.gymboard_api.model.Gym;
import org.hibernate.annotations.CreationTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "gym_exercise_submission")
@Table(name = "exercise_submission")
public class ExerciseSubmission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -15,11 +17,18 @@ public class ExerciseSubmission {
@CreationTimestamp
private LocalDateTime createdAt;
@Column(nullable = false, updatable = false, length = 63)
private String submitterName;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Gym gym;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Exercise exercise;
@Column(nullable = false, updatable = false, length = 63)
private String submitterName;
@Column(nullable = false)
private boolean verified;
@Column(nullable = false, precision = 7, scale = 2)
private BigDecimal weight;
}

View File

@ -0,0 +1,25 @@
package nl.andrewlalis.gymboard_api.service;
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
import nl.andrewlalis.gymboard_api.dao.GymRepository;
import nl.andrewlalis.gymboard_api.model.Gym;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
@Service
public class GymService {
private final GymRepository gymRepository;
public GymService(GymRepository gymRepository) {
this.gymRepository = gymRepository;
}
@Transactional(readOnly = true)
public GymResponse getGym(String countryCode, String city, String gymName) {
Gym gym = gymRepository.findByRawId(gymName, city, countryCode)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new GymResponse(gym);
}
}