From e77c9b418fdfb935ff8e99f10f607a4bbd7e1c8c Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sun, 20 Oct 2013 23:14:51 -0700 Subject: First stage of a major refactoring. Question and Answer can now be serialized and deserialized to/from JSON. As such, I no longer have to pass awkward maps around. As part of this change I have introduced a Provider interface to abstract out loading and persisting these beans. The scoring logic has been completed factored out of SurveyResultsResource and into the various ScoringEngines. Tests have been added for Question, Answer, and the ScoringEngines. A bug has been fixed in computing the value for slider questions. The label identifiers in the circle questions have changed from all lower case to camel case. That is, topleft is now topLeft. Several issues have been corrected in the circle answers where the point values did not match the labels. Testing and code coverage support and reports have been added. --- src/com/p4square/grow/backend/GrowBackend.java | 22 ++++ src/com/p4square/grow/backend/db/CassandraKey.java | 28 +++++ .../grow/backend/db/CassandraProviderImpl.java | 42 +++++++ src/com/p4square/grow/backend/resources/Point.java | 52 -------- src/com/p4square/grow/backend/resources/Score.java | 49 -------- .../backend/resources/SurveyResultsResource.java | 139 +++++---------------- .../backend/resources/TrainingRecordResource.java | 2 + 7 files changed, 125 insertions(+), 209 deletions(-) create mode 100644 src/com/p4square/grow/backend/db/CassandraKey.java create mode 100644 src/com/p4square/grow/backend/db/CassandraProviderImpl.java delete mode 100644 src/com/p4square/grow/backend/resources/Point.java delete mode 100644 src/com/p4square/grow/backend/resources/Score.java (limited to 'src/com/p4square/grow/backend') diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java index 533cf09..45e0fa2 100644 --- a/src/com/p4square/grow/backend/GrowBackend.java +++ b/src/com/p4square/grow/backend/GrowBackend.java @@ -15,6 +15,13 @@ import org.restlet.routing.Router; import com.p4square.grow.config.Config; import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.backend.db.CassandraKey; +import com.p4square.grow.backend.db.CassandraProviderImpl; + +import com.p4square.grow.model.Question; + +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.QuestionProvider; import com.p4square.grow.backend.resources.AccountResource; import com.p4square.grow.backend.resources.BannerResource; @@ -29,11 +36,15 @@ import com.p4square.grow.backend.resources.TrainingResource; * @author Jesse Morgan */ public class GrowBackend extends Application { + private static final String DEFAULT_COLUMN = "value"; + private final static Logger LOG = Logger.getLogger(GrowBackend.class); private final Config mConfig; private final CassandraDatabase mDatabase; + private final Provider mQuestionProvider; + public GrowBackend() { this(new Config()); } @@ -41,6 +52,13 @@ public class GrowBackend extends Application { public GrowBackend(Config config) { mConfig = config; mDatabase = new CassandraDatabase(); + + mQuestionProvider = new QuestionProvider(new CassandraProviderImpl(mDatabase, "strings", Question.class)) { + @Override + public CassandraKey makeKey(String questionId) { + return new CassandraKey("/questions/" + questionId, DEFAULT_COLUMN); + } + }; } @Override @@ -102,6 +120,10 @@ public class GrowBackend extends Application { return mDatabase; } + public Provider getQuestionProvider() { + return mQuestionProvider; + } + /** * Stand-alone main for testing. */ diff --git a/src/com/p4square/grow/backend/db/CassandraKey.java b/src/com/p4square/grow/backend/db/CassandraKey.java new file mode 100644 index 0000000..8e23087 --- /dev/null +++ b/src/com/p4square/grow/backend/db/CassandraKey.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +/** + * CassandraKey represents a Cassandra key / column pair. + * + * @author Jesse Morgan + */ +public class CassandraKey { + private final String mId; + private final String mColumn; + + public CassandraKey(String id, String column) { + mId = id; + mColumn = column; + } + + public String getId() { + return mId; + } + + public String getColumn() { + return mColumn; + } +} diff --git a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java new file mode 100644 index 0000000..fb6e34e --- /dev/null +++ b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; + +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Provider implementation backed by a Cassandra ColumnFamily. + * + * @author Jesse Morgan + */ +public class CassandraProviderImpl extends JsonEncodedProvider { + private final CassandraDatabase mDb; + private final String mColumnFamily; + + public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class clazz) { + super(clazz); + + mDb = db; + mColumnFamily = columnFamily; + } + + @Override + public V get(CassandraKey key) throws IOException { + String blob = mDb.getKey(mColumnFamily, key.getId(), key.getColumn()); + return decode(blob); + } + + @Override + public void put(CassandraKey key, V obj) throws IOException { + String blob = encode(obj); + mDb.putKey(mColumnFamily, key.getId(), key.getColumn(), blob); + } +} diff --git a/src/com/p4square/grow/backend/resources/Point.java b/src/com/p4square/grow/backend/resources/Point.java deleted file mode 100644 index e1b15a8..0000000 --- a/src/com/p4square/grow/backend/resources/Point.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -/** - * Simple double based point class. - * - * @author Jesse Morgan - */ -class Point { - public static Point valueOf(String str) { - final int comma = str.indexOf(','); - if (comma == -1) { - throw new IllegalArgumentException("Malformed point string"); - } - - final String sX = str.substring(0, comma); - final String sY = str.substring(comma + 1); - - return new Point(Double.valueOf(sX), Double.valueOf(sY)); - } - - private final double mX; - private final double mY; - - public Point(double x, double y) { - mX = x; - mY = y; - } - - public double distance(Point other) { - final double dx = mX - other.mX; - final double dy = mY - other.mY; - - return Math.sqrt(dx*dx + dy*dy); - } - - public double getX() { - return mX; - } - - public double getY() { - return mY; - } - - @Override - public String toString() { - return String.format("%.2f,%.2f", mX, mY); - } -} diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/backend/resources/Score.java deleted file mode 100644 index 6f52c02..0000000 --- a/src/com/p4square/grow/backend/resources/Score.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -/** - * Simple structure containing a score's sum and count. - * - * @author Jesse Morgan - */ -class Score { - /** - * Return the integer value for the given Score String. - */ - public static int numericScore(String score) { - if ("teacher".equals(score)) { - return 4; - } else if ("disciple".equals(score)) { - return 3; - } else if ("believer".equals(score)) { - return 2; - } else { - return 1; - } - } - - double sum; - int count; - - @Override - public String toString() { - final double score = sum / count; - - if (score >= 4) { - return "teacher"; - - } else if (score >= 3) { - return "disciple"; - - } else if (score >= 2) { - return "believer"; - - } else { - return "seeker"; - } - } - -} diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java index f0bb2aa..91d4d0f 100644 --- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java +++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java @@ -14,16 +14,20 @@ import org.codehaus.jackson.map.ObjectMapper; import org.restlet.data.MediaType; import org.restlet.data.Status; -import org.restlet.resource.ServerResource; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; +import org.restlet.resource.ServerResource; import org.apache.log4j.Logger; -import com.p4square.grow.model.Answer; -import com.p4square.grow.model.Question; import com.p4square.grow.backend.GrowBackend; import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.Answer; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.RecordedAnswer; +import com.p4square.grow.model.Score; +import com.p4square.grow.provider.Provider; + /** * Store the user's answers to the assessment and generate their score. @@ -31,15 +35,16 @@ import com.p4square.grow.backend.db.CassandraDatabase; * @author Jesse Morgan */ public class SurveyResultsResource extends ServerResource { - private final static Logger cLog = Logger.getLogger(SurveyResultsResource.class); + private static final Logger LOG = Logger.getLogger(SurveyResultsResource.class); - private final static ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = new ObjectMapper(); static enum RequestType { ASSESSMENT, ANSWER } private CassandraDatabase mDb; + private Provider mQuestionProvider; private RequestType mRequestType; private String mUserId; @@ -51,6 +56,7 @@ public class SurveyResultsResource extends ServerResource { final GrowBackend backend = (GrowBackend) getApplication(); mDb = backend.getDatabase(); + mQuestionProvider = backend.getQuestionProvider(); mUserId = getAttribute("userId"); mQuestionId = getAttribute("questionId"); @@ -105,7 +111,7 @@ public class SurveyResultsResource extends ServerResource { success = true; } catch (Exception e) { - cLog.warn("Caught exception putting answer: " + e.getMessage(), e); + LOG.warn("Caught exception putting answer: " + e.getMessage(), e); } break; @@ -146,18 +152,30 @@ public class SurveyResultsResource extends ServerResource { continue; } - final String questionId = c.getName(); - final String answerId = c.getStringValue(); - if (!scoringDone) { - scoringDone = !scoreQuestion(score, questionId, answerId); + try { + Question question = mQuestionProvider.get(c.getName()); + RecordedAnswer userAnswer = MAPPER.readValue(c.getStringValue(), RecordedAnswer.class); + + if (question == null) { + LOG.warn("Answer for unknown question: " + c.getName()); + continue; + } + + LOG.error("Scoring questionId: " + c.getName()); + scoringDone = !question.scoreAnswer(score, userAnswer); + + } catch (Exception e) { + LOG.error("Failed to score question: {userid: \"" + mUserId + + "\", questionid:\"" + c.getName() + + "\", userAnswer:\"" + c.getStringValue() + "\"}", e); } totalAnswers++; } - sb.append(", \"score\":" + score.sum / score.count); - sb.append(", \"sum\":" + score.sum); - sb.append(", \"count\":" + score.count); + sb.append(", \"score\":" + score.getScore()); + sb.append(", \"sum\":" + score.getSum()); + sb.append(", \"count\":" + score.getCount()); sb.append(", \"totalAnswers\":" + totalAnswers); sb.append(", \"result\":\"" + score.toString() + "\""); } @@ -170,99 +188,4 @@ public class SurveyResultsResource extends ServerResource { return summary; } - - private boolean scoreQuestion(final Score score, final String questionId, - final String answerJson) { - - final String data = mDb.getKey("strings", "/questions/" + questionId); - - try { - final Map questionMap = MAPPER.readValue(data, Map.class); - final Map answerMap = MAPPER.readValue(answerJson, Map.class); - final Question question = new Question((Map) questionMap); - final String answerId = (String) answerMap.get("answerId"); - - switch (question.getType()) { - case TEXT: - case IMAGE: - final Answer answer = question.getAnswers().get(answerId); - if (answer == null) { - cLog.warn("Got unknown answer " + answerId - + " for question " + questionId); - } else { - if (!scoreAnswer(score, answer)) { - return false; // Quit scoring - } - } - break; - - case SLIDER: - score.sum += Double.valueOf(answerId) * 4 + 1; - score.count++; - break; - - case CIRCLE: - case QUAD: - scoreQuad(score, question, answerId); - break; - } - - } catch (Exception e) { - cLog.error("Exception parsing question id " + questionId, e); - } - - return true; - } - - private boolean scoreAnswer(final Score score, final Answer answer) { - switch (answer.getType()) { - case TRUMP: - score.sum = answer.getScoreFactor(); - score.count = 1; - return false; // Quit scoring. - - case AVERAGE: - score.sum += answer.getScoreFactor(); - score.count++; - break; - - case NONE: - break; - } - - return true; // Continue scoring - } - - private boolean scoreQuad(final Score score, final Question question, - final String answerId) { - - Point[] answers = new Point[question.getAnswers().size()]; - { - int i = 0; - for (String answer : question.getAnswers().keySet()) { - answers[i++] = Point.valueOf(answer); - } - } - - Point userAnswer = Point.valueOf(answerId); - - double minDistance = Double.MAX_VALUE; - int answerIndex = 0; - for (int i = 0; i < answers.length; i++) { - final double distance = userAnswer.distance(answers[i]); - if (distance < minDistance) { - minDistance = distance; - answerIndex = i; - } - } - - cLog.debug("Quad " + question.getId() + ": Got answer " - + answers[answerIndex].toString() + " for user point " + answerId); - - final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); - score.sum += answer.getScoreFactor(); - score.count++; - - return true; // Continue scoring - } } diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java index 009d0fe..6de9507 100644 --- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java +++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java @@ -27,6 +27,8 @@ import org.apache.log4j.Logger; import com.p4square.grow.backend.GrowBackend; import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.Score; + /** * * @author Jesse Morgan -- cgit v1.2.3