Added improved sample data generation.
This commit is contained in:
parent
54f17d4cec
commit
184491b9ea
|
@ -0,0 +1,27 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util;
|
||||||
|
|
||||||
|
import org.apache.commons.csv.CSVFormat;
|
||||||
|
import org.apache.commons.csv.CSVRecord;
|
||||||
|
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class CsvUtil {
|
||||||
|
private CsvUtil() {}
|
||||||
|
|
||||||
|
public static void load(Path csvFile, ThrowableConsumer<CSVRecord> recordConsumer) throws IOException {
|
||||||
|
var reader = new FileReader(csvFile.toFile());
|
||||||
|
CSVFormat format = CSVFormat.DEFAULT.builder()
|
||||||
|
.setHeader()
|
||||||
|
.setSkipHeaderRecord(true)
|
||||||
|
.build();
|
||||||
|
for (var record : format.parse(reader)) {
|
||||||
|
try {
|
||||||
|
recordConsumer.accept(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,223 +0,0 @@
|
||||||
package nl.andrewlalis.gymboard_api.util;
|
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.CityRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.CountryRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.*;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.RoleRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserPersonalDetailsRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserPreferencesRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserCreationPayload;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.Role;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserPersonalDetails;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
|
||||||
import org.apache.commons.csv.CSVFormat;
|
|
||||||
import org.apache.commons.csv.CSVRecord;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
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.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple component that loads sample data that's useful when testing the application.
|
|
||||||
*/
|
|
||||||
@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;
|
|
||||||
private final ExerciseSubmissionService submissionService;
|
|
||||||
private final RoleRepository roleRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final UserPersonalDetailsRepository personalDetailsRepository;
|
|
||||||
private final UserPreferencesRepository preferencesRepository;
|
|
||||||
private final UserService userService;
|
|
||||||
|
|
||||||
@Value("${app.cdn-origin}")
|
|
||||||
private String cdnOrigin;
|
|
||||||
|
|
||||||
public SampleDataLoader(
|
|
||||||
CountryRepository countryRepository,
|
|
||||||
CityRepository cityRepository,
|
|
||||||
GymRepository gymRepository,
|
|
||||||
ExerciseRepository exerciseRepository,
|
|
||||||
ExerciseSubmissionService submissionService,
|
|
||||||
RoleRepository roleRepository, UserRepository userRepository, UserPersonalDetailsRepository personalDetailsRepository, UserPreferencesRepository preferencesRepository, UserService userService) {
|
|
||||||
this.countryRepository = countryRepository;
|
|
||||||
this.cityRepository = cityRepository;
|
|
||||||
this.gymRepository = gymRepository;
|
|
||||||
this.exerciseRepository = exerciseRepository;
|
|
||||||
this.submissionService = submissionService;
|
|
||||||
this.roleRepository = roleRepository;
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.personalDetailsRepository = personalDetailsRepository;
|
|
||||||
this.preferencesRepository = preferencesRepository;
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
|
||||||
Path markerFile = Path.of(".sample_data");
|
|
||||||
if (Files.exists(markerFile)) return;
|
|
||||||
|
|
||||||
log.info("Generating sample data.");
|
|
||||||
try {
|
|
||||||
generateSampleData();
|
|
||||||
secondPassGenerateSampleData();
|
|
||||||
Files.writeString(markerFile, "Yes");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
protected void generateSampleData() throws Exception {
|
|
||||||
loadCsv("exercises", record -> {
|
|
||||||
exerciseRepository.save(new Exercise(record.get("short-name"), record.get("name")));
|
|
||||||
});
|
|
||||||
loadCsv("countries", record -> {
|
|
||||||
countryRepository.save(new Country(record.get("code"), record.get("name")));
|
|
||||||
});
|
|
||||||
loadCsv("cities", record -> {
|
|
||||||
var country = countryRepository.findById(record.get("country-code")).orElseThrow();
|
|
||||||
String shortName = record.get("short-name");
|
|
||||||
String name = record.get("name");
|
|
||||||
cityRepository.save(new City(shortName, name, country));
|
|
||||||
});
|
|
||||||
loadCsv("gyms", record -> {
|
|
||||||
var city = cityRepository.findByShortNameAndCountryCode(
|
|
||||||
record.get("city-short-name"),
|
|
||||||
record.get("country-code")
|
|
||||||
).orElseThrow();
|
|
||||||
gymRepository.save(new Gym(
|
|
||||||
city,
|
|
||||||
record.get("short-name"),
|
|
||||||
record.get("name"),
|
|
||||||
record.get("website-url"),
|
|
||||||
new GeoPoint(
|
|
||||||
new BigDecimal(record.get("latitude")),
|
|
||||||
new BigDecimal(record.get("longitude"))
|
|
||||||
),
|
|
||||||
record.get("street-address")
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Loading sample submissions involves sending content to the Gymboard CDN service.
|
|
||||||
// We upload a video for each submission, and wait until all uploads are processed before continuing.
|
|
||||||
|
|
||||||
final CdnClient cdnClient = new CdnClient(cdnOrigin);
|
|
||||||
|
|
||||||
loadCsv("submissions", record -> {
|
|
||||||
var exercise = exerciseRepository.findById(record.get("exercise-short-name")).orElseThrow();
|
|
||||||
BigDecimal weight = new BigDecimal(record.get("raw-weight"));
|
|
||||||
WeightUnit unit = WeightUnit.parse(record.get("weight-unit"));
|
|
||||||
int reps = Integer.parseInt(record.get("reps"));
|
|
||||||
String name = record.get("submitter-name");
|
|
||||||
CompoundGymId gymId = CompoundGymId.parse(record.get("gym-id"));
|
|
||||||
String videoFilename = record.get("video-filename");
|
|
||||||
|
|
||||||
log.info("Uploading video {} to CDN...", videoFilename);
|
|
||||||
var video = cdnClient.uploads.uploadVideo(Path.of("sample_data", videoFilename), "video/mp4");
|
|
||||||
submissionService.createSubmission(gymId, new ExerciseSubmissionPayload(
|
|
||||||
name,
|
|
||||||
exercise.getShortName(),
|
|
||||||
weight.floatValue(),
|
|
||||||
unit.name(),
|
|
||||||
reps,
|
|
||||||
video.id()
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
loadCsv("users", record -> {
|
|
||||||
String email = record.get("email");
|
|
||||||
String password = record.get("password");
|
|
||||||
String name = record.get("name");
|
|
||||||
String[] roleNames = record.get("roles").split("\\s*\\n\\s*");
|
|
||||||
LocalDate birthDate = LocalDate.parse(record.get("birth-date"));
|
|
||||||
BigDecimal currentWeight = new BigDecimal(record.get("current-weight"));
|
|
||||||
WeightUnit currentWeightUnit = WeightUnit.parse(record.get("current-weight-unit"));
|
|
||||||
BigDecimal metricWeight = new BigDecimal(currentWeight.toString());
|
|
||||||
if (currentWeightUnit == WeightUnit.POUNDS) {
|
|
||||||
metricWeight = WeightUnit.toKilograms(metricWeight);
|
|
||||||
}
|
|
||||||
UserPersonalDetails.PersonSex sex = UserPersonalDetails.PersonSex.parse(record.get("sex"));
|
|
||||||
|
|
||||||
UserCreationPayload payload = new UserCreationPayload(email, password, name);
|
|
||||||
var resp = userService.createUser(payload, false);
|
|
||||||
User user = userRepository.findByIdWithRoles(resp.id()).orElseThrow();
|
|
||||||
for (var roleName : roleNames) {
|
|
||||||
if (roleName.isBlank()) continue;
|
|
||||||
Role role = roleRepository.findById(roleName.strip().toLowerCase())
|
|
||||||
.orElseGet(() -> roleRepository.save(new Role(roleName.strip().toLowerCase())));
|
|
||||||
user.getRoles().add(role);
|
|
||||||
}
|
|
||||||
userRepository.save(user);
|
|
||||||
var pd = personalDetailsRepository.findById(user.getId()).orElseThrow();
|
|
||||||
pd.setBirthDate(birthDate);
|
|
||||||
pd.setCurrentWeight(currentWeight);
|
|
||||||
pd.setCurrentWeightUnit(currentWeightUnit);
|
|
||||||
pd.setCurrentMetricWeight(metricWeight);
|
|
||||||
pd.setSex(sex);
|
|
||||||
personalDetailsRepository.save(pd);
|
|
||||||
var p = preferencesRepository.findById(user.getId()).orElseThrow();
|
|
||||||
p.setLocale(record.get("locale"));
|
|
||||||
p.setAccountPrivate(Boolean.parseBoolean(record.get("account-private")));
|
|
||||||
preferencesRepository.save(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
protected void secondPassGenerateSampleData() throws Exception {
|
|
||||||
loadCsv("users", record -> {
|
|
||||||
String email = record.get("email");
|
|
||||||
String[] followingEmails = record.get("following").split("\\s*\\n\\s*");
|
|
||||||
User user = userRepository.findByEmail(email).orElseThrow();
|
|
||||||
for (String followingEmail : followingEmails) {
|
|
||||||
User userToFollow = userRepository.findByEmail(followingEmail).orElseThrow();
|
|
||||||
userService.followUser(user.getId(), userToFollow.getId());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface ThrowableConsumer<T> {
|
|
||||||
void accept(T item) throws Exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadCsv(String csvName, ThrowableConsumer<CSVRecord> recordConsumer) throws IOException {
|
|
||||||
String path = "sample_data/" + csvName + ".csv";
|
|
||||||
log.info("Loading data from {}...", path);
|
|
||||||
var reader = new FileReader(path);
|
|
||||||
CSVFormat format = CSVFormat.DEFAULT.builder()
|
|
||||||
.setHeader()
|
|
||||||
.setSkipHeaderRecord(true)
|
|
||||||
.build();
|
|
||||||
for (var record : format.parse(reader)) {
|
|
||||||
try {
|
|
||||||
recordConsumer.accept(record);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util;
|
||||||
|
|
||||||
|
public interface ThrowableConsumer<T> {
|
||||||
|
void accept(T item) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines a component that can generate sample data for testing
|
||||||
|
* the application. It must define a method to generate data, and optionally, it
|
||||||
|
* can specify a collection of <em>other</em> sample data generator classes that
|
||||||
|
* it depends on. For example, a gym generator might depend on a generator that
|
||||||
|
* creates countries and cities.
|
||||||
|
* <p>
|
||||||
|
* Note that all classes which implement this interface should be annotated
|
||||||
|
* as a <code>@Component</code> and <code>@Profile("development")</code> to
|
||||||
|
* ensure that we keep sample data generation away from production.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public interface SampleDataGenerator {
|
||||||
|
void generate() throws Exception;
|
||||||
|
|
||||||
|
default Collection<Class<? extends SampleDataGenerator>> dependencies() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple component that loads sample data that's useful when testing the application.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleDataLoader implements ApplicationListener<ContextRefreshedEvent> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SampleDataLoader.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of all sample data generators that the application has loaded.
|
||||||
|
*/
|
||||||
|
private final List<SampleDataGenerator> generators;
|
||||||
|
|
||||||
|
public SampleDataLoader(List<SampleDataGenerator> generators) {
|
||||||
|
this.generators = generators;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
Path markerFile = Path.of(".sample_data");
|
||||||
|
if (Files.exists(markerFile)) return;
|
||||||
|
log.info("Generating sample data.");
|
||||||
|
try {
|
||||||
|
Set<SampleDataGenerator> completedGenerators = new HashSet<>();
|
||||||
|
for (var gen : generators) {
|
||||||
|
runGenerator(gen, completedGenerators);
|
||||||
|
}
|
||||||
|
Files.writeString(markerFile, "Yes");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runGenerator(SampleDataGenerator gen, Set<SampleDataGenerator> completed) {
|
||||||
|
for (var dep : gen.dependencies()) {
|
||||||
|
runGenerator(getGeneratorByClass(dep), completed);
|
||||||
|
}
|
||||||
|
if (!completed.contains(gen)) {
|
||||||
|
try {
|
||||||
|
log.info("Running sample data generator: {}", gen.getClass().getSimpleName());
|
||||||
|
gen.generate();
|
||||||
|
completed.add(gen);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Generator failed: " + gen.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SampleDataGenerator getGeneratorByClass(Class<? extends SampleDataGenerator> c) {
|
||||||
|
for (var gen : generators) {
|
||||||
|
if (gen.getClass().equals(c)) return gen;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Missing generator: " + c.getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.CsvUtil;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleExerciseGenerator implements SampleDataGenerator {
|
||||||
|
private final ExerciseRepository exerciseRepository;
|
||||||
|
|
||||||
|
public SampleExerciseGenerator(ExerciseRepository exerciseRepository) {
|
||||||
|
this.exerciseRepository = exerciseRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate() throws Exception {
|
||||||
|
CsvUtil.load(Path.of("sample_data", "exercises.csv"), r -> {
|
||||||
|
exerciseRepository.save(new Exercise(r.get("short-name"), r.get("name")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.CityRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.CountryRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.City;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.Country;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.CsvUtil;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleGeoDataGenerator implements SampleDataGenerator {
|
||||||
|
private final CityRepository cityRepository;
|
||||||
|
private final CountryRepository countryRepository;
|
||||||
|
|
||||||
|
public SampleGeoDataGenerator(CityRepository cityRepository, CountryRepository countryRepository) {
|
||||||
|
this.cityRepository = cityRepository;
|
||||||
|
this.countryRepository = countryRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate() throws Exception {
|
||||||
|
CsvUtil.load(Path.of("sample_data", "countries.csv"), r -> {
|
||||||
|
countryRepository.save(new Country(r.get("code"), r.get("name")));
|
||||||
|
});
|
||||||
|
CsvUtil.load(Path.of("sample_data", "cities.csv"), r -> {
|
||||||
|
var country = countryRepository.findById(r.get("country-code")).orElseThrow();
|
||||||
|
cityRepository.save(new City(r.get("short-name"), r.get("name"), country));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.CityRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.GeoPoint;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.CsvUtil;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleGymGenerator implements SampleDataGenerator {
|
||||||
|
private final CityRepository cityRepository;
|
||||||
|
private final GymRepository gymRepository;
|
||||||
|
|
||||||
|
public SampleGymGenerator(CityRepository cityRepository, GymRepository gymRepository) {
|
||||||
|
this.cityRepository = cityRepository;
|
||||||
|
this.gymRepository = gymRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate() throws Exception {
|
||||||
|
CsvUtil.load(Path.of("sample_data", "gyms.csv"), r -> {
|
||||||
|
var city = cityRepository.findByShortNameAndCountryCode(
|
||||||
|
r.get("city-short-name"),
|
||||||
|
r.get("country-code")
|
||||||
|
).orElseThrow();
|
||||||
|
gymRepository.save(new Gym(
|
||||||
|
city,
|
||||||
|
r.get("short-name"),
|
||||||
|
r.get("name"),
|
||||||
|
r.get("website-url"),
|
||||||
|
new GeoPoint(
|
||||||
|
new BigDecimal(r.get("latitude")),
|
||||||
|
new BigDecimal(r.get("longitude"))
|
||||||
|
),
|
||||||
|
r.get("street-address")
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends SampleDataGenerator>> dependencies() {
|
||||||
|
return Set.of(SampleGeoDataGenerator.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.CsvUtil;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleSubmissionGenerator implements SampleDataGenerator {
|
||||||
|
private final ExerciseRepository exerciseRepository;
|
||||||
|
private final ExerciseSubmissionService submissionService;
|
||||||
|
|
||||||
|
@Value("${app.cdn-origin}")
|
||||||
|
private String cdnOrigin;
|
||||||
|
|
||||||
|
public SampleSubmissionGenerator(ExerciseRepository exerciseRepository, ExerciseSubmissionService submissionService) {
|
||||||
|
this.exerciseRepository = exerciseRepository;
|
||||||
|
this.submissionService = submissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate() throws Exception {
|
||||||
|
final CdnClient cdnClient = new CdnClient(cdnOrigin);
|
||||||
|
CsvUtil.load(Path.of("sample_data", "submissions.csv"), r -> {
|
||||||
|
var exercise = exerciseRepository.findById(r.get("exercise-short-name")).orElseThrow();
|
||||||
|
BigDecimal weight = new BigDecimal(r.get("raw-weight"));
|
||||||
|
WeightUnit unit = WeightUnit.parse(r.get("weight-unit"));
|
||||||
|
int reps = Integer.parseInt(r.get("reps"));
|
||||||
|
String name = r.get("submitter-name");
|
||||||
|
CompoundGymId gymId = CompoundGymId.parse(r.get("gym-id"));
|
||||||
|
String videoFilename = r.get("video-filename");
|
||||||
|
|
||||||
|
var video = cdnClient.uploads.uploadVideo(Path.of("sample_data", videoFilename), "video/mp4");
|
||||||
|
submissionService.createSubmission(gymId, new ExerciseSubmissionPayload(
|
||||||
|
name,
|
||||||
|
exercise.getShortName(),
|
||||||
|
weight.floatValue(),
|
||||||
|
unit.name(),
|
||||||
|
reps,
|
||||||
|
video.id()
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends SampleDataGenerator>> dependencies() {
|
||||||
|
return Set.of(SampleExerciseGenerator.class, SampleUserGenerator.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.dao.RoleRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserFollowingRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.Role;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.UserFollowing;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.UserPersonalDetails;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.CsvUtil;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("development")
|
||||||
|
public class SampleUserGenerator implements SampleDataGenerator {
|
||||||
|
private final ULID ulid;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final UserFollowingRepository followingRepository;
|
||||||
|
|
||||||
|
public SampleUserGenerator(ULID ulid, UserRepository userRepository, PasswordEncoder passwordEncoder, RoleRepository roleRepository, UserFollowingRepository followingRepository) {
|
||||||
|
this.ulid = ulid;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.roleRepository = roleRepository;
|
||||||
|
this.followingRepository = followingRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate() throws Exception {
|
||||||
|
Path usersCsvPath = Path.of("sample_data", "users.csv");
|
||||||
|
CsvUtil.load(usersCsvPath, r -> {
|
||||||
|
User user = new User(
|
||||||
|
ulid.nextULID(),
|
||||||
|
true,
|
||||||
|
r.get("email"),
|
||||||
|
passwordEncoder.encode(r.get("password")),
|
||||||
|
r.get("name")
|
||||||
|
);
|
||||||
|
String[] roleNames = r.get("roles").split("\\s*\\n\\s*");
|
||||||
|
for (var roleName : roleNames) {
|
||||||
|
if (roleName.isBlank()) continue;
|
||||||
|
Role role = roleRepository.findById(roleName.strip().toLowerCase())
|
||||||
|
.orElseGet(() -> roleRepository.save(new Role(roleName.strip().toLowerCase())));
|
||||||
|
user.getRoles().add(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the user's personal details.
|
||||||
|
var pd = user.getPersonalDetails();
|
||||||
|
String birthDateStr = r.get("birth-date");
|
||||||
|
if (birthDateStr != null && !birthDateStr.isBlank()) {
|
||||||
|
pd.setBirthDate(LocalDate.parse(birthDateStr));
|
||||||
|
}
|
||||||
|
String currentWeightStr = r.get("current-weight");
|
||||||
|
String currentWeightUnitStr = r.get("current-weight-unit");
|
||||||
|
if (
|
||||||
|
currentWeightStr != null && !currentWeightStr.isBlank() &&
|
||||||
|
currentWeightUnitStr != null && !currentWeightUnitStr.isBlank()
|
||||||
|
) {
|
||||||
|
BigDecimal currentWeight = new BigDecimal(currentWeightStr);
|
||||||
|
WeightUnit currentWeightUnit = WeightUnit.parse(currentWeightUnitStr);
|
||||||
|
BigDecimal metricWeight = new BigDecimal(currentWeightStr);
|
||||||
|
if (currentWeightUnit == WeightUnit.POUNDS) {
|
||||||
|
metricWeight = WeightUnit.toKilograms(metricWeight);
|
||||||
|
}
|
||||||
|
pd.setCurrentWeight(currentWeight);
|
||||||
|
pd.setCurrentWeightUnit(currentWeightUnit);
|
||||||
|
pd.setCurrentMetricWeight(metricWeight);
|
||||||
|
}
|
||||||
|
pd.setSex(UserPersonalDetails.PersonSex.parse(r.get("sex")));
|
||||||
|
|
||||||
|
// Set up the user's preferences.
|
||||||
|
var p = user.getPreferences();
|
||||||
|
p.setLocale(r.get("locale"));
|
||||||
|
p.setAccountPrivate(Boolean.parseBoolean(r.get("account-private")));
|
||||||
|
userRepository.save(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do a second pass to add follower information.
|
||||||
|
CsvUtil.load(usersCsvPath, r -> {
|
||||||
|
User user = userRepository.findByEmail(r.get("email")).orElseThrow();
|
||||||
|
String[] followingEmails = r.get("following").split("\\s+");
|
||||||
|
for (String followingEmail : followingEmails) {
|
||||||
|
User userToFollow = userRepository.findByEmail(followingEmail).orElseThrow();
|
||||||
|
followingRepository.save(new UserFollowing(userToFollow, user));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,3 +10,8 @@ export interface GymSearchResult {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserSearchResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
package nl.andrewlalis.gymboardcdn.service;
|
package nl.andrewlalis.gymboardcdn.service;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboardcdn.model.StoredFile;
|
import nl.andrewlalis.gymboardcdn.model.StoredFile;
|
||||||
|
import nl.andrewlalis.gymboardcdn.model.StoredFileRepository;
|
||||||
import nl.andrewlalis.gymboardcdn.util.ULID;
|
import nl.andrewlalis.gymboardcdn.util.ULID;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service that manages storing and retrieving files from a base filesystem.
|
* The service that manages storing and retrieving files from a base filesystem.
|
||||||
|
@ -27,9 +34,11 @@ public class FileService {
|
||||||
@Value("${app.files.temp-dir}")
|
@Value("${app.files.temp-dir}")
|
||||||
private String tempDir;
|
private String tempDir;
|
||||||
|
|
||||||
|
private final StoredFileRepository storedFileRepository;
|
||||||
private final ULID ulid;
|
private final ULID ulid;
|
||||||
|
|
||||||
public FileService(ULID ulid) {
|
public FileService(StoredFileRepository storedFileRepository, ULID ulid) {
|
||||||
|
this.storedFileRepository = storedFileRepository;
|
||||||
this.ulid = ulid;
|
this.ulid = ulid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,4 +87,30 @@ public class FileService {
|
||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduled task that removes any StoredFile entities for which no more
|
||||||
|
* physical file exists.
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
|
@Transactional
|
||||||
|
public void removeOrphanedFiles() {
|
||||||
|
Pageable pageable = PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||||
|
Page<StoredFile> page = storedFileRepository.findAll(pageable);
|
||||||
|
while (!page.isEmpty()) {
|
||||||
|
for (var storedFile : page) {
|
||||||
|
try {
|
||||||
|
Path filePath = getStoragePathForFile(storedFile);
|
||||||
|
if (Files.notExists(filePath)) {
|
||||||
|
log.warn("Removing stored file {} because it no longer exists on the disk.", storedFile.getId());
|
||||||
|
storedFileRepository.delete(storedFile);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Couldn't get storage path for stored file.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageable = pageable.next();
|
||||||
|
page = storedFileRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue