added docs and annotation validation; fixed pagination

This commit is contained in:
Brian Long 2023-02-10 17:21:47 -05:00
parent 9a43692415
commit 974426c5da
10 changed files with 70 additions and 109 deletions

@ -7,7 +7,6 @@ import com.poststats.transformer.impl.DaoConverter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
@ -17,9 +16,7 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.SecurityContext;
import org.slf4j.Logger;
/**
@ -28,14 +25,11 @@ import org.slf4j.Logger;
@RequestScoped
@Path("/golf/course/{courseId}")
@Tag(name = "Course API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(responseCode = "401", description = "You are not authorized"),
@ApiResponse(responseCode = "403", description = "You are not identified or authenticated")
})
@ApiResponse(responseCode = "200", description = "Success")
@ApiResponse(responseCode = "404", description = "A golf course with the specified ID could not be found")
public class CourseApi {
@Parameter(name = "courseId", description = "A unique identifier for a golf course")
@Parameter(description = "A unique identifier for a golf course")
@PathParam("courseId")
private int courseId;
@ -59,10 +53,7 @@ public class CourseApi {
summary = "Retrieves limited meta-data about a course.",
description = "Retreives name, location, and other direct meta-data about the specified course."
)
@ApiResponses({
@ApiResponse(responseCode = "404", description = "A golf course with the specified ID could not be found")
})
public Course get(@Context SecurityContext scontext) {
public Course get() {
FlexMap row = this.courseService.get(this.courseId);
if (row == null)
throw new WebApplicationException("Course not found", Status.NOT_FOUND);

@ -16,6 +16,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Positive;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@ -25,7 +30,6 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response.Status;
import java.util.List;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
/**
@ -34,6 +38,11 @@ import org.slf4j.Logger;
@RequestScoped
@Path("/golf/courses")
@Tag(name = "Course API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(responseCode = "400", description = "A query parameter is not valid"),
@ApiResponse(responseCode = "404", description = "No matching golf courses were found")
})
public class CoursesApi {
@Inject
@ -57,22 +66,16 @@ public class CoursesApi {
summary = "Searches for golf courses by their name.",
description = "Searches for golf courses by their name, excluding prefix (e.g. The) and suffix (e.g. Golf Club), returning limited meta-data about each matched golf course."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(responseCode = "404", description = "No matching golf courses were found"),
@ApiResponse(responseCode = "411", description = "The page number must be at least 1"),
@ApiResponse(responseCode = "413", description = "The page size is too large")
})
@Parameters({
@Parameter(
name = "name",
required = true,
description = "A fragment or whole golf course name; the prefix and suffix are not included"
), @Parameter(name = "page", description = "A page number, starting at 1", example = "1"),
@Parameter(name = "perPage", description = "A page size", example = "10")
)
})
@Tag(name = "Search API")
public PagedCollection<List<Course>> searchByName(@QueryParam("name") String name, @BeanParam Pagination paging) {
public PagedCollection<List<Course>> searchByName(@QueryParam("name") String name,
@BeanParam @Valid Pagination paging) {
SubList<? extends FlexMap> rows = this.courseService.findByName(name, paging.getPage(), paging.getPerPage());
if (rows.isEmpty())
throw new WebApplicationException("No matching courses found", Status.NOT_FOUND);
@ -90,27 +93,19 @@ public class CoursesApi {
summary = "Searches for golf courses by their location.",
description = "Searches for golf courses by their major jurisdiction (e.g. US States), returning limited meta-data about each matched golf course."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(
responseCode = "404",
description = "The country, state, or matching golf courses were not found"
), @ApiResponse(responseCode = "411", description = "The page number must be at least 1"),
@ApiResponse(responseCode = "413", description = "The page size is too large")
})
@Parameters({
@Parameter(name = "country", required = true, description = "A country code", example = "US"),
@Parameter(name = "state", description = "A State or high-level jurisdiction", example = "FL"),
@Parameter(name = "page", description = "A page number, starting at 1", example = "1"),
@Parameter(name = "perPage", description = "A page size", example = "10")
@Parameter(name = "state", description = "A State or high-level jurisdiction", example = "FL")
})
@Tag(name = "Search API")
public PagedCollection<List<Course>> searchByJurisdiction(@PathParam("country") String country,
@PathParam("state") String state, @BeanParam Pagination paging) {
@PathParam("state") String state, @BeanParam @Valid Pagination paging) {
SubList<? extends FlexMap> rows = this.courseService.findByJurisdiction(country, state, paging.getPage(),
paging.getPerPage());
if (rows.isEmpty())
throw new WebApplicationException("No matching golf courses found", Status.NOT_FOUND);
this.logger.trace("found: {} [{}-{}] of {}", rows.size(), rows.getStartIndex() + 1, rows.getEndIndex(),
rows.getTotal());
return this.converter.convertValue(rows, Course.class).withPage(paging.getPage())
.withPerPage(paging.getPerPage());
@ -123,15 +118,6 @@ public class CoursesApi {
summary = "Searches for golf courses by their location.",
description = "Searches for golf courses by their major jurisdiction (e.g. US States), returning limited meta-data about each matched golf course."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(
responseCode = "400",
description = "A latitude/longitude/radius was outside its natural limits"
), @ApiResponse(responseCode = "404", description = "No golf courses were found"),
@ApiResponse(responseCode = "411", description = "The page number must be at least 1"),
@ApiResponse(responseCode = "413", description = "The page size is too large")
})
@Parameters({
@Parameter(
name = "latitude",
@ -144,30 +130,19 @@ public class CoursesApi {
required = true,
description = "A longitude in decimal degrees",
example = "-81.34891"
), @Parameter(name = "radius", description = "A search radius in miles", example = "10"),
@Parameter(name = "page", description = "A page number, starting at 1", example = "1"),
@Parameter(name = "perPage", description = "A page size", example = "20")
), @Parameter(name = "radius", description = "A search radius in miles", example = "10")
})
@Tag(name = "Search API")
public PagedCollection<List<Course>> searchByJurisdiction(@QueryParam("latitude") double latitude,
@QueryParam("longitude") double longitude, @QueryParam("radius") Integer radiusInMiles,
@BeanParam Pagination paging) {
if (latitude < -90.0 || latitude > 90.0)
throw new WebApplicationException("A latitude of -90 to 90 is expected", HttpStatus.SC_BAD_REQUEST);
if (longitude < -180.0 || longitude > 180.0)
throw new WebApplicationException("A longitude of -180 to 180 is expected", HttpStatus.SC_BAD_REQUEST);
if (radiusInMiles == null)
radiusInMiles = 10;
if (radiusInMiles <= 0)
throw new WebApplicationException("A positive radius is expected", HttpStatus.SC_BAD_REQUEST);
if (radiusInMiles > 100)
throw new WebApplicationException("A radius under 100 miles is expected", HttpStatus.SC_BAD_REQUEST);
public PagedCollection<List<Course>> searchByJurisdiction(
@QueryParam("latitude") @DecimalMin("-90.0") @DecimalMax("90.0") double latitude,
@QueryParam("longitude") @DecimalMin("-180.0") @DecimalMax("180.0") double longitude,
@QueryParam("radius") @Positive @Max(100) Integer radiusInMiles, @BeanParam @Valid Pagination paging) {
SubList<? extends FlexMap> rows = this.courseService.findByLocation(latitude, longitude, radiusInMiles,
paging.getPage(), paging.getPerPage());
if (rows.isEmpty())
throw new WebApplicationException("No matching facilities found", Status.NOT_FOUND);
this.logger.trace("found: {} [{}-{}] of {}", rows.size(), rows.getStartIndex() + 1, rows.getEndIndex(),
rows.getTotal());
return this.converter.convertValue(rows, Course.class).withPage(paging.getPage())
.withPerPage(paging.getPerPage());

@ -2,10 +2,11 @@ package com.poststats.golf.service;
import com.brianlong.util.FlexMap;
import com.brianlong.util.SubList;
import com.poststats.service.CacheableService;
import java.util.Collection;
import java.util.Map;
public interface CourseService {
public interface CourseService extends CacheableService<Integer> {
/**
* This method retrieves meta-data about the specified course.

@ -1,11 +1,12 @@
package com.poststats.golf.service;
import com.brianlong.util.FlexMap;
import com.poststats.service.CacheableService;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public interface EventService {
public interface EventService extends CacheableService<Long> {
/**
* This method retrieves meta-data about the specified event.

@ -2,10 +2,11 @@ package com.poststats.golf.service;
import com.brianlong.util.FlexMap;
import com.poststats.golf.security.AuthenticatedPerson;
import com.poststats.service.CacheableService;
import java.util.Collection;
import java.util.Map;
public interface PersonService {
public interface PersonService extends CacheableService<Long> {
/**
* This method builds a `UserPrincipal` object about the golfer.

@ -1,10 +1,11 @@
package com.poststats.golf.service;
import com.brianlong.util.FlexMap;
import com.poststats.service.CacheableService;
import java.util.Collection;
import java.util.Map;
public interface SeriesService {
public interface SeriesService extends CacheableService<Integer> {
/**
* This method retrieves meta-data about the specified series.

@ -18,10 +18,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ApplicationScoped
public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements CourseService {
@ -41,8 +38,8 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
FlexMap row = super.get(Integer.valueOf(courseId));
if (row == null)
return null;
row.put("facility", this.facilityService.get(row.getInteger("facilityID")));
this.injectCache(row, this.facilityService, "facilityID", "facility");
return row;
}
@ -50,19 +47,14 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
public Map<Integer, ? extends FlexMap> get(Collection<Integer> courseIds) {
Map<Integer, ? extends FlexMap> rows = super.get(courseIds);
// bulk fetch series from cache/db
Set<Integer> facilityIds = new HashSet<>();
for (Entry<Integer, ? extends FlexMap> row : rows.entrySet())
facilityIds.add(row.getValue().getInteger("facilityId"));
Map<Integer, ? extends FlexMap> facilities = this.facilityService.get(facilityIds);
for (Entry<Integer, ? extends FlexMap> row : rows.entrySet())
row.getValue().put("facility", facilities.get(row.getValue().getInteger("facilityId")));
this.injectCache(rows.values(), this.facilityService, "facilityID", "facility");
return rows;
}
@Override
public SubList<? extends FlexMap> findByName(String name, int page, int perPage) {
SubList<? extends FlexMap> rows;
try {
FlexPreparedStatement fps = this.sqlSelectByName.buildPreparedStatement();
try {
@ -70,31 +62,34 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
fps.setVarchar(2, "%" + name + "%");
fps.setSmallintU(3, (page - 1) * perPage);
fps.setSmallintU(4, perPage);
return (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
rows = (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
} finally {
fps.close();
}
} catch (SQLException se) {
throw new ServiceException(se);
}
this.injectCache(rows, this.facilityService, "facilityID", "facility");
return rows;
}
@Inject
@Named(Constants.STATEMENT_PROVIDER_GOLF)
@Statement(
feature = ResultSubSetFeature.class,
sql = "SELECT FP.*, FS.*, F.*, CP.prefix coursePrefix, C.* "
sql = "SELECT CP.*, C.* "
+ "FROM ~g~.Course C "
+ " LEFT JOIN ~g~.CoursePrefix CP ON (C.prefixID=CP.prefixID) "
+ " INNER JOIN ~p~.Facility F ON (C.facilityID=F.facilityID) "
+ " LEFT JOIN ~p~.FacilityPrefix FP ON (F.prefixID=FP.prefixID) "
+ " LEFT JOIN ~p~.FacilitySuffix FS ON (F.suffixID=FS.suffixID) "
+ "WHERE course LIKE ? OR facility LIKE ?"
+ "WHERE course LIKE ? OR facility LIKE ? "
)
private StatementProvider sqlSelectByName;
@Override
public SubList<? extends FlexMap> findByJurisdiction(String country, String state, int page, int perPage) {
SubList<? extends FlexMap> rows;
try {
FlexPreparedStatement fps = this.sqlSelectByJurisdiction.buildPreparedStatement();
try {
@ -102,26 +97,28 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
fps.setVarchar(2, state);
fps.setSmallintU(3, (page - 1) * perPage);
fps.setSmallintU(4, perPage);
return (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
rows = (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
} finally {
fps.close();
}
} catch (SQLException se) {
throw new ServiceException(se);
}
this.injectCache(rows, this.facilityService, "facilityID", "facility");
return rows;
}
@Inject
@Named(Constants.STATEMENT_PROVIDER_GOLF)
@Statement(
feature = ResultSubSetFeature.class,
sql = "SELECT FP.*, FS.*, F.*, CP.prefix coursePrefix, C.* "
sql = "SELECT CP.*, C.* "
+ "FROM ~g~.Course C "
+ " LEFT JOIN ~g~.CoursePrefix CP ON (C.prefixID=CP.prefixID) "
+ " INNER JOIN ~p~.Facility F ON (C.facilityID=F.facilityID) "
+ " LEFT JOIN ~p~.FacilityPrefix FP ON (F.prefixID=FP.prefixID) "
+ " LEFT JOIN ~p~.FacilitySuffix FS ON (F.suffixID=FS.suffixID) "
+ "WHERE addrcountry=? AND addrstate=? AND C.deadline IS NULL AND F.deadline IS NULL "
+ "WHERE F.addrcountry=? AND F.addrstate=? "
+ " AND C.deadline IS NULL "
)
private StatementProvider sqlSelectByJurisdiction;
@ -129,6 +126,7 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
public SubList<? extends FlexMap> findByLocation(double latitude, double longitude, int radiusInMiles, int page,
int perPage) {
double degrees = PostStatsSQL.miles2degrees(radiusInMiles);
SubList<? extends FlexMap> rows;
try {
FlexPreparedStatement fps = this.sqlSelectByGeolocation.buildPreparedStatement();
@ -139,27 +137,28 @@ public class CourseServiceDAO extends CacheableServiceDAO<Integer> implements Co
fps.setDouble(4, longitude + degrees);
fps.setSmallintU(5, (page - 1) * perPage);
fps.setSmallintU(6, perPage);
return (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
rows = (SubList<? extends FlexMap>) fps.executeQuery().getAllRows();
} finally {
fps.close();
}
} catch (SQLException se) {
throw new ServiceException(se);
}
this.injectCache(rows, this.facilityService, "facilityID", "facility");
return rows;
}
@Inject
@Named(Constants.STATEMENT_PROVIDER_GOLF)
@Statement(
feature = ResultSubSetFeature.class,
sql = "SELECT FP.*, FS.*, F.*, CP.prefix coursePrefix, C.* "
sql = "SELECT CP.*, C.* "
+ "FROM ~g~.Course C "
+ " LEFT JOIN ~g~.CoursePrefix CP ON (C.prefixID=CP.prefixID) "
+ " INNER JOIN ~p~.Facility F ON (C.facilityID=F.facilityID) "
+ " LEFT JOIN ~p~.FacilityPrefix FP ON (F.prefixID=FP.prefixID) "
+ " LEFT JOIN ~p~.FacilitySuffix FS ON (F.suffixID=FS.suffixID) "
+ "WHERE F.latitude>=? AND F.latitude<=? AND F.longitude>=? AND F.longitude<=? "
+ " AND C.deadline IS NULL AND F.deadline IS NULL "
+ " AND C.deadline IS NULL "
)
private StatementProvider sqlSelectByGeolocation;

@ -35,8 +35,9 @@ public class EventRoundServiceDAO extends CacheableServiceDAO<Long> implements E
FlexMap row = super.get(Long.valueOf(eventId));
if (row == null)
return null;
row.put("course", this.courseService.get(row.getInteger("courseID")));
this.injectCache(row, this.courseService, "courseID", "course");
row.put("course", this.courseService.get(row.getInteger("courseID")));
return row;
}

@ -17,7 +17,6 @@ import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ApplicationScoped
@ -38,8 +37,8 @@ public class EventServiceDAO extends CacheableServiceDAO<Long> implements EventS
FlexMap row = super.get(Long.valueOf(eventId));
if (row == null)
return null;
row.put("series", this.seriesService.get(row.getInteger("seriesID")));
this.injectCache(row, this.seriesService, "seriesID", "series");
return row;
}
@ -47,14 +46,7 @@ public class EventServiceDAO extends CacheableServiceDAO<Long> implements EventS
public Map<Long, ? extends FlexMap> get(Collection<Long> eventIds) {
Map<Long, ? extends FlexMap> rows = super.get(eventIds);
// bulk fetch series from cache/db
Set<Integer> seriesIds = new HashSet<>();
for (Entry<Long, ? extends FlexMap> row : rows.entrySet())
seriesIds.add(row.getValue().getInteger("seriesId"));
Map<Integer, ? extends FlexMap> serieses = this.seriesService.get(seriesIds);
for (Entry<Long, ? extends FlexMap> row : rows.entrySet())
row.getValue().put("series", serieses.get(row.getValue().getInteger("seriesID")));
this.injectCache(rows.values(), this.seriesService, "seriesID", "series");
return rows;
}

@ -25,7 +25,6 @@ import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ApplicationScoped
@ -74,16 +73,16 @@ public class PersonServiceDAO extends CacheableServiceDAO<Long> implements Perso
FlexMap row = this.get(Long.valueOf(personId));
if (row == null)
return null;
row.put("person", this.poststatsPersonService.get(personId));
this.injectCache(row, this.poststatsPersonService, "personID", "person");
return row;
}
@Override
public Map<Long, ? extends FlexMap> get(Collection<Long> ids) {
Map<Long, ? extends FlexMap> rows = super.get(ids);
Map<Long, ? extends FlexMap> persons = this.poststatsPersonService.get(ids);
for (Entry<Long, ? extends FlexMap> row : rows.entrySet())
row.getValue().put("person", persons.get(row.getKey()));
this.injectCache(rows.values(), this.poststatsPersonService, "personID", "person");
return rows;
}