Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> gameboardIds = assignments.stream().map(AssignmentDTO::getGameboardId).collect(Collectors.toList());
Map<String, GameboardDTO> gameboardsMap = this.gameManager.getGameboardsWithAttempts(gameboardIds, currentlyLoggedInUser)
Comment thread
jsharkey13 marked this conversation as resolved.
Map<String, GameboardDTO> gameboardsMap = this.gameManager
.getGameboardsWithAttemptsAndUserSavedInformation(gameboardIds, currentlyLoggedInUser)
.stream().collect(Collectors.toMap(GameboardDTO::getId, Function.identity()));

// we want to populate gameboard details for the assignment DTO.
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,14 @@ public final Response getBookDetailPage(@Context final Request request,
List<String> additionalGameboardIds = Objects.requireNonNullElse(bookPageDO.getExtensionGameboards(), Collections.emptyList());
List<String> allGameboardIds = Stream.of(gameboardIds, additionalGameboardIds)
.flatMap(Collection::stream).collect(Collectors.toList());
List<GameboardDTO> linkedGameboards = gameManager.getGameboards(allGameboardIds);

List<GameboardDTO> linkedGameboards;
try {
RegisteredUserDTO registeredUser = userManager.getCurrentRegisteredUser(httpServletRequest);
linkedGameboards = gameManager.getGameboardsWithUserSavedInformation(allGameboardIds, registeredUser);
} catch (final NoUserLoggedInException e) {
linkedGameboards = gameManager.getGameboards(allGameboardIds);
}

bookPageDTO.setGameboards(linkedGameboards
.stream()
Expand Down
93 changes: 49 additions & 44 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -294,7 +293,7 @@ public final List<GameboardDTO> getGameboards(final List<String> gameboardIds,
final Map<String, Map<String, List<QuestionValidationResponse>>> userQuestionAttempts)
throws SegueDatabaseException, ContentManagerException {
if (null == gameboardIds || gameboardIds.isEmpty()) {
return new ArrayList<>();
return Collections.emptyList();
}

List<GameboardDTO> gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds);
Expand All @@ -306,10 +305,34 @@ public final List<GameboardDTO> getGameboards(final List<String> 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<GameboardDTO> getGameboardsWithUserSavedInformation(final List<String> gameboardIds, final RegisteredUserDTO user)
throws SegueDatabaseException {
if (null == gameboardIds || gameboardIds.isEmpty()) {
return Collections.emptyList();
}

List<GameboardDTO> gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds);
Set<String> 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.
Expand All @@ -321,18 +344,23 @@ public final List<GameboardDTO> getGameboards(final List<String> gameboardIds,
* @throws ContentManagerException
* - if there is a problem resolving content
*/
public final List<GameboardDTO> getGameboardsWithAttempts(final List<String> gameboardIds, final RegisteredUserDTO user)
public final List<GameboardDTO> getGameboardsWithAttemptsAndUserSavedInformation(final List<String> gameboardIds, final RegisteredUserDTO user)
throws SegueDatabaseException, ContentManagerException {
if (null == gameboardIds || gameboardIds.isEmpty()) {
return new ArrayList<>();
return Collections.emptyList();
}

List<GameboardDTO> gameboardsByIds = this.gameboardPersistenceManager.getGameboardsByIds(gameboardIds);
List<String> questionPageIds = gameboardsByIds.stream().map(GameboardDTO::getContents).flatMap(Collection::stream).map(GameboardItem::getId).collect(Collectors.toList());
List<String> questionPageIds = gameboardsByIds.stream().map(GameboardDTO::getContents).flatMap(Collection::stream)
.map(GameboardItem::getId).collect(Collectors.toList());

Map<String, Map<String, List<LightweightQuestionValidationResponse>>> userQuestionAttempts =
questionManager.getMatchingLightweightQuestionAttempts(user, questionPageIds);
for (GameboardDTO gb : gameboardsByIds) {
augmentGameboardWithQuestionAttemptInformation(gb, userQuestionAttempts);
Set<String> savedBoardIds = this.gameboardPersistenceManager.getGameboardIdsLinkedToUser(user.getId(), gameboardIds);

for (GameboardDTO gameboard : gameboardsByIds) {
augmentGameboardWithQuestionAttemptInformation(gameboard, userQuestionAttempts);
gameboard.setSavedToCurrentUser(savedBoardIds.contains(gameboard.getId()));
}

return gameboardsByIds;
Expand Down Expand Up @@ -384,10 +412,15 @@ public final GameboardDTO getGameboard(final String gameboardId, final AbstractS
final Map<String, ? extends Map<String, ? extends List<? extends LightweightQuestionValidationResponse>>> 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;
}

/**
Expand Down Expand Up @@ -748,36 +781,6 @@ public static List<Question> 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<String, ? extends Map<String, ? extends List<? extends LightweightQuestionValidationResponse>>> 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.
*
Expand Down Expand Up @@ -957,8 +960,10 @@ private static List<Content> 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<String> linkedIds = this.gameboardPersistenceManager
.getGameboardIdsLinkedToUser(user.getId(), Collections.singleton(gameboardId));
return linkedIds.contains(gameboardId);
}

/**
* Store a gameboard in a public location.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, GameboardDO> gameboardNonPersistentStorage;
Expand Down Expand Up @@ -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<String> getGameboardIdsLinkedToUser(final Long userId, final Collection<String> 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<String> 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<String> getGameboardIdsLinkedToUser(final Long userId) throws SegueDatabaseException {
Set<String> 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);
}
}
Expand Down
Loading