diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java index d12a9f8e6e..c4756c7992 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java @@ -184,7 +184,8 @@ public Response getAssignments(@Context final HttpServletRequest request) { // Gather all gameboards we need to augment for the assignments in a single query List gameboardIds = assignments.stream().map(AssignmentDTO::getGameboardId).collect(Collectors.toList()); - Map gameboardsMap = this.gameManager.getGameboardsWithAttempts(gameboardIds, currentlyLoggedInUser) + Map gameboardsMap = this.gameManager + .getGameboardsWithAttemptsAndUserSavedInformation(gameboardIds, currentlyLoggedInUser) .stream().collect(Collectors.toMap(GameboardDTO::getId, Function.identity())); // we want to populate gameboard details for the assignment DTO. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java index bba638e41e..0e4d355946 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java @@ -1007,7 +1007,14 @@ public final Response getBookDetailPage(@Context final Request request, List additionalGameboardIds = Objects.requireNonNullElse(bookPageDO.getExtensionGameboards(), Collections.emptyList()); List allGameboardIds = Stream.of(gameboardIds, additionalGameboardIds) .flatMap(Collection::stream).collect(Collectors.toList()); - List linkedGameboards = gameManager.getGameboards(allGameboardIds); + + List linkedGameboards; + try { + RegisteredUserDTO registeredUser = userManager.getCurrentRegisteredUser(httpServletRequest); + linkedGameboards = gameManager.getGameboardsWithUserSavedInformation(allGameboardIds, registeredUser); + } catch (final NoUserLoggedInException e) { + linkedGameboards = gameManager.getGameboards(allGameboardIds); + } bookPageDTO.setGameboards(linkedGameboards .stream() diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 6f1259925b..a87c12a6e1 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -60,7 +60,6 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -294,7 +293,7 @@ public final List getGameboards(final List gameboardIds, final Map>> userQuestionAttempts) throws SegueDatabaseException, ContentManagerException { if (null == gameboardIds || gameboardIds.isEmpty()) { - return new ArrayList<>(); + return Collections.emptyList(); } List gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds); @@ -306,10 +305,34 @@ public final List getGameboards(final List gameboardIds, } /** - * Get a list of gameboards by their ids, augmented with attempt information. + * Get a list of gameboards by their ids, augmented with whether the user has it saved to their boards. * - * Note: These gameboards WILL be augmented with user attempt information, but not whether the gameboard is saved - * to the user's boards. + * @param gameboardIds + * - to look up. + * @param user + * - the user to augment the gameboard for. + * @return the gameboards or null. + * @throws SegueDatabaseException + * - if there is a problem retrieving the gameboards in the database + */ + public final List getGameboardsWithUserSavedInformation(final List gameboardIds, final RegisteredUserDTO user) + throws SegueDatabaseException { + if (null == gameboardIds || gameboardIds.isEmpty()) { + return Collections.emptyList(); + } + + List gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds); + Set savedBoardIds = this.gameboardPersistenceManager.getGameboardIdsLinkedToUser(user.getId(), gameboardIds); + + for (GameboardDTO gameboard : gameboardsByIds) { + gameboard.setSavedToCurrentUser(savedBoardIds.contains(gameboard.getId())); + } + + return gameboardsByIds; + } + + /** + * Get a list of gameboards by their ids, augmented with attempt information AND whether the user has it saved to their boards. * * @param gameboardIds * - to look up. @@ -321,18 +344,23 @@ public final List getGameboards(final List gameboardIds, * @throws ContentManagerException * - if there is a problem resolving content */ - public final List getGameboardsWithAttempts(final List gameboardIds, final RegisteredUserDTO user) + public final List getGameboardsWithAttemptsAndUserSavedInformation(final List gameboardIds, final RegisteredUserDTO user) throws SegueDatabaseException, ContentManagerException { if (null == gameboardIds || gameboardIds.isEmpty()) { - return new ArrayList<>(); + return Collections.emptyList(); } List gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds); - List questionPageIds = gameboardsByIds.stream().map(GameboardDTO::getContents).flatMap(Collection::stream).map(GameboardItem::getId).collect(Collectors.toList()); + List questionPageIds = gameboardsByIds.stream().map(GameboardDTO::getContents).flatMap(Collection::stream) + .map(GameboardItem::getId).collect(Collectors.toList()); + Map>> userQuestionAttempts = questionManager.getMatchingLightweightQuestionAttempts(user, questionPageIds); - for (GameboardDTO gb : gameboardsByIds) { - augmentGameboardWithQuestionAttemptInformation(gb, userQuestionAttempts); + Set savedBoardIds = this.gameboardPersistenceManager.getGameboardIdsLinkedToUser(user.getId(), gameboardIds); + + for (GameboardDTO gameboard : gameboardsByIds) { + augmentGameboardWithQuestionAttemptInformation(gameboard, userQuestionAttempts); + gameboard.setSavedToCurrentUser(savedBoardIds.contains(gameboard.getId())); } return gameboardsByIds; @@ -384,10 +412,15 @@ public final GameboardDTO getGameboard(final String gameboardId, final AbstractS final Map>> userQuestionAttempts) throws SegueDatabaseException, ContentManagerException { + GameboardDTO gameboard = this.gameboardPersistenceManager.getGameboardById(gameboardId); + gameboard = augmentGameboardWithQuestionAttemptInformation(gameboard, userQuestionAttempts); // we need to augment the DTO with whether this gameboard is in a users my boards list. - return augmentGameboardWithQuestionAttemptInformationAndUserInformation( - this.gameboardPersistenceManager.getGameboardById(gameboardId), userQuestionAttempts, user); + if (user instanceof RegisteredUserDTO registeredUser) { + gameboard.setSavedToCurrentUser(isBoardLinkedToUser(registeredUser, gameboardId)); + } + + return gameboard; } /** @@ -748,36 +781,6 @@ public static List getAllMarkableDOQuestionPartsDFSOrder(final Content return filterDOQuestionParts(dfs); } - /** - * Augments the gameboards with question attempt information AND whether or not the user has it in their boards. - * - * @param gameboardDTO - * - the DTO of the gameboard. - * @param questionAttemptsFromUser - * - the users question attempt data. - * @param user - * - the user to check whether the board is in their boards list - * @return Augmented Gameboard. - * @throws SegueDatabaseException - * - if there is an error retrieving the content requested. - * @throws ContentManagerException - * - if there is an error retrieving the content requested. - */ - private GameboardDTO augmentGameboardWithQuestionAttemptInformationAndUserInformation(final GameboardDTO gameboardDTO, - final Map>> questionAttemptsFromUser, - final AbstractSegueUserDTO user) - throws SegueDatabaseException, ContentManagerException { - if (user instanceof RegisteredUserDTO registeredUser) { - gameboardDTO - .setSavedToCurrentUser(this.isBoardLinkedToUser(registeredUser, gameboardDTO.getId())); - } - - this.augmentGameboardWithQuestionAttemptInformation(gameboardDTO, questionAttemptsFromUser); - - return gameboardDTO; - } - - /** * Augments the gameboards with question attempt information NOT whether the user has it in their my board page. * @@ -957,8 +960,10 @@ private static List depthFirstDOQuestionSearch(final Content c, final L */ private boolean isBoardLinkedToUser(final RegisteredUserDTO user, final String gameboardId) throws SegueDatabaseException { - return this.gameboardPersistenceManager.isBoardLinkedToUser(user.getId(), gameboardId); - } + Set linkedIds = this.gameboardPersistenceManager + .getGameboardIdsLinkedToUser(user.getId(), Collections.singleton(gameboardId)); + return linkedIds.contains(gameboardId); + } /** * Store a gameboard in a public location. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/GameboardPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/GameboardPersistenceManager.java index 95da801764..33fed3a785 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/GameboardPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/GameboardPersistenceManager.java @@ -23,6 +23,7 @@ import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,6 +78,7 @@ public class GameboardPersistenceManager { private static final Logger log = LoggerFactory.getLogger(GameboardPersistenceManager.class); private static final Long GAMEBOARD_TTL_MINUTES = 30L; private static final int GAMEBOARD_ITEM_MAP_BATCH_SIZE = 1000; + private static final int MAX_GAMEBOARD_IDS_TO_MATCH = 50; private final PostgresSqlDb database; private final Cache gameboardNonPersistentStorage; @@ -260,34 +262,70 @@ public boolean isPermanentlyStored(final String gameboardIdToTest) throws SegueD } /** - * Determines whether a given game board is already in a users my boards list. Only boards in persistent storage - * should be linked to a user. - * - * @param userId - * to check - * @param gameboardId - * to look up - * @return true if it is false if not - * @throws SegueDatabaseException - * if there is a database error + * Determines which of a given collection of gameboard IDs are in a user's saved gameboards. + * + * IMPORTANT: If too many gameboard IDs are provided, this instead returns ALL of the user's saved board IDs. + * + * @param userId the user to check saved gameboards for + * @param gameboardIds the list of gameboard IDs to check + * @return the subset of the provided gameboard IDs that the user has saved OR all of a user's saved gameboard IDs + * if the provided list of gameboard IDs is too long. + * @throws SegueDatabaseException if there is a database error */ - public boolean isBoardLinkedToUser(final Long userId, final String gameboardId) throws SegueDatabaseException { - if (userId == null || gameboardId == null) { - return false; + public Set getGameboardIdsLinkedToUser(final Long userId, final Collection gameboardIds) throws SegueDatabaseException { + if (gameboardIds.size() > MAX_GAMEBOARD_IDS_TO_MATCH) { + log.debug("Attempting to match too many ({}) gameboard IDs; returning all saved boards for the user instead!", + gameboardIds.size()); + return getGameboardIdsLinkedToUser(userId); } - String query = "SELECT COUNT(*) AS TOTAL FROM user_gameboards WHERE user_id = ? AND gameboard_id = ?;"; + Set linkedGameboardIds = Sets.newHashSet(); + + String query = "SELECT gameboard_id FROM user_gameboards WHERE user_id = ? AND gameboard_id = ANY(?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query); ) { pst.setLong(1, userId); - pst.setObject(2, gameboardId); + + Array gameboardIdArray = conn.createArrayOf("TEXT", gameboardIds.toArray()); + pst.setArray(2, gameboardIdArray); try (ResultSet results = pst.executeQuery()) { - results.next(); - return results.getInt("TOTAL") == 1; + while (results.next()) { + linkedGameboardIds.add(results.getString("gameboard_id")); + } + return linkedGameboardIds; + } finally { + gameboardIdArray.free(); } - } catch (SQLException e) { + } catch (final SQLException e) { + throw new SegueDatabaseException("Postgres exception", e); + } + } + + /** + * Gets the gameboard IDs that a user has in their saved boards. + * + * @param userId the user to check saved gameboards for + * @return a set of the user's saved gameboard IDs + * @throws SegueDatabaseException if there is a database error + */ + public Set getGameboardIdsLinkedToUser(final Long userId) throws SegueDatabaseException { + Set linkedGameboardIds = Sets.newHashSet(); + + String query = "SELECT gameboard_id FROM user_gameboards WHERE user_id = ?;"; + try (Connection conn = database.getDatabaseConnection(); + PreparedStatement pst = conn.prepareStatement(query); + ) { + pst.setLong(1, userId); + + try (ResultSet results = pst.executeQuery()) { + while (results.next()) { + linkedGameboardIds.add(results.getString("gameboard_id")); + } + return linkedGameboardIds; + } + } catch (final SQLException e) { throw new SegueDatabaseException("Postgres exception", e); } }