summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
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>