Changed to ULID submission ids.
This commit is contained in:
parent
a8168768a8
commit
617bd45127
|
@ -5,3 +5,7 @@ An HTTP/REST API powered by Java and Spring Boot. This API serves as the main en
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
To ease development, `nl.andrewlalis.gymboard_api.model.SampleDataLoader` will run on startup and populate the database with some sample entities. You can regenerate this data by manually deleting the database, and deleting the `.sample_data` marker file that's generated in the project directory.
|
To ease development, `nl.andrewlalis.gymboard_api.model.SampleDataLoader` will run on startup and populate the database with some sample entities. You can regenerate this data by manually deleting the database, and deleting the `.sample_data` marker file that's generated in the project directory.
|
||||||
|
|
||||||
|
## ULIDs
|
||||||
|
|
||||||
|
For entities that don't need a human-readable primary key (or keys), we choose to use [ULID](https://github.com/ulid/spec) strings, which are like UUIDs, but use a timestamp based preamble such that their values are monotonically increasing, and lexicographically ordered by creation time. The result is a pseudorandom string of 26 characters which appears random to a human, yet is efficient as a primary key. It's also near-impossible for automated systems to guess previous/next ids.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.0.1</version>
|
<version>3.0.2</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>nl.andrewlalis</groupId>
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
package nl.andrewlalis.gymboard_api.config;
|
package nl.andrewlalis.gymboard_api.config;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.web.filter.CorsFilter;
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfig {
|
public class WebConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CorsFilter corsFilter() {
|
public CorsFilter corsFilter() {
|
||||||
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
@ -23,4 +25,9 @@ public class WebConfig {
|
||||||
source.registerCorsConfiguration("/**", config);
|
source.registerCorsConfiguration("/**", config);
|
||||||
return new CorsFilter(source);
|
return new CorsFilter(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ULID ulid() {
|
||||||
|
return new ULID();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package nl.andrewlalis.gymboard_api.controller;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import nl.andrewlalis.gymboard_api.dao.StoredFileRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.model.StoredFile;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class FileController {
|
|
||||||
private final StoredFileRepository fileRepository;
|
|
||||||
|
|
||||||
public FileController(StoredFileRepository fileRepository) {
|
|
||||||
this.fileRepository = fileRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(path = "/files/{fileId}")
|
|
||||||
public void getFile(@PathVariable long fileId, HttpServletResponse response) throws IOException {
|
|
||||||
StoredFile file = fileRepository.findById(fileId)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
response.setContentType(file.getMimeType());
|
|
||||||
response.setContentLengthLong(file.getSize());
|
|
||||||
response.getOutputStream().write(file.getContent());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
package nl.andrewlalis.gymboard_api.controller;
|
package nl.andrewlalis.gymboard_api.controller;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.*;
|
import nl.andrewlalis.gymboard_api.controller.dto.*;
|
||||||
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService;
|
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService;
|
||||||
import nl.andrewlalis.gymboard_api.service.GymService;
|
import nl.andrewlalis.gymboard_api.service.GymService;
|
||||||
|
@ -45,11 +44,6 @@ public class GymController {
|
||||||
return submissionService.createSubmission(CompoundGymId.parse(compoundId), payload);
|
return submissionService.createSubmission(CompoundGymId.parse(compoundId), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/submissions/{submissionId}")
|
|
||||||
public ExerciseSubmissionResponse getSubmission(@PathVariable String compoundId, @PathVariable long submissionId) {
|
|
||||||
return submissionService.getSubmission(CompoundGymId.parse(compoundId), submissionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(path = "/submissions/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(path = "/submissions/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public UploadedFileResponse uploadVideo(
|
public UploadedFileResponse uploadVideo(
|
||||||
@PathVariable String compoundId,
|
@PathVariable String compoundId,
|
||||||
|
@ -57,13 +51,4 @@ public class GymController {
|
||||||
) {
|
) {
|
||||||
return uploadService.handleSubmissionUpload(CompoundGymId.parse(compoundId), file);
|
return uploadService.handleSubmissionUpload(CompoundGymId.parse(compoundId), file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/submissions/{submissionId}/video")
|
|
||||||
public void getSubmissionVideo(
|
|
||||||
@PathVariable String compoundId,
|
|
||||||
@PathVariable long submissionId,
|
|
||||||
HttpServletResponse response
|
|
||||||
) {
|
|
||||||
submissionService.streamVideo(CompoundGymId.parse(compoundId), submissionId, response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||||
|
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "/submissions")
|
||||||
|
public class SubmissionController {
|
||||||
|
private final ExerciseSubmissionService submissionService;
|
||||||
|
|
||||||
|
public SubmissionController(ExerciseSubmissionService submissionService) {
|
||||||
|
this.submissionService = submissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/{submissionId}")
|
||||||
|
public ExerciseSubmissionResponse getSubmission(@PathVariable String submissionId) {
|
||||||
|
return submissionService.getSubmission(submissionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/{submissionId}/video")
|
||||||
|
public void getSubmissionVideo(@PathVariable String submissionId, HttpServletResponse response) {
|
||||||
|
submissionService.streamVideo(submissionId, response);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
public record ExerciseSubmissionResponse(
|
public record ExerciseSubmissionResponse(
|
||||||
long id,
|
String id,
|
||||||
String createdAt,
|
String createdAt,
|
||||||
GymSimpleResponse gym,
|
GymSimpleResponse gym,
|
||||||
ExerciseResponse exercise,
|
ExerciseResponse exercise,
|
||||||
|
|
|
@ -8,6 +8,6 @@ import org.springframework.stereotype.Repository;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ExerciseSubmissionRepository extends JpaRepository<ExerciseSubmission, Long>, JpaSpecificationExecutor<ExerciseSubmission> {
|
public interface ExerciseSubmissionRepository extends JpaRepository<ExerciseSubmission, String>, JpaSpecificationExecutor<ExerciseSubmission> {
|
||||||
List<ExerciseSubmission> findAllByStatus(ExerciseSubmission.Status status);
|
List<ExerciseSubmission> findAllByStatus(ExerciseSubmission.Status status);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package nl.andrewlalis.gymboard_api.dao.exercise;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -10,4 +11,8 @@ import java.util.Optional;
|
||||||
@Repository
|
@Repository
|
||||||
public interface ExerciseSubmissionVideoFileRepository extends JpaRepository<ExerciseSubmissionVideoFile, Long> {
|
public interface ExerciseSubmissionVideoFileRepository extends JpaRepository<ExerciseSubmissionVideoFile, Long> {
|
||||||
Optional<ExerciseSubmissionVideoFile> findBySubmission(ExerciseSubmission submission);
|
Optional<ExerciseSubmissionVideoFile> findBySubmission(ExerciseSubmission submission);
|
||||||
|
|
||||||
|
@Query("SELECT f FROM ExerciseSubmissionVideoFile f WHERE " +
|
||||||
|
"f.submission.id = :submissionId AND f.submission.complete = true")
|
||||||
|
Optional<ExerciseSubmissionVideoFile> findByCompletedSubmissionId(String submissionId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,8 @@ public class ExerciseSubmission {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@Column(nullable = false, updatable = false, length = 26)
|
||||||
private Long id;
|
private String id;
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
@ -66,9 +66,18 @@ public class ExerciseSubmission {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private int reps;
|
private int reps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker that's used to simplify queries where we just want submissions
|
||||||
|
* that are in a status that's not WAITING, PROCESSING, or FAILED, i.e.
|
||||||
|
* a successful submission that's been processed.
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean complete;
|
||||||
|
|
||||||
public ExerciseSubmission() {}
|
public ExerciseSubmission() {}
|
||||||
|
|
||||||
public ExerciseSubmission(Gym gym, Exercise exercise, String submitterName, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) {
|
public ExerciseSubmission(String id, Gym gym, Exercise exercise, String submitterName, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) {
|
||||||
|
this.id = id;
|
||||||
this.gym = gym;
|
this.gym = gym;
|
||||||
this.exercise = exercise;
|
this.exercise = exercise;
|
||||||
this.submitterName = submitterName;
|
this.submitterName = submitterName;
|
||||||
|
@ -77,9 +86,10 @@ public class ExerciseSubmission {
|
||||||
this.metricWeight = metricWeight;
|
this.metricWeight = metricWeight;
|
||||||
this.reps = reps;
|
this.reps = reps;
|
||||||
this.status = Status.WAITING;
|
this.status = Status.WAITING;
|
||||||
|
this.complete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,4 +132,12 @@ public class ExerciseSubmission {
|
||||||
public int getReps() {
|
public int getReps() {
|
||||||
return reps;
|
return reps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isComplete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComplete(boolean complete) {
|
||||||
|
this.complete = complete;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -33,7 +34,6 @@ import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ public class ExerciseSubmissionService {
|
||||||
private final ExerciseSubmissionTempFileRepository tempFileRepository;
|
private final ExerciseSubmissionTempFileRepository tempFileRepository;
|
||||||
private final ExerciseSubmissionVideoFileRepository submissionVideoFileRepository;
|
private final ExerciseSubmissionVideoFileRepository submissionVideoFileRepository;
|
||||||
private final Executor taskExecutor;
|
private final Executor taskExecutor;
|
||||||
|
private final ULID ulid;
|
||||||
|
|
||||||
public ExerciseSubmissionService(GymRepository gymRepository,
|
public ExerciseSubmissionService(GymRepository gymRepository,
|
||||||
StoredFileRepository fileRepository,
|
StoredFileRepository fileRepository,
|
||||||
|
@ -59,8 +60,7 @@ public class ExerciseSubmissionService {
|
||||||
ExerciseSubmissionRepository exerciseSubmissionRepository,
|
ExerciseSubmissionRepository exerciseSubmissionRepository,
|
||||||
ExerciseSubmissionTempFileRepository tempFileRepository,
|
ExerciseSubmissionTempFileRepository tempFileRepository,
|
||||||
ExerciseSubmissionVideoFileRepository submissionVideoFileRepository,
|
ExerciseSubmissionVideoFileRepository submissionVideoFileRepository,
|
||||||
Executor taskExecutor
|
Executor taskExecutor, ULID ulid) {
|
||||||
) {
|
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
this.fileRepository = fileRepository;
|
this.fileRepository = fileRepository;
|
||||||
this.exerciseRepository = exerciseRepository;
|
this.exerciseRepository = exerciseRepository;
|
||||||
|
@ -68,34 +68,19 @@ public class ExerciseSubmissionService {
|
||||||
this.tempFileRepository = tempFileRepository;
|
this.tempFileRepository = tempFileRepository;
|
||||||
this.submissionVideoFileRepository = submissionVideoFileRepository;
|
this.submissionVideoFileRepository = submissionVideoFileRepository;
|
||||||
this.taskExecutor = taskExecutor;
|
this.taskExecutor = taskExecutor;
|
||||||
|
this.ulid = ulid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ExerciseSubmissionResponse getSubmission(CompoundGymId id, long submissionId) {
|
public ExerciseSubmissionResponse getSubmission(String submissionId) {
|
||||||
Gym gym = gymRepository.findByCompoundId(id)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
ExerciseSubmission submission = exerciseSubmissionRepository.findById(submissionId)
|
ExerciseSubmission submission = exerciseSubmissionRepository.findById(submissionId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
if (!submission.getGym().getId().equals(gym.getId())) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
||||||
return new ExerciseSubmissionResponse(submission);
|
return new ExerciseSubmissionResponse(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public void streamVideo(CompoundGymId compoundId, long submissionId, HttpServletResponse response) {
|
public void streamVideo(String submissionId, HttpServletResponse response) {
|
||||||
// TODO: Make a faster way to stream videos, should be one DB call instead of this mess.
|
ExerciseSubmissionVideoFile videoFile = submissionVideoFileRepository.findByCompletedSubmissionId(submissionId)
|
||||||
Gym gym = gymRepository.findByCompoundId(compoundId)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
ExerciseSubmission submission = exerciseSubmissionRepository.findById(submissionId)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
if (!submission.getGym().getId().equals(gym.getId())) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
||||||
Set<ExerciseSubmission.Status> validStatuses = Set.of(
|
|
||||||
ExerciseSubmission.Status.COMPLETED,
|
|
||||||
ExerciseSubmission.Status.VERIFIED
|
|
||||||
);
|
|
||||||
if (!validStatuses.contains(submission.getStatus())) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
ExerciseSubmissionVideoFile videoFile = submissionVideoFileRepository.findBySubmission(submission)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
response.setContentType(videoFile.getFile().getMimeType());
|
response.setContentType(videoFile.getFile().getMimeType());
|
||||||
response.setContentLengthLong(videoFile.getFile().getSize());
|
response.setContentLengthLong(videoFile.getFile().getSize());
|
||||||
|
@ -140,7 +125,8 @@ public class ExerciseSubmissionService {
|
||||||
metricWeight = metricWeight.multiply(new BigDecimal("0.45359237"));
|
metricWeight = metricWeight.multiply(new BigDecimal("0.45359237"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ExerciseSubmission submission = exerciseSubmissionRepository.save(new ExerciseSubmission(
|
ExerciseSubmission submission = exerciseSubmissionRepository.saveAndFlush(new ExerciseSubmission(
|
||||||
|
ulid.nextULID(),
|
||||||
gym,
|
gym,
|
||||||
exercise,
|
exercise,
|
||||||
payload.name(),
|
payload.name(),
|
||||||
|
@ -179,7 +165,7 @@ public class ExerciseSubmissionService {
|
||||||
* </p>
|
* </p>
|
||||||
* @param submissionId The submission's id.
|
* @param submissionId The submission's id.
|
||||||
*/
|
*/
|
||||||
private void processSubmission(long submissionId) {
|
private void processSubmission(String submissionId) {
|
||||||
log.info("Starting processing of submission {}.", submissionId);
|
log.info("Starting processing of submission {}.", submissionId);
|
||||||
// First try and fetch the submission.
|
// First try and fetch the submission.
|
||||||
Optional<ExerciseSubmission> optionalSubmission = exerciseSubmissionRepository.findById(submissionId);
|
Optional<ExerciseSubmission> optionalSubmission = exerciseSubmissionRepository.findById(submissionId);
|
||||||
|
@ -250,6 +236,7 @@ public class ExerciseSubmissionService {
|
||||||
file
|
file
|
||||||
));
|
));
|
||||||
submission.setStatus(ExerciseSubmission.Status.COMPLETED);
|
submission.setStatus(ExerciseSubmission.Status.COMPLETED);
|
||||||
|
submission.setComplete(true);
|
||||||
exerciseSubmissionRepository.save(submission);
|
exerciseSubmissionRepository.save(submission);
|
||||||
// And delete the temporary files.
|
// And delete the temporary files.
|
||||||
try {
|
try {
|
||||||
|
@ -323,6 +310,7 @@ public class ExerciseSubmissionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then remove any files in the directory which don't correspond to a valid file in the db.
|
// Then remove any files in the directory which don't correspond to a valid file in the db.
|
||||||
|
if (Files.notExists(UploadService.SUBMISSION_TEMP_FILE_DIR)) return;
|
||||||
try (var s = Files.list(UploadService.SUBMISSION_TEMP_FILE_DIR)) {
|
try (var s = Files.list(UploadService.SUBMISSION_TEMP_FILE_DIR)) {
|
||||||
for (var path : s.toList()) {
|
for (var path : s.toList()) {
|
||||||
if (!tempFileRepository.existsByPath(path.toString())) {
|
if (!tempFileRepository.existsByPath(path.toString())) {
|
||||||
|
|
|
@ -0,0 +1,456 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sulky-modules - several general-purpose modules.
|
||||||
|
* Copyright (C) 2007-2019 Joern Huxhorn
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2019 Joern Huxhorn
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* https://github.com/ulid/spec
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.ShortClassName")
|
||||||
|
public class ULID
|
||||||
|
{
|
||||||
|
private static final char[] ENCODING_CHARS = {
|
||||||
|
'0','1','2','3','4','5','6','7','8','9',
|
||||||
|
'A','B','C','D','E','F','G','H','J','K',
|
||||||
|
'M','N','P','Q','R','S','T','V','W','X',
|
||||||
|
'Y','Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final byte[] DECODING_CHARS = {
|
||||||
|
// 0
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 8
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 16
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 24
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 32
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 40
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 48
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7,
|
||||||
|
// 56
|
||||||
|
8, 9, -1, -1, -1, -1, -1, -1,
|
||||||
|
// 64
|
||||||
|
-1, 10, 11, 12, 13, 14, 15, 16,
|
||||||
|
// 72
|
||||||
|
17, 1, 18, 19, 1, 20, 21, 0,
|
||||||
|
// 80
|
||||||
|
22, 23, 24, 25, 26, -1, 27, 28,
|
||||||
|
// 88
|
||||||
|
29, 30, 31, -1, -1, -1, -1, -1,
|
||||||
|
// 96
|
||||||
|
-1, 10, 11, 12, 13, 14, 15, 16,
|
||||||
|
// 104
|
||||||
|
17, 1, 18, 19, 1, 20, 21, 0,
|
||||||
|
// 112
|
||||||
|
22, 23, 24, 25, 26, -1, 27, 28,
|
||||||
|
// 120
|
||||||
|
29, 30, 31,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final int MASK = 0x1F;
|
||||||
|
private static final int MASK_BITS = 5;
|
||||||
|
private static final long TIMESTAMP_OVERFLOW_MASK = 0xFFFF_0000_0000_0000L;
|
||||||
|
private static final long TIMESTAMP_MSB_MASK = 0xFFFF_FFFF_FFFF_0000L;
|
||||||
|
private static final long RANDOM_MSB_MASK = 0xFFFFL;
|
||||||
|
|
||||||
|
private final Random random;
|
||||||
|
|
||||||
|
public ULID()
|
||||||
|
{
|
||||||
|
this(new SecureRandom());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ULID(Random random)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(random, "random must not be null!");
|
||||||
|
this.random = random;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendULID(StringBuilder stringBuilder)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(stringBuilder, "stringBuilder must not be null!");
|
||||||
|
internalAppendULID(stringBuilder, System.currentTimeMillis(), random);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nextULID()
|
||||||
|
{
|
||||||
|
return nextULID(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nextULID(long timestamp)
|
||||||
|
{
|
||||||
|
return internalUIDString(timestamp, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Value nextValue()
|
||||||
|
{
|
||||||
|
return nextValue(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Value nextValue(long timestamp)
|
||||||
|
{
|
||||||
|
return internalNextValue(timestamp, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next monotonic value. If an overflow happened while incrementing
|
||||||
|
* the random part of the given previous ULID value then the returned value will
|
||||||
|
* have a zero random part.
|
||||||
|
*
|
||||||
|
* @param previousUlid the previous ULID value.
|
||||||
|
* @return the next monotonic value.
|
||||||
|
*/
|
||||||
|
public Value nextMonotonicValue(Value previousUlid)
|
||||||
|
{
|
||||||
|
return nextMonotonicValue(previousUlid, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next monotonic value. If an overflow happened while incrementing
|
||||||
|
* the random part of the given previous ULID value then the returned value will
|
||||||
|
* have a zero random part.
|
||||||
|
*
|
||||||
|
* @param previousUlid the previous ULID value.
|
||||||
|
* @param timestamp the timestamp of the next ULID value.
|
||||||
|
* @return the next monotonic value.
|
||||||
|
*/
|
||||||
|
public Value nextMonotonicValue(Value previousUlid, long timestamp)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(previousUlid, "previousUlid must not be null!");
|
||||||
|
if(previousUlid.timestamp() == timestamp)
|
||||||
|
{
|
||||||
|
return previousUlid.increment();
|
||||||
|
}
|
||||||
|
return nextValue(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next monotonic value or empty if an overflow happened while incrementing
|
||||||
|
* the random part of the given previous ULID value.
|
||||||
|
*
|
||||||
|
* @param previousUlid the previous ULID value.
|
||||||
|
* @return the next monotonic value or empty if an overflow happened.
|
||||||
|
*/
|
||||||
|
public Optional<Value> nextStrictlyMonotonicValue(Value previousUlid)
|
||||||
|
{
|
||||||
|
return nextStrictlyMonotonicValue(previousUlid, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next monotonic value or empty if an overflow happened while incrementing
|
||||||
|
* the random part of the given previous ULID value.
|
||||||
|
*
|
||||||
|
* @param previousUlid the previous ULID value.
|
||||||
|
* @param timestamp the timestamp of the next ULID value.
|
||||||
|
* @return the next monotonic value or empty if an overflow happened.
|
||||||
|
*/
|
||||||
|
public Optional<Value> nextStrictlyMonotonicValue(Value previousUlid, long timestamp)
|
||||||
|
{
|
||||||
|
Value result = nextMonotonicValue(previousUlid, timestamp);
|
||||||
|
if(result.compareTo(previousUlid) < 1)
|
||||||
|
{
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Value parseULID(String ulidString)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(ulidString, "ulidString must not be null!");
|
||||||
|
if(ulidString.length() != 26)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("ulidString must be exactly 26 chars long.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String timeString = ulidString.substring(0, 10);
|
||||||
|
long time = internalParseCrockford(timeString);
|
||||||
|
if ((time & TIMESTAMP_OVERFLOW_MASK) != 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("ulidString must not exceed '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'!");
|
||||||
|
}
|
||||||
|
String part1String = ulidString.substring(10, 18);
|
||||||
|
String part2String = ulidString.substring(18);
|
||||||
|
long part1 = internalParseCrockford(part1String);
|
||||||
|
long part2 = internalParseCrockford(part2String);
|
||||||
|
|
||||||
|
long most = (time << 16) | (part1 >>> 24);
|
||||||
|
long least = part2 | (part1 << 40);
|
||||||
|
return new Value(most, least);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Value fromBytes(byte[] data)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(data, "data must not be null!");
|
||||||
|
if(data.length != 16)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("data must be 16 bytes in length!");
|
||||||
|
}
|
||||||
|
long mostSignificantBits = 0;
|
||||||
|
long leastSignificantBits = 0;
|
||||||
|
for (int i=0; i<8; i++)
|
||||||
|
{
|
||||||
|
mostSignificantBits = (mostSignificantBits << 8) | (data[i] & 0xff);
|
||||||
|
}
|
||||||
|
for (int i=8; i<16; i++)
|
||||||
|
{
|
||||||
|
leastSignificantBits = (leastSignificantBits << 8) | (data[i] & 0xff);
|
||||||
|
}
|
||||||
|
return new Value(mostSignificantBits, leastSignificantBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Value
|
||||||
|
implements Comparable<Value>, Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = -3563159514112487717L;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The most significant 64 bits of this ULID.
|
||||||
|
*/
|
||||||
|
private final long mostSignificantBits;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The least significant 64 bits of this ULID.
|
||||||
|
*/
|
||||||
|
private final long leastSignificantBits;
|
||||||
|
|
||||||
|
public Value(long mostSignificantBits, long leastSignificantBits)
|
||||||
|
{
|
||||||
|
this.mostSignificantBits = mostSignificantBits;
|
||||||
|
this.leastSignificantBits = leastSignificantBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most significant 64 bits of this ULID's 128 bit value.
|
||||||
|
*
|
||||||
|
* @return The most significant 64 bits of this ULID's 128 bit value
|
||||||
|
*/
|
||||||
|
public long getMostSignificantBits() {
|
||||||
|
return mostSignificantBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the least significant 64 bits of this ULID's 128 bit value.
|
||||||
|
*
|
||||||
|
* @return The least significant 64 bits of this ULID's 128 bit value
|
||||||
|
*/
|
||||||
|
public long getLeastSignificantBits() {
|
||||||
|
return leastSignificantBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public long timestamp()
|
||||||
|
{
|
||||||
|
return mostSignificantBits >>> 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toBytes()
|
||||||
|
{
|
||||||
|
byte[] result=new byte[16];
|
||||||
|
for (int i=0; i<8; i++)
|
||||||
|
{
|
||||||
|
result[i] = (byte)((mostSignificantBits >> ((7-i)*8)) & 0xFF);
|
||||||
|
}
|
||||||
|
for (int i=8; i<16; i++)
|
||||||
|
{
|
||||||
|
result[i] = (byte)((leastSignificantBits >> ((15-i)*8)) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Value increment()
|
||||||
|
{
|
||||||
|
long lsb = leastSignificantBits;
|
||||||
|
if(lsb != 0xFFFF_FFFF_FFFF_FFFFL)
|
||||||
|
{
|
||||||
|
return new Value(mostSignificantBits, lsb+1);
|
||||||
|
}
|
||||||
|
long msb = mostSignificantBits;
|
||||||
|
if((msb & RANDOM_MSB_MASK) != RANDOM_MSB_MASK)
|
||||||
|
{
|
||||||
|
return new Value(msb + 1, 0);
|
||||||
|
}
|
||||||
|
return new Value(msb & TIMESTAMP_MSB_MASK, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
long hilo = mostSignificantBits ^ leastSignificantBits;
|
||||||
|
return ((int)(hilo >> 32)) ^ (int) hilo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Value value = (Value) o;
|
||||||
|
|
||||||
|
return mostSignificantBits == value.mostSignificantBits
|
||||||
|
&& leastSignificantBits == value.leastSignificantBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Value val)
|
||||||
|
{
|
||||||
|
// The ordering is intentionally set up so that the ULIDs
|
||||||
|
// can simply be numerically compared as two numbers
|
||||||
|
return (this.mostSignificantBits < val.mostSignificantBits ? -1 :
|
||||||
|
(this.mostSignificantBits > val.mostSignificantBits ? 1 :
|
||||||
|
(this.leastSignificantBits < val.leastSignificantBits ? -1 :
|
||||||
|
(this.leastSignificantBits > val.leastSignificantBits ? 1 :
|
||||||
|
0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
char[] buffer = new char[26];
|
||||||
|
|
||||||
|
internalWriteCrockford(buffer, timestamp(), 10, 0);
|
||||||
|
long value = ((mostSignificantBits & 0xFFFFL) << 24);
|
||||||
|
long interim = (leastSignificantBits >>> 40);
|
||||||
|
value = value | interim;
|
||||||
|
internalWriteCrockford(buffer, value, 8, 10);
|
||||||
|
internalWriteCrockford(buffer, leastSignificantBits, 8, 18);
|
||||||
|
|
||||||
|
return new String(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* http://crockford.com/wrmg/base32.html
|
||||||
|
*/
|
||||||
|
static void internalAppendCrockford(StringBuilder builder, long value, int count)
|
||||||
|
{
|
||||||
|
for(int i = count-1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
int index = (int)((value >>> (i * MASK_BITS)) & MASK);
|
||||||
|
builder.append(ENCODING_CHARS[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static long internalParseCrockford(String input)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(input, "input must not be null!");
|
||||||
|
int length = input.length();
|
||||||
|
if(length > 12)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("input length must not exceed 12 but was "+length+"!");
|
||||||
|
}
|
||||||
|
|
||||||
|
long result = 0;
|
||||||
|
for(int i=0;i<length;i++)
|
||||||
|
{
|
||||||
|
char current = input.charAt(i);
|
||||||
|
byte value = -1;
|
||||||
|
if(current < DECODING_CHARS.length)
|
||||||
|
{
|
||||||
|
value = DECODING_CHARS[current];
|
||||||
|
}
|
||||||
|
if(value < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Illegal character '"+current+"'!");
|
||||||
|
}
|
||||||
|
result |= ((long)value) << ((length - 1 - i)*MASK_BITS);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* http://crockford.com/wrmg/base32.html
|
||||||
|
*/
|
||||||
|
static void internalWriteCrockford(char[] buffer, long value, int count, int offset)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int index = (int)((value >>> ((count - i - 1) * MASK_BITS)) & MASK);
|
||||||
|
buffer[offset+i] = ENCODING_CHARS[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String internalUIDString(long timestamp, Random random)
|
||||||
|
{
|
||||||
|
checkTimestamp(timestamp);
|
||||||
|
|
||||||
|
char[] buffer = new char[26];
|
||||||
|
|
||||||
|
internalWriteCrockford(buffer, timestamp, 10, 0);
|
||||||
|
// could use nextBytes(byte[] bytes) instead
|
||||||
|
internalWriteCrockford(buffer, random.nextLong(), 8, 10);
|
||||||
|
internalWriteCrockford(buffer, random.nextLong(), 8, 18);
|
||||||
|
|
||||||
|
return new String(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void internalAppendULID(StringBuilder builder, long timestamp, Random random)
|
||||||
|
{
|
||||||
|
checkTimestamp(timestamp);
|
||||||
|
|
||||||
|
internalAppendCrockford(builder, timestamp, 10);
|
||||||
|
// could use nextBytes(byte[] bytes) instead
|
||||||
|
internalAppendCrockford(builder, random.nextLong(), 8);
|
||||||
|
internalAppendCrockford(builder, random.nextLong(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Value internalNextValue(long timestamp, Random random)
|
||||||
|
{
|
||||||
|
checkTimestamp(timestamp);
|
||||||
|
// could use nextBytes(byte[] bytes) instead
|
||||||
|
long mostSignificantBits = random.nextLong();
|
||||||
|
long leastSignificantBits = random.nextLong();
|
||||||
|
mostSignificantBits &= 0xFFFF;
|
||||||
|
mostSignificantBits |= (timestamp << 16);
|
||||||
|
return new Value(mostSignificantBits, leastSignificantBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkTimestamp(long timestamp)
|
||||||
|
{
|
||||||
|
if((timestamp & TIMESTAMP_OVERFLOW_MASK) != 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("ULID does not support timestamps after +10889-08-02T05:31:50.655Z!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue