diff --git a/gymboard-api/.gitignore b/gymboard-api/.gitignore index 7ed0d6b..5e7b517 100644 --- a/gymboard-api/.gitignore +++ b/gymboard-api/.gitignore @@ -30,3 +30,5 @@ build/ ### VS Code ### .vscode/ + +.sample_data diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java new file mode 100644 index 0000000..0e0d4d0 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java @@ -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); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/GymResponse.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/GymResponse.java new file mode 100644 index 0000000..f16cb24 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/GymResponse.java @@ -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() + ); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java new file mode 100644 index 0000000..0b22f07 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java @@ -0,0 +1,3 @@ +package nl.andrewlalis.gymboard_api.controller.dto; + +public record RawGymId(String countryCode, String cityCode, String gymName) {} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CityRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CityRepository.java new file mode 100644 index 0000000..bb3714d --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CityRepository.java @@ -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 { +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CountryRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CountryRepository.java new file mode 100644 index 0000000..db95810 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/CountryRepository.java @@ -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 { +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java new file mode 100644 index 0000000..f0b0c03 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java @@ -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 { + @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 findByRawId(String gym, String city, String country); +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseRepository.java new file mode 100644 index 0000000..b83f779 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseRepository.java @@ -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 { +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/City.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/City.java new file mode 100644 index 0000000..91c9837 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/City.java @@ -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(); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/CityId.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/CityId.java new file mode 100644 index 0000000..699988e --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/CityId.java @@ -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); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Country.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Country.java new file mode 100644 index 0000000..cdee1f3 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Country.java @@ -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; + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GeoPoint.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GeoPoint.java new file mode 100644 index 0000000..c82d30b --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GeoPoint.java @@ -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); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Gym.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Gym.java index 80b7f8e..3ea835f 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Gym.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/Gym.java @@ -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; + } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GymId.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GymId.java index ccf73e8..09e4f21 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GymId.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/GymId.java @@ -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()); } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/SampleDataLoader.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/SampleDataLoader.java new file mode 100644 index 0000000..6615dd8 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/SampleDataLoader.java @@ -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 { + 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" + )); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/Exercise.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/Exercise.java new file mode 100644 index 0000000..3b69c50 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/Exercise.java @@ -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; + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/ExerciseSubmission.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java similarity index 54% rename from gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/ExerciseSubmission.java rename to gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java index 9ddc07b..d0c2c07 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/ExerciseSubmission.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java @@ -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; } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java new file mode 100644 index 0000000..3aff7f7 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java @@ -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); + } +}