summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2013-10-20 23:14:51 -0700
committerJesse Morgan <jesse@jesterpm.net>2013-10-20 23:14:51 -0700
commite77c9b418fdfb935ff8e99f10f607a4bbd7e1c8c (patch)
tree9005f8c0e3b18f853fb8093b01f6aad64949bc13 /src
parent5037f4797461649994068d97a8433b6cd793c523 (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java22
-rw-r--r--src/com/p4square/grow/backend/db/CassandraKey.java28
-rw-r--r--src/com/p4square/grow/backend/db/CassandraProviderImpl.java42
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResultsResource.java139
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingRecordResource.java2
-rw-r--r--src/com/p4square/grow/frontend/GrowFrontend.java8
-rw-r--r--src/com/p4square/grow/frontend/JsonRequestProvider.java64
-rw-r--r--src/com/p4square/grow/frontend/SurveyPageResource.java25
-rw-r--r--src/com/p4square/grow/model/Answer.java135
-rw-r--r--src/com/p4square/grow/model/CircleQuestion.java89
-rw-r--r--src/com/p4square/grow/model/ImageQuestion.java24
-rw-r--r--src/com/p4square/grow/model/Point.java (renamed from src/com/p4square/grow/backend/resources/Point.java)35
-rw-r--r--src/com/p4square/grow/model/QuadQuestion.java89
-rw-r--r--src/com/p4square/grow/model/QuadScoringEngine.java49
-rw-r--r--src/com/p4square/grow/model/Question.java134
-rw-r--r--src/com/p4square/grow/model/RecordedAnswer.java34
-rw-r--r--src/com/p4square/grow/model/Score.java (renamed from src/com/p4square/grow/backend/resources/Score.java)29
-rw-r--r--src/com/p4square/grow/model/ScoringEngine.java26
-rw-r--r--src/com/p4square/grow/model/SimpleScoringEngine.java26
-rw-r--r--src/com/p4square/grow/model/SliderQuestion.java24
-rw-r--r--src/com/p4square/grow/model/SliderScoringEngine.java29
-rw-r--r--src/com/p4square/grow/model/TextQuestion.java24
-rw-r--r--src/com/p4square/grow/provider/JsonEncodedProvider.java60
-rw-r--r--src/com/p4square/grow/provider/Provider.java31
-rw-r--r--src/com/p4square/grow/provider/QuestionProvider.java41
-rw-r--r--src/templates/templates/question-circle.ftl8
26 files changed, 1019 insertions, 198 deletions
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 <jesse@jesterpm.net>
*/
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<String, Question> 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<CassandraKey>(new CassandraProviderImpl<Question>(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<String, Question> 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 <jesse@jesterpm.net>
+ */
+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 <jesse@jesterpm.net>
+ */
+public class CassandraProviderImpl<V> extends JsonEncodedProvider<CassandraKey, V> {
+ private final CassandraDatabase mDb;
+ private final String mColumnFamily;
+
+ public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class<V> 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/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 <jesse@jesterpm.net>
*/
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<String, Question> 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<String, Object>) 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 <jesse@jesterpm.net>
diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java
index 327554e..6a74bda 100644
--- a/src/com/p4square/grow/frontend/GrowFrontend.java
+++ b/src/com/p4square/grow/frontend/GrowFrontend.java
@@ -156,10 +156,10 @@ public class GrowFrontend extends FMFacade {
// Static content
try {
- component.getDefaultHost().attach("/images/", new FileServingApp("./build/images/"));
- component.getDefaultHost().attach("/scripts", new FileServingApp("./build/scripts"));
- component.getDefaultHost().attach("/style.css", new FileServingApp("./build/style.css"));
- component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/favicon.ico"));
+ component.getDefaultHost().attach("/images/", new FileServingApp("./build/root/images/"));
+ component.getDefaultHost().attach("/scripts", new FileServingApp("./build/root/scripts"));
+ component.getDefaultHost().attach("/style.css", new FileServingApp("./build/root/style.css"));
+ component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/root/favicon.ico"));
} catch (IOException e) {
LOG.error("Could not create directory for static resources: "
+ e.getMessage(), e);
diff --git a/src/com/p4square/grow/frontend/JsonRequestProvider.java b/src/com/p4square/grow/frontend/JsonRequestProvider.java
new file mode 100644
index 0000000..c372251
--- /dev/null
+++ b/src/com/p4square/grow/frontend/JsonRequestProvider.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+import com.p4square.grow.provider.JsonEncodedProvider;
+
+/**
+ * Fetch a JSON object via a Request.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class JsonRequestProvider<V> extends JsonEncodedProvider<String, V> {
+
+ private final Restlet mDispatcher;
+
+ public JsonRequestProvider(Restlet dispatcher, Class<V> clazz) {
+ super(clazz);
+
+ mDispatcher = dispatcher;
+ }
+
+ @Override
+ public V get(String url) throws IOException {
+ Request request = new Request(Method.GET, url);
+ Response response = mDispatcher.handle(request);
+ Representation representation = response.getEntity();
+
+ if (!response.getStatus().isSuccess()) {
+ if (representation != null) {
+ representation.release();
+ }
+
+ throw new IOException("Could not get object. " + response.getStatus());
+ }
+
+ return decode(representation.getText());
+ }
+
+ @Override
+ public void put(String url, V obj) throws IOException {
+ final Request request = new Request(Method.PUT, url);
+ request.setEntity(new StringRepresentation(encode(obj)));
+
+ final Response response = mDispatcher.handle(request);
+
+ if (!response.getStatus().isSuccess()) {
+ throw new IOException("Could not put object. " + response.getStatus());
+ }
+
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java
index 415b46c..f864014 100644
--- a/src/com/p4square/grow/frontend/SurveyPageResource.java
+++ b/src/com/p4square/grow/frontend/SurveyPageResource.java
@@ -4,6 +4,8 @@
package com.p4square.grow.frontend;
+import java.io.IOException;
+
import java.util.Map;
import java.util.HashMap;
@@ -27,6 +29,8 @@ import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
import com.p4square.grow.model.Question;
+import com.p4square.grow.provider.QuestionProvider;
+import com.p4square.grow.provider.Provider;
/**
* SurveyPageResource handles rendering the survey and processing user's answers.
@@ -44,6 +48,7 @@ public class SurveyPageResource extends FreeMarkerPageResource {
private Config mConfig;
private Template mSurveyTemplate;
private JsonRequestClient mJsonClient;
+ private Provider<String, Question> mQuestionProvider;
// Fields pertaining to this request.
private String mQuestionId;
@@ -62,6 +67,12 @@ public class SurveyPageResource extends FreeMarkerPageResource {
}
mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+ mQuestionProvider = new QuestionProvider<String>(new JsonRequestProvider<Question>(getContext().getClientDispatcher(), Question.class)) {
+ @Override
+ public String makeKey(String questionId) {
+ return getBackendEndpoint() + "/assessment/question/" + questionId;
+ }
+ };
mQuestionId = getAttribute("questionId");
mUserId = getRequest().getClientInfo().getUser().getIdentifier();
@@ -102,7 +113,7 @@ public class SurveyPageResource extends FreeMarkerPageResource {
String selectedAnswer = getAnswer(mQuestionId);
Map root = getRootObject();
- root.put("question", question.getMap());
+ root.put("question", question);
root.put("selectedAnswerId", selectedAnswer);
// Get the question count and compute progress
@@ -214,17 +225,9 @@ public class SurveyPageResource extends FreeMarkerPageResource {
private Question getQuestion(String id) {
try {
- Map<?, ?> questionData = null;
-
- JsonResponse response = backendGet("/assessment/question/" + id);
- if (!response.getStatus().isSuccess()) {
- return null;
- }
- questionData = response.getMap();
+ return mQuestionProvider.get(id);
- return new Question((Map<String, Object>) questionData);
-
- } catch (ClientException e) {
+ } catch (IOException e) {
LOG.warn("Error fetching question.", e);
return null;
}
diff --git a/src/com/p4square/grow/model/Answer.java b/src/com/p4square/grow/model/Answer.java
index 4c84060..57a1e5d 100644
--- a/src/com/p4square/grow/model/Answer.java
+++ b/src/com/p4square/grow/model/Answer.java
@@ -4,7 +4,7 @@
package com.p4square.grow.model;
-import java.util.Map;
+import org.apache.log4j.Logger;
/**
* This is the model of an assessment question's answer.
@@ -12,52 +12,131 @@ import java.util.Map;
* @author Jesse Morgan <jesse@jesterpm.net>
*/
public class Answer {
+ private static final Logger LOG = Logger.getLogger(Answer.class);
+
+ /**
+ * ScoreType determines how the answer will be scored.
+ *
+ */
public static enum ScoreType {
- NONE, AVERAGE, TRUMP;
- }
-
- private final String mAnswerId;
- private final String mAnswerText;
- private final ScoreType mType;
- private final float mScoreFactor;
- private final String mNextQuestionId;
-
- public Answer(final String id, final Map<String, Object> answer) {
- mAnswerId = id;
- mAnswerText = (String) answer.get("text");
- final String typeStr = (String) answer.get("type");
- if (typeStr == null) {
- mType = ScoreType.AVERAGE;
- } else {
- mType = ScoreType.valueOf(typeStr.toUpperCase());
- }
+ /**
+ * This question has no effect on the score.
+ */
+ NONE,
- if (mType != ScoreType.NONE) {
- mScoreFactor = Float.valueOf((String) answer.get("score"));
- } else {
- mScoreFactor = 0;
- }
+ /**
+ * The score of this question is part of the average.
+ */
+ AVERAGE,
- mNextQuestionId = (String) answer.get("nextQuestion");
+ /**
+ * The score of this question is the total score, no other questions
+ * matter after this point.
+ */
+ TRUMP;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
}
- public String getId() {
- return mAnswerId;
+ private String mAnswerText;
+ private ScoreType mType;
+ private float mScoreFactor;
+ private String mNextQuestionId;
+
+ public Answer() {
+ mType = ScoreType.AVERAGE;
}
+ /**
+ * @return The text associated with the answer.
+ */
public String getText() {
return mAnswerText;
}
+ /**
+ * Set the text associated with the answer.
+ * @param text The new text.
+ */
+ public void setText(String text) {
+ mAnswerText = text;
+ }
+
+ /**
+ * @return the ScoreType for the Answer.
+ */
public ScoreType getType() {
return mType;
}
- public float getScoreFactor() {
+ /**
+ * Set the ScoreType for the answer.
+ * @param type The new ScoreType.
+ */
+ public void setType(ScoreType type) {
+ mType = type;
+ }
+
+ /**
+ * @return the delta of the score if this answer is selected.
+ */
+ public float getScore() {
+ if (mType == ScoreType.NONE) {
+ return 0;
+ }
+
return mScoreFactor;
}
+ /**
+ * Set the score delta for this answer.
+ * @param score The new delta.
+ */
+ public void setScore(float score) {
+ mScoreFactor = score;
+ }
+
+ /**
+ * @return the id of the next question if this answer is selected, or null
+ * if selecting this answer has no effect.
+ */
public String getNextQuestion() {
return mNextQuestionId;
}
+
+ /**
+ * Set the id of the next question when this answer is selected.
+ * @param id The next question id or null to proceed as usual.
+ */
+ public void setNextQuestion(String id) {
+ mNextQuestionId = id;
+ }
+
+ /**
+ * Adjust the running score for the selection of this answer.
+ * @param score The running score to adjust.
+ * @return true if scoring should continue, false if this answer trumps all.
+ */
+ public boolean score(final Score score) {
+ switch (getType()) {
+ case TRUMP:
+ score.sum = getScore();
+ score.count = 1;
+ return false; // Quit scoring.
+
+ case AVERAGE:
+ LOG.error("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }");
+ score.sum += getScore();
+ score.count++;
+ break;
+
+ case NONE:
+ break;
+ }
+
+ return true; // Continue scoring
+ }
}
diff --git a/src/com/p4square/grow/model/CircleQuestion.java b/src/com/p4square/grow/model/CircleQuestion.java
new file mode 100644
index 0000000..71acc14
--- /dev/null
+++ b/src/com/p4square/grow/model/CircleQuestion.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Circle Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CircleQuestion extends Question {
+ private static final ScoringEngine ENGINE = new QuadScoringEngine();
+
+ private String mTopLeft;
+ private String mTopRight;
+ private String mBottomLeft;
+ private String mBottomRight;
+
+ /**
+ * @return the Top Left label.
+ */
+ public String getTopLeft() {
+ return mTopLeft;
+ }
+
+ /**
+ * Set the Top Left label.
+ * @param s The new top left label.
+ */
+ public void setTopLeft(String s) {
+ mTopLeft = s;
+ }
+
+ /**
+ * @return the Top Right label.
+ */
+ public String getTopRight() {
+ return mTopRight;
+ }
+
+ /**
+ * Set the Top Right label.
+ * @param s The new top left label.
+ */
+ public void setTopRight(String s) {
+ mTopRight = s;
+ }
+
+ /**
+ * @return the Bottom Left label.
+ */
+ public String getBottomLeft() {
+ return mBottomLeft;
+ }
+
+ /**
+ * Set the Bottom Left label.
+ * @param s The new top left label.
+ */
+ public void setBottomLeft(String s) {
+ mBottomLeft = s;
+ }
+
+ /**
+ * @return the Bottom Right label.
+ */
+ public String getBottomRight() {
+ return mBottomRight;
+ }
+
+ /**
+ * Set the Bottom Right label.
+ * @param s The new top left label.
+ */
+ public void setBottomRight(String s) {
+ mBottomRight = s;
+ }
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.CIRCLE;
+ }
+}
diff --git a/src/com/p4square/grow/model/ImageQuestion.java b/src/com/p4square/grow/model/ImageQuestion.java
new file mode 100644
index 0000000..d94c32c
--- /dev/null
+++ b/src/com/p4square/grow/model/ImageQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Image Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ImageQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SimpleScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.IMAGE;
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Point.java b/src/com/p4square/grow/model/Point.java
index e1b15a8..e9fc0ca 100644
--- a/src/com/p4square/grow/backend/resources/Point.java
+++ b/src/com/p4square/grow/model/Point.java
@@ -2,17 +2,23 @@
* Copyright 2013 Jesse Morgan
*/
-package com.p4square.grow.backend.resources;
+package com.p4square.grow.model;
/**
* Simple double based point class.
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-class Point {
+public class Point {
+ /**
+ * Parse a comma separated x,y pair into a point.
+ *
+ * @return The point represented by the string.
+ * @throws IllegalArgumentException if the input is malformed.
+ */
public static Point valueOf(String str) {
final int comma = str.indexOf(',');
- if (comma == -1) {
+ if (comma == -1 || comma == 0 || comma == str.length() - 1) {
throw new IllegalArgumentException("Malformed point string");
}
@@ -20,16 +26,28 @@ class Point {
final String sY = str.substring(comma + 1);
return new Point(Double.valueOf(sX), Double.valueOf(sY));
- }
+ }
private final double mX;
private final double mY;
+ /**
+ * Create a new point with the given coordinates.
+ *
+ * @param x The x coordinate.
+ * @param y The y coordinate.
+ */
public Point(double x, double y) {
mX = x;
mY = y;
}
+ /**
+ * Compute the distance between this point and another.
+ *
+ * @param other The other point.
+ * @return The distance between this point and other.
+ */
public double distance(Point other) {
final double dx = mX - other.mX;
final double dy = mY - other.mY;
@@ -37,14 +55,23 @@ class Point {
return Math.sqrt(dx*dx + dy*dy);
}
+ /**
+ * @return The x coordinate.
+ */
public double getX() {
return mX;
}
+ /**
+ * @return The y coordinate.
+ */
public double getY() {
return mY;
}
+ /**
+ * @return The point represented as a comma separated pair.
+ */
@Override
public String toString() {
return String.format("%.2f,%.2f", mX, mY);
diff --git a/src/com/p4square/grow/model/QuadQuestion.java b/src/com/p4square/grow/model/QuadQuestion.java
new file mode 100644
index 0000000..a7b4179
--- /dev/null
+++ b/src/com/p4square/grow/model/QuadQuestion.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Two-dimensional Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class QuadQuestion extends Question {
+ private static final ScoringEngine ENGINE = new QuadScoringEngine();
+
+ private String mTop;
+ private String mRight;
+ private String mBottom;
+ private String mLeft;
+
+ /**
+ * @return the top label.
+ */
+ public String getTop() {
+ return mTop;
+ }
+
+ /**
+ * Set the top label.
+ * @param s The new top label.
+ */
+ public void setTop(String s) {
+ mTop = s;
+ }
+
+ /**
+ * @return the right label.
+ */
+ public String getRight() {
+ return mRight;
+ }
+
+ /**
+ * Set the right label.
+ * @param s The new right label.
+ */
+ public void setRight(String s) {
+ mRight = s;
+ }
+
+ /**
+ * @return the bottom label.
+ */
+ public String getBottom() {
+ return mBottom;
+ }
+
+ /**
+ * Set the bottom label.
+ * @param s The new bottom label.
+ */
+ public void setBottom(String s) {
+ mBottom = s;
+ }
+
+ /**
+ * @return the left label.
+ */
+ public String getLeft() {
+ return mLeft;
+ }
+
+ /**
+ * Set the left label.
+ * @param s The new left label.
+ */
+ public void setLeft(String s) {
+ mLeft = s;
+ }
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.QUAD;
+ }
+}
diff --git a/src/com/p4square/grow/model/QuadScoringEngine.java b/src/com/p4square/grow/model/QuadScoringEngine.java
new file mode 100644
index 0000000..33403b5
--- /dev/null
+++ b/src/com/p4square/grow/model/QuadScoringEngine.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import com.p4square.grow.model.Point;
+
+/**
+ * QuadScoringEngine expects the user's answer to be a Point string. We find
+ * the closest answer Point to the user's answer and treat that as the answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class QuadScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ // Find all of the answer points.
+ Point[] answers = new Point[question.getAnswers().size()];
+ {
+ int i = 0;
+ for (String answerStr : question.getAnswers().keySet()) {
+ answers[i++] = Point.valueOf(answerStr);
+ }
+ }
+
+ // Parse the user's answer.
+ Point userPoint = Point.valueOf(userAnswer.getAnswerId());
+
+ // Find the closest answer point to the user's answer.
+ double minDistance = Double.MAX_VALUE;
+ int answerIndex = 0;
+ for (int i = 0; i < answers.length; i++) {
+ final double distance = userPoint.distance(answers[i]);
+ if (distance < minDistance) {
+ minDistance = distance;
+ answerIndex = i;
+ }
+ }
+
+ LOG.debug("Quad " + question.getId() + ": Got answer "
+ + answers[answerIndex].toString() + " for user point " + userAnswer);
+
+ // Get the answer and update the score.
+ final Answer answer = question.getAnswers().get(answers[answerIndex].toString());
+ return answer.score(score);
+ }
+}
diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java
index 387d723..37deffa 100644
--- a/src/com/p4square/grow/model/Question.java
+++ b/src/com/p4square/grow/model/Question.java
@@ -4,76 +4,125 @@
package com.p4square.grow.model;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import org.codehaus.jackson.annotate.JsonSubTypes;
+import org.codehaus.jackson.annotate.JsonSubTypes.Type;
+import org.codehaus.jackson.annotate.JsonTypeInfo;
+
/**
* Model of an assessment question.
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-public class Question {
- public static enum QuestionType {
- TEXT, IMAGE, SLIDER, QUAD, CIRCLE;
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type")
+@JsonSubTypes({
+ @Type(value = TextQuestion.class, name = "text"),
+ @Type(value = ImageQuestion.class, name = "image"),
+ @Type(value = SliderQuestion.class, name = "slider"),
+ @Type(value = QuadQuestion.class, name = "quad"),
+ @Type(value = CircleQuestion.class, name = "circle"),
+})
+public abstract class Question {
+ /**
+ * QuestionType indicates the type of Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+ public enum QuestionType {
+ TEXT,
+ IMAGE,
+ SLIDER,
+ QUAD,
+ CIRCLE;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
}
- private final Map<String, Object> mMap;
- private final String mQuestionId;
- private final QuestionType mType;
- private final String mQuestionText;
+ private String mQuestionId;
+ private QuestionType mType;
+ private String mQuestionText;
private Map<String, Answer> mAnswers;
- private final String mPreviousQuestionId;
- private final String mNextQuestionId;
-
- public Question(final Map<String, Object> map) {
- mMap = map;
- mQuestionId = (String) map.get("id");
- mType = QuestionType.valueOf(((String) map.get("type")).toUpperCase());
-
- mQuestionText = (String) map.get("text");
-
- mPreviousQuestionId = (String) map.get("previousQuestion");
- mNextQuestionId = (String) map.get("nextQuestion");
+ private String mPreviousQuestionId;
+ private String mNextQuestionId;
+ public Question() {
mAnswers = new HashMap<String, Answer>();
- for (Map.Entry<String, Object> answer :
- ((Map<String, Object>) map.get("answers")).entrySet()) {
-
- final String id = answer.getKey();
- final Map<String, Object> answerMap = (Map<String, Object>) answer.getValue();
- final Answer answerObj = new Answer(id, answerMap);
- mAnswers.put(id, answerObj);
- }
}
+ /**
+ * @return the id String for this question.
+ */
public String getId() {
return mQuestionId;
}
- public QuestionType getType() {
- return mType;
+ /**
+ * Set the id String for this question.
+ * @param id New id
+ */
+ public void setId(String id) {
+ mQuestionId = id;
}
- public String getText() {
+ /**
+ * @return The Question text.
+ */
+ public String getQuestion() {
return mQuestionText;
}
+ /**
+ * Set the question text.
+ * @param value The new question text.
+ */
+ public void setQuestion(String value) {
+ mQuestionText = value;
+ }
+
+ /**
+ * @return The id String of the previous question or null if no previous question exists.
+ */
public String getPreviousQuestion() {
return mPreviousQuestionId;
}
+ /**
+ * Set the id string of the previous question.
+ * @param id Previous question id or null if there is no previous question.
+ */
+ public void setPreviousQuestion(String id) {
+ mPreviousQuestionId = id;
+ }
+
+ /**
+ * @return The id String of the next question or null if no next question exists.
+ */
public String getNextQuestion() {
return mNextQuestionId;
}
- public Map<String, Answer> getAnswers() {
- return Collections.unmodifiableMap(mAnswers);
+ /**
+ * Set the id string of the next question.
+ * @param id next question id or null if there is no next question.
+ */
+ public void setNextQuestion(String id) {
+ mNextQuestionId = id;
}
- public Map<String, Object> getMap() {
- return Collections.unmodifiableMap(mMap);
+ /**
+ * @return a map of Answer id Strings to Answer objects.
+ */
+ public Map<String, Answer> getAnswers() {
+ return mAnswers;
}
/**
@@ -98,4 +147,19 @@ public class Question {
return nextQuestion;
}
+
+ /**
+ * Update the score based on the answer to this question.
+ *
+ * @param score The running score to update.
+ * @param answer The answer give to this question.
+ * @return true if scoring should continue, false if this answer trumps everything else.
+ */
+ public abstract boolean scoreAnswer(Score score, RecordedAnswer answer);
+
+ /**
+ * @return the QuestionType of this question.
+ */
+ public abstract QuestionType getType();
+
}
diff --git a/src/com/p4square/grow/model/RecordedAnswer.java b/src/com/p4square/grow/model/RecordedAnswer.java
new file mode 100644
index 0000000..7d9905d
--- /dev/null
+++ b/src/com/p4square/grow/model/RecordedAnswer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Simple model for a user's assessment answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class RecordedAnswer {
+ private String mAnswerId;
+
+ /**
+ * @return The user's answer.
+ */
+ public String getAnswerId() {
+ return mAnswerId;
+ }
+
+ /**
+ * Set the answer id field.
+ * @param id The new id.
+ */
+ public void setAnswerId(String id) {
+ mAnswerId = id;
+ }
+
+ @Override
+ public String toString() {
+ return mAnswerId;
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/model/Score.java
index 6f52c02..b6deda2 100644
--- a/src/com/p4square/grow/backend/resources/Score.java
+++ b/src/com/p4square/grow/model/Score.java
@@ -2,14 +2,14 @@
* Copyright 2013 Jesse Morgan
*/
-package com.p4square.grow.backend.resources;
+package com.p4square.grow.model;
/**
* Simple structure containing a score's sum and count.
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-class Score {
+public class Score {
/**
* Return the integer value for the given Score String.
*/
@@ -28,9 +28,30 @@ class Score {
double sum;
int count;
+ /**
+ * @return The sum of all the points.
+ */
+ public double getSum() {
+ return sum;
+ }
+
+ /**
+ * @return The number of questions included in the score.
+ */
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * @return The final score.
+ */
+ public double getScore() {
+ return sum / count;
+ }
+
@Override
public String toString() {
- final double score = sum / count;
+ final double score = getScore();
if (score >= 4) {
return "teacher";
@@ -45,5 +66,5 @@ class Score {
return "seeker";
}
}
-
+
}
diff --git a/src/com/p4square/grow/model/ScoringEngine.java b/src/com/p4square/grow/model/ScoringEngine.java
new file mode 100644
index 0000000..8ff18b3
--- /dev/null
+++ b/src/com/p4square/grow/model/ScoringEngine.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.apache.log4j.Logger;
+
+/**
+ * ScoringEngine computes the score for a question and a given answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class ScoringEngine {
+ protected static final Logger LOG = Logger.getLogger(ScoringEngine.class);
+
+ /**
+ * Update the score based on the given question and answer.
+ *
+ * @param score The running score to update.
+ * @param question The question to compute the score for.
+ * @param answer The answer give to this question.
+ * @return true if scoring should continue, false if this answer trumps everything else.
+ */
+ public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer);
+}
diff --git a/src/com/p4square/grow/model/SimpleScoringEngine.java b/src/com/p4square/grow/model/SimpleScoringEngine.java
new file mode 100644
index 0000000..6ef2dbb
--- /dev/null
+++ b/src/com/p4square/grow/model/SimpleScoringEngine.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * SimpleScoringEngine expects the user's answer to a valid answer id and
+ * scores accordingly.
+ *
+ * If the answer id is not valid an Exception is thrown.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SimpleScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ final Answer answer = question.getAnswers().get(userAnswer.getAnswerId());
+ if (answer == null) {
+ throw new IllegalArgumentException("Not a valid answer.");
+ }
+
+ return answer.score(score);
+ }
+}
diff --git a/src/com/p4square/grow/model/SliderQuestion.java b/src/com/p4square/grow/model/SliderQuestion.java
new file mode 100644
index 0000000..f0861e3
--- /dev/null
+++ b/src/com/p4square/grow/model/SliderQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Slider Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SliderQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SliderScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.SLIDER;
+ }
+}
diff --git a/src/com/p4square/grow/model/SliderScoringEngine.java b/src/com/p4square/grow/model/SliderScoringEngine.java
new file mode 100644
index 0000000..76811b3
--- /dev/null
+++ b/src/com/p4square/grow/model/SliderScoringEngine.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * SliderScoringEngine expects the user's answer to be a decimal value in the
+ * range [0, 1]. The value is scaled to the range [1, 4] and added to the
+ * score.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SliderScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ float delta = Float.valueOf(userAnswer.getAnswerId()) * 3 + 1;
+
+ if (delta < 0 || delta > 4) {
+ throw new IllegalArgumentException("Answer out of bounds.");
+ }
+
+ score.sum += delta;
+ score.count++;
+
+ return true;
+ }
+}
diff --git a/src/com/p4square/grow/model/TextQuestion.java b/src/com/p4square/grow/model/TextQuestion.java
new file mode 100644
index 0000000..88c2a34
--- /dev/null
+++ b/src/com/p4square/grow/model/TextQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Text Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TextQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SimpleScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.TEXT;
+ }
+}
diff --git a/src/com/p4square/grow/provider/JsonEncodedProvider.java b/src/com/p4square/grow/provider/JsonEncodedProvider.java
new file mode 100644
index 0000000..605b18c
--- /dev/null
+++ b/src/com/p4square/grow/provider/JsonEncodedProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import java.io.IOException;
+
+import org.codehaus.jackson.map.DeserializationConfig;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.SerializationConfig;
+
+/**
+ * Provider provides a simple interface for loading and persisting
+ * objects.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class JsonEncodedProvider<K, V> implements Provider<K, V> {
+ public static final ObjectMapper MAPPER = new ObjectMapper();
+ static {
+ MAPPER.configure(SerializationConfig.Feature.WRITE_ENUMS_USING_TO_STRING, true);
+ MAPPER.configure(DeserializationConfig.Feature.READ_ENUMS_USING_TO_STRING, true);
+ MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ private final Class<V> mClazz;
+
+ public JsonEncodedProvider(Class<V> clazz) {
+ mClazz = clazz;
+ }
+
+ /**
+ * Encode the object as JSON.
+ *
+ * @param obj The object to encode.
+ * @return The JSON encoding of obj.
+ * @throws IOException if the object cannot be encoded.
+ */
+ protected String encode(V obj) throws IOException {
+ return MAPPER.writeValueAsString(obj);
+ }
+
+ /**
+ * Decode the JSON string as an object.
+ *
+ * @param blob The JSON data to decode.
+ * @return The decoded object or null if blob is null.
+ * @throws IOException If an object cannot be decoded.
+ */
+ protected V decode(String blob) throws IOException {
+ if (blob == null) {
+ return null;
+ }
+
+ V obj = MAPPER.readValue(blob, mClazz);
+ return obj;
+ }
+}
+
diff --git a/src/com/p4square/grow/provider/Provider.java b/src/com/p4square/grow/provider/Provider.java
new file mode 100644
index 0000000..ca6af25
--- /dev/null
+++ b/src/com/p4square/grow/provider/Provider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import java.io.IOException;
+
+/**
+ * Provider provides a simple interface for loading and persisting
+ * objects.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface Provider<K, V> {
+ /**
+ * Retrieve the object with the given key.
+ *
+ * @param key The key for the object.
+ * @return The object or null if not found.
+ */
+ V get(K key) throws IOException;
+
+ /**
+ * Persist the object with the given key.
+ *
+ * @param key The key for the object.
+ * @param obj The object to persist.
+ */
+ void put(K key, V obj) throws IOException;
+}
diff --git a/src/com/p4square/grow/provider/QuestionProvider.java b/src/com/p4square/grow/provider/QuestionProvider.java
new file mode 100644
index 0000000..b569dc8
--- /dev/null
+++ b/src/com/p4square/grow/provider/QuestionProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import java.io.IOException;
+
+import com.p4square.grow.model.Question;
+
+/**
+ * QuestionProvider wraps an existing Provider to get and put Questions.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class QuestionProvider<K> implements Provider<String, Question> {
+
+ private Provider<K, Question> mProvider;
+
+ public QuestionProvider(Provider<K, Question> provider) {
+ mProvider = provider;
+ }
+
+ @Override
+ public Question get(String key) throws IOException {
+ return mProvider.get(makeKey(key));
+ }
+
+ @Override
+ public void put(String key, Question obj) throws IOException {
+ mProvider.put(makeKey(key), obj);
+ }
+
+ /**
+ * Make a Key for questionId.
+ *
+ * @param questionId The question id.
+ * @return a key for questionId.
+ */
+ protected abstract K makeKey(String questionId);
+}
diff --git a/src/templates/templates/question-circle.ftl b/src/templates/templates/question-circle.ftl
index 7f48ff8..fbb2e61 100644
--- a/src/templates/templates/question-circle.ftl
+++ b/src/templates/templates/question-circle.ftl
@@ -9,15 +9,15 @@
<div class="quadQuestion question">
<div class="above">
- <span class="left">${question.topleft}</span>
- <span class="right">${question.topright}</span>
+ <span class="left">${question.topLeft}</span>
+ <span class="right">${question.topRight}</span>
</div>
<div class="middle">
<div class="quad"><img src="${staticRoot}/images/quadselector.png" class="selector" /></div>
</div>
<div class="below">
- <span class="left">${question.bottomleft}</span>
- <span class="right">${question.bottomright}</span>
+ <span class="left">${question.bottomLeft}</span>
+ <span class="right">${question.bottomRight}</span>
</div>
</div>