Java tutorial
/** * Copyright 2015 Stephen Cummins * * 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. */ package uk.ac.cam.cl.dtg.segue.quiz; import static uk.ac.cam.cl.dtg.segue.api.Constants.ANONYMOUS_SESSION_DURATION_IN_MINUTES; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.api.client.util.Lists; import com.google.api.client.util.Maps; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.inject.Inject; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; import uk.ac.cam.cl.dtg.segue.database.PostgresSqlDb; import uk.ac.cam.cl.dtg.segue.dos.QuestionValidationResponse; /** * @author sac92 * */ public class PgQuestionAttempts implements IQuestionAttemptManager { private static final Logger log = LoggerFactory.getLogger(PgQuestionAttempts.class); private final PostgresSqlDb database; private final ObjectMapper objectMapper; // cache of anonymousUserId --> Map of questionPageId --> Map of questionId --> List of Validation responses private final Cache<String, Map<String, Map<String, List<QuestionValidationResponse>>>> anonymousQuestionAttemptsCache; /** * @param ds * - data source * @param objectMapper * - for mapping between DO and DTO */ @Inject public PgQuestionAttempts(final PostgresSqlDb ds, final ContentMapper objectMapper) { this.database = ds; this.objectMapper = objectMapper.getSharedContentObjectMapper(); anonymousQuestionAttemptsCache = CacheBuilder.newBuilder() .expireAfterAccess(ANONYMOUS_SESSION_DURATION_IN_MINUTES, TimeUnit.MINUTES) .<String, Map<String, Map<String, List<QuestionValidationResponse>>>>build(); } @Override public void registerAnonymousQuestionAttempt(final String userId, final String questionPageId, final String fullQuestionId, final QuestionValidationResponse questionAttempt) throws SegueDatabaseException { Map<String, Map<String, List<QuestionValidationResponse>>> userAttempts = anonymousQuestionAttemptsCache .getIfPresent(userId); if (null == userAttempts) { userAttempts = new HashMap<String, Map<String, List<QuestionValidationResponse>>>(); anonymousQuestionAttemptsCache.put(userId, userAttempts); } if (userAttempts.get(questionPageId) == null) { userAttempts.put(questionPageId, new HashMap<String, List<QuestionValidationResponse>>()); } if (userAttempts.get(questionPageId).get(fullQuestionId) == null) { userAttempts.get(questionPageId).put(fullQuestionId, new ArrayList<QuestionValidationResponse>()); } userAttempts.get(questionPageId).get(fullQuestionId).add(questionAttempt); } /** * * @param anonymousId * to lookup * @return the question pageId --> full questionId --> list of responses. (or null if no data) */ @Override public Map<String, Map<String, List<QuestionValidationResponse>>> getAnonymousQuestionAttempts( final String anonymousId) { if (this.anonymousQuestionAttemptsCache.getIfPresent(anonymousId) != null) { return this.anonymousQuestionAttemptsCache.getIfPresent(anonymousId); } else { return Maps.newHashMap(); } } @Override public void registerQuestionAttempt(final Long userId, final String questionPageId, final String fullQuestionId, final QuestionValidationResponse questionAttempt) throws SegueDatabaseException { PreparedStatement pst; try (Connection conn = database.getDatabaseConnection()) { pst = conn.prepareStatement("INSERT INTO question_attempts(user_id, " + "question_id, question_attempt, correct, \"timestamp\")" + " VALUES (?, ?, ?::text::jsonb, ?, ?);", Statement.RETURN_GENERATED_KEYS); pst.setLong(1, userId); pst.setString(2, fullQuestionId); pst.setString(3, objectMapper.writeValueAsString(questionAttempt)); if (questionAttempt.isCorrect() != null) { pst.setBoolean(4, questionAttempt.isCorrect()); } else { pst.setNull(4, java.sql.Types.NULL); } pst.setTimestamp(5, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save question attempt."); } } catch (SQLException e) { throw new SegueDatabaseException("Postgres exception", e); } catch (JsonProcessingException e) { throw new SegueDatabaseException("Unable to process json exception", e); } } @Override public Map<String, Map<String, List<QuestionValidationResponse>>> getQuestionAttempts(final Long userId) throws SegueDatabaseException { PreparedStatement pst; try (Connection conn = database.getDatabaseConnection()) { pst = conn.prepareStatement( "Select * FROM question_attempts WHERE user_id = ? ORDER BY \"timestamp\" ASC"); pst.setLong(1, userId); ResultSet results = pst.executeQuery(); Map<String, Map<String, List<QuestionValidationResponse>>> mapOfQuestionAttemptsByPage = Maps .newHashMap(); while (results.next()) { QuestionValidationResponse questionAttempt = objectMapper .readValue(results.getString("question_attempt"), QuestionValidationResponse.class); String questionPageId = questionAttempt.getQuestionId().split("\\|")[0]; String questionId = questionAttempt.getQuestionId(); Map<String, List<QuestionValidationResponse>> attemptsForThisQuestionPage = mapOfQuestionAttemptsByPage .get(questionPageId); if (null == attemptsForThisQuestionPage) { attemptsForThisQuestionPage = Maps.newHashMap(); mapOfQuestionAttemptsByPage.put(questionPageId, attemptsForThisQuestionPage); } List<QuestionValidationResponse> listOfResponses = attemptsForThisQuestionPage.get(questionId); if (null == listOfResponses) { listOfResponses = Lists.newArrayList(); attemptsForThisQuestionPage.put(questionId, listOfResponses); } listOfResponses.add(questionAttempt); } return mapOfQuestionAttemptsByPage; } catch (SQLException e) { throw new SegueDatabaseException("Postgres exception", e); } catch (IOException e) { throw new SegueDatabaseException("Exception while parsing json", e); } } @Override public Map<Long, Map<String, Map<String, List<QuestionValidationResponse>>>> getQuestionAttemptsByUsersAndQuestionPrefix( final List<Long> userIds, final List<String> questionPageIds) throws SegueDatabaseException { Validate.notEmpty(questionPageIds); if (userIds.isEmpty()) { return Maps.newHashMap(); } PreparedStatement pst; try (Connection conn = database.getDatabaseConnection()) { StringBuilder query = new StringBuilder(); query.append("Select * FROM question_attempts WHERE"); // add all of the user ids we are interested in. if (userIds != null && !userIds.isEmpty()) { StringBuilder inParams = new StringBuilder(); inParams.append("?"); for (int i = 1; i < userIds.size(); i++) { inParams.append(",?"); } query.append(String.format(" user_id IN (%s)", inParams.toString())); } // add all of the question page ids we are interested in StringBuilder questionIdsSB = new StringBuilder(); if (questionPageIds != null && !questionPageIds.isEmpty()) { questionIdsSB.append("^("); questionIdsSB.append(questionPageIds.get(0)); for (int i = 1; i < questionPageIds.size(); i++) { questionIdsSB.append("|" + questionPageIds.get(i)); } questionIdsSB.append(")"); query.append(" AND question_id ~ ?"); } query.append(" ORDER BY \"timestamp\" ASC"); pst = conn.prepareStatement(query.toString()); Map<Long, Map<String, Map<String, List<QuestionValidationResponse>>>> mapToReturn = Maps.newHashMap(); int index = 1; for (int i = 0; i < userIds.size(); i++) { pst.setLong(index++, userIds.get(i)); mapToReturn.put(userIds.get(i), new HashMap<String, Map<String, List<QuestionValidationResponse>>>()); } pst.setString(index++, questionIdsSB.toString()); ResultSet results = pst.executeQuery(); while (results.next()) { QuestionValidationResponse questionAttempt = objectMapper .readValue(results.getString("question_attempt"), QuestionValidationResponse.class); String questionPageId = questionAttempt.getQuestionId().split("\\|")[0]; String questionId = questionAttempt.getQuestionId(); Map<String, Map<String, List<QuestionValidationResponse>>> mapOfQuestionAttemptsByPage = mapToReturn .get(results.getLong("user_id")); Map<String, List<QuestionValidationResponse>> attemptsForThisQuestionPage = mapOfQuestionAttemptsByPage .get(questionPageId); if (null == attemptsForThisQuestionPage) { attemptsForThisQuestionPage = Maps.newHashMap(); mapOfQuestionAttemptsByPage.put(questionPageId, attemptsForThisQuestionPage); } List<QuestionValidationResponse> listOfResponses = attemptsForThisQuestionPage.get(questionId); if (null == listOfResponses) { listOfResponses = Lists.newArrayList(); attemptsForThisQuestionPage.put(questionId, listOfResponses); } listOfResponses.add(questionAttempt); } return mapToReturn; } catch (SQLException e) { throw new SegueDatabaseException("Postgres exception", e); } catch (IOException e) { throw new SegueDatabaseException("Exception while parsing json", e); } } /** * Merges any question data stored in the session (this will only happen for anonymous users). * * @param anonymousUserId * - containing the question attempts. * @param registeredUserId * - the account to merge with. * @throws NoUserLoggedInException * - Unable to merge as the user is still anonymous. * @throws SegueDatabaseException * - if we are unable to locate the questions attempted by this user already. */ @Override public void mergeAnonymousQuestionInformationWithRegisteredUserRecord(final String anonymousUserId, final Long registeredUserId) throws SegueDatabaseException { Validate.notNull(anonymousUserId, "Anonymous user must not be null when merging anonymousQuestion info"); Validate.notNull(registeredUserId, "Registered user must not be null when merging anonymousQuestion info"); Map<String, Map<String, List<QuestionValidationResponse>>> anonymouslyAnsweredQuestions = this .getAnonymousQuestionAttempts(anonymousUserId); if (anonymouslyAnsweredQuestions.isEmpty()) { return; } int count = 0; for (String questionPageId : anonymouslyAnsweredQuestions.keySet()) { for (String questionId : anonymouslyAnsweredQuestions.get(questionPageId).keySet()) { for (QuestionValidationResponse questionResponse : anonymouslyAnsweredQuestions.get(questionPageId) .get(questionId)) { this.registerQuestionAttempt(registeredUserId, questionPageId, questionId, questionResponse); count++; } } } log.info(String.format("Merged anonymously answered questions (%s) with known user account (%s)", count, registeredUserId)); // delete the session attribute as merge has completed. this.anonymousQuestionAttemptsCache.invalidate(anonymousUserId); } }