diff options
38 files changed, 1672 insertions, 235 deletions
diff --git a/devfiles/questions/03.json b/devfiles/questions/03.json index b0d32ab..6c69630 100644 --- a/devfiles/questions/03.json +++ b/devfiles/questions/03.json @@ -2,10 +2,10 @@ "id": "03", "question": "In my life, Jesus is:", "type": "circle", - "topleft": "Leader & Savior and I generally apply his teaching", - "topright": "Leader & Savior and obedience is essential", - "bottomleft": "The Son of God, and I'm learning more about him", - "bottomright": "An important person or figure in history", + "topLeft": "Leader & Savior and I generally apply his teaching", + "topRight": "Leader & Savior and obedience is essential", + "bottomLeft": "The Son of God, and I'm learning more about him", + "bottomRight": "An important person or figure in history", "answers": { "1.00,1.00": { "score": "4" }, diff --git a/devfiles/questions/06.json b/devfiles/questions/06.json index abba954..3bfabf0 100644 --- a/devfiles/questions/06.json +++ b/devfiles/questions/06.json @@ -2,10 +2,10 @@ "id": "06", "question": "I believe the Bible is:", "type": "circle", - "topleft": "Inspired word of God and I try to read it regularly", - "topright": "Authoritative and essential to my life and I read it daily", - "bottomleft": "Pretty good wisdom that I occasionally read", - "bottomright": "A collection of moral stories, but generally not relevant to me", + "topLeft": "Inspired word of God and I try to read it regularly", + "topRight": "Authoritative and essential to my life and I read it daily", + "bottomLeft": "Pretty good wisdom that I occasionally read", + "bottomRight": "A collection of moral stories, but generally not relevant to me", "answers": { "1.00,1.00": { "score": "4" }, diff --git a/devfiles/questions/09.json b/devfiles/questions/09.json index 15ced2d..832cd6c 100644 --- a/devfiles/questions/09.json +++ b/devfiles/questions/09.json @@ -2,10 +2,10 @@ "id": "09", "question": "Prayer for me is:", "type": "circle", - "topleft": "Important and I pray regularly", - "topright": "Constant, conversational, a lifestyle, and absolutely indispensible in my life", - "bottomleft": "Helpful I think, and I pray occasionally", - "bottomright": "Not important, I don't believe it's helpful or useful", + "topLeft": "Important and I pray regularly", + "topRight": "Constant, conversational, a lifestyle, and absolutely indispensible in my life", + "bottomLeft": "Helpful I think, and I pray occasionally", + "bottomRight": "Not important, I don't believe it's helpful or useful", "answers": { "1.00,1.00": { "score": "4" }, diff --git a/devfiles/questions/13.json b/devfiles/questions/13.json index 7a74502..236b5b1 100644 --- a/devfiles/questions/13.json +++ b/devfiles/questions/13.json @@ -2,16 +2,16 @@ "id": "13", "question": "Those closest to me are:", "type": "circle", - "bottomright": "Outside the church", - "bottomleft": "Inside the church but don't know my secrets", - "topleft": "Inside the church and know me a bit", - "topright": "Church family, knows me well, and prays for me", + "bottomRight": "Outside the church", + "bottomLeft": "Inside the church but don't know my secrets", + "topLeft": "Inside the church and know me a bit", + "topRight": "Church family, knows me well, and prays for me", "answers": { - "1.00,1.00": { "score": "2" }, - "1.00,-1.00": { "score": "4" }, - "-1.00,-1.00": { "score": "3" }, - "-1.00,1.00": { "score": "1" } + "1.00,1.00": { "score": "4" }, + "1.00,-1.00": { "score": "1" }, + "-1.00,-1.00": { "score": "2" }, + "-1.00,1.00": { "score": "3" } }, "previousQuestion": "12", diff --git a/devfiles/questions/17.json b/devfiles/questions/17.json index b1b3f5e..2469a13 100644 --- a/devfiles/questions/17.json +++ b/devfiles/questions/17.json @@ -2,16 +2,16 @@ "id": "17", "question": "Character, integrity, and excellent personal conduct are:", "type": "circle", - "bottomleft": "what I aspire toward, but I'm a workin progress", - "bottomright": "Old Fashioned and not realistic", - "topleft": "Important but you can lead others if you're trying hard", - "topright": "Essential to me personally, and required in order to lead others", + "bottomLeft": "What I aspire toward, but I'm a work in progress", + "bottomRight": "Old Fashioned and not realistic", + "topLeft": "Important but you can lead others if you're trying hard", + "topRight": "Essential to me personally, and required in order to lead others", "answers": { - "1.00,1.00": { "score": "1" }, - "1.00,-1.00": { "score": "4" }, - "-1.00,-1.00": { "score": "2" }, - "-1.00,1.00": { "score": "3" } + "1.00,1.00": { "score": "4" }, + "1.00,-1.00": { "score": "1" }, + "-1.00,-1.00": { "score": "3" }, + "-1.00,1.00": { "score": "2" } }, "previousQuestion": "16", @@ -1,8 +1,11 @@ <ivy-module version="2.0" xmlns:m="http://ant.apache.org/ivy/maven"> <info organisation="com.p4square.grow" module="frontend" revision="1.0-snapshot" /> - + <configurations> <conf name="default" /> + <conf name="build" extends="default" /> + <conf name="test" extends="default" /> + <conf name="runtime" extends="default" /> <conf name="sources" /> </configurations> @@ -11,15 +14,18 @@ <artifact type="jar" ext="jar" conf="default" /> <artifact type="source" ext="jar" conf="sources" m:classifier="sources" /> </publications> - - <dependencies defaultconf="default,sources"> - <dependency org="net.jesterpm" name="fmfacade" rev="[1.0-SNAPSHOT,)" /> - <dependency org="org.restlet.jee" name="org.restlet.ext.httpclient" rev="[2.1,)" /> - <dependency org="org.restlet.jee" name="org.restlet.ext.wadl" rev="[2.1,)" /> + + <dependencies defaultconfmapping="*->default"> + <dependency org="junit" name="junit" rev="4.7" conf="test" /> + <dependency org="net.sourceforge.cobertura" name="cobertura" rev="1.9rc1" conf="test" /> + + <dependency org="net.jesterpm" name="fmfacade" rev="[1.0-SNAPSHOT,)" conf="default" /> + <dependency org="org.restlet.jee" name="org.restlet.ext.httpclient" rev="[2.1,)" conf="default" /> + <dependency org="org.restlet.jee" name="org.restlet.ext.wadl" rev="[2.1,)" conf="default" /> <!-- Backend Dependencies --> - <dependency org="com.netflix.astyanax" name="astyanax-core" rev="[1.0,)" /> - <dependency org="com.netflix.astyanax" name="astyanax-thrift" rev="[1.0,)" /> - <dependency org="com.netflix.astyanax" name="astyanax-cassandra" rev="[1.0,)" /> + <dependency org="com.netflix.astyanax" name="astyanax-core" rev="[1.0,)" conf="default" /> + <dependency org="com.netflix.astyanax" name="astyanax-thrift" rev="[1.0,)" conf="default" /> + <dependency org="com.netflix.astyanax" name="astyanax-cassandra" rev="[1.0,)" conf="default" /> </dependencies> </ivy-module> 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> diff --git a/tst/com/p4square/grow/model/AnswerTest.java b/tst/com/p4square/grow/model/AnswerTest.java new file mode 100644 index 0000000..1747773 --- /dev/null +++ b/tst/com/p4square/grow/model/AnswerTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for the Answer class. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class AnswerTest { + private static final double DELTA = 1e-15; + + public static void main(String... args) { + org.junit.runner.JUnitCore.main(AnswerTest.class.getName()); + } + + /** + * Verify that the correct default values are returned. + */ + @Test + public void testDefaults() { + Answer a = new Answer(); + + // Type should default to AVERAGE + assertEquals(Answer.ScoreType.AVERAGE, a.getType()); + + // NextQuestion should default to null + assertNull(a.getNextQuestion()); + } + + /** + * Verify that getters and setters function correctly. + */ + @Test + public void testGetAndSet() { + Answer a = new Answer(); + + a.setText("Answer Text"); + assertEquals("Answer Text", a.getText()); + + a.setType(Answer.ScoreType.TRUMP); + assertEquals(Answer.ScoreType.TRUMP, a.getType()); + + a.setScore(10); + assertEquals(10, a.getScore(), DELTA); + + a.setNextQuestion("nextQuestion"); + assertEquals("nextQuestion", a.getNextQuestion()); + } + + /** + * Verify that when the ScoreType is NONE, the score is 0. + */ + @Test + public void testScoreTypeNone() { + Answer a = new Answer(); + + a.setScore(10); + assertEquals(10, a.getScore(), DELTA); + + a.setType(Answer.ScoreType.NONE); + assertEquals(0, a.getScore(), DELTA); + } + + /** + * Test score() with type TRUMP. + */ + @Test + public void testScoreTrump() { + Score score = new Score(); + score.sum = 10; + score.count = 2; + + Answer a = new Answer(); + a.setType(Answer.ScoreType.TRUMP); + a.setScore(5); + + assertFalse(a.score(score)); + + assertEquals(5, score.getSum(), DELTA); + assertEquals(1, score.getCount()); + } + + /** + * Test score() with type NONE. + */ + @Test + public void testScoreNone() { + Score score = new Score(); + score.sum = 10; + score.count = 2; + + Answer a = new Answer(); + a.setScore(5); + a.setType(Answer.ScoreType.NONE); + + assertTrue(a.score(score)); + + assertEquals(10, score.getSum(), DELTA); + assertEquals(2, score.getCount()); + } + + /** + * Test score() with type AVERAGE. + */ + @Test + public void testScoreAverage() { + Score score = new Score(); + score.sum = 10; + score.count = 2; + + Answer a = new Answer(); + a.setScore(5); + a.setType(Answer.ScoreType.AVERAGE); + + assertTrue(a.score(score)); + + assertEquals(15, score.getSum(), DELTA); + assertEquals(3, score.getCount()); + } + + /** + * Verify that ScoreType.toString() returns the proper strings. + */ + @Test + public void testScoreTypeToString() { + assertEquals("none", Answer.ScoreType.NONE.toString()); + assertEquals("average", Answer.ScoreType.AVERAGE.toString()); + assertEquals("trump", Answer.ScoreType.TRUMP.toString()); + } +} diff --git a/tst/com/p4square/grow/model/PointTest.java b/tst/com/p4square/grow/model/PointTest.java new file mode 100644 index 0000000..a4e7cc0 --- /dev/null +++ b/tst/com/p4square/grow/model/PointTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for the Point class. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class PointTest { + private static final double DELTA = 1e-15; + + public static void main(String... args) { + org.junit.runner.JUnitCore.main(PointTest.class.getName()); + } + + /** + * Verify that the constructor works properly. + */ + @Test + public void testHappyCase() { + Point p = new Point(1, 2); + assertEquals(1, p.getX(), DELTA); + assertEquals(2, p.getY(), DELTA); + } + + /** + * Verify distance is computed correctly. + */ + @Test + public void testDistance() { + Point p1, p2; + + // Simple line + p1 = new Point(2, 1); + p2 = new Point(-2, 1); + assertEquals(4, p1.distance(p2), DELTA); + assertEquals(4, p2.distance(p1), DELTA); + + // Across origin + p1 = new Point(5, 1); + p2 = new Point(-3, -2); + assertEquals(Math.sqrt(73), p1.distance(p2), DELTA); + assertEquals(Math.sqrt(73), p2.distance(p1), DELTA); + } + + /** + * Verify toString returns the expected string. + */ + @Test + public void testToString() { + Point p = new Point(-1.12345, 2.3); + assertEquals("-1.12,2.30", p.toString()); + } + + /** + * Verify that valueOf correctly parses a variety of strings. + */ + @Test + public void testValueOfHappyCase() { + Point p; + + p = Point.valueOf("1,2"); + assertEquals(1, p.getX(), DELTA); + assertEquals(2, p.getY(), DELTA); + + p = Point.valueOf("1.5,2.0"); + assertEquals(1.5, p.getX(), DELTA); + assertEquals(2.0, p.getY(), DELTA); + + p = Point.valueOf("-1.5,2.0"); + assertEquals(-1.5, p.getX(), DELTA); + assertEquals(2.0, p.getY(), DELTA); + + p = Point.valueOf("1.5,-2.0"); + assertEquals(1.5, p.getX(), DELTA); + assertEquals(-2.0, p.getY(), DELTA); + + p = Point.valueOf("-1.5,-2.0"); + assertEquals(-1.5, p.getX(), DELTA); + assertEquals(-2.0, p.getY(), DELTA); + } + + /** + * Verify that valueOf fails on null string. + */ + @Test(expected = NullPointerException.class) + public void testValueOfNull() { + Point.valueOf(null); + } + + /** + * Verify that valueOf fails on empty string. + */ + @Test(expected = IllegalArgumentException.class) + public void testValueOfEmptyString() { + Point.valueOf(""); + } + + /** + * Verify that valueOf fails on missing comma. + */ + @Test(expected = IllegalArgumentException.class) + public void testValueOfMissingComma() { + Point.valueOf("123"); + } + + /** + * Verify that valueOf fails on missing x. + */ + @Test(expected = IllegalArgumentException.class) + public void testValueOfMissingX() { + Point.valueOf(",12"); + } + + /** + * Verify that valueOf fails on missing y. + */ + @Test(expected = IllegalArgumentException.class) + public void testValueOfMissingY() { + Point.valueOf("12,"); + } +} diff --git a/tst/com/p4square/grow/model/QuadScoringEngineTest.java b/tst/com/p4square/grow/model/QuadScoringEngineTest.java new file mode 100644 index 0000000..246a59f --- /dev/null +++ b/tst/com/p4square/grow/model/QuadScoringEngineTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test the QuadScoringEngine. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuadScoringEngineTest { + private static final double DELTA = 1e-4; + + public static void main(String... args) { + org.junit.runner.JUnitCore.main(QuadScoringEngineTest.class.getName()); + } + + private Question mQuestion; + private ScoringEngine mEngine; + + @Before + public void setup() { + // Setup the Question + mQuestion = new QuadQuestion(); + Map<String, Answer> answers = mQuestion.getAnswers(); + + // Create four answers at (-1,-1), (1, -1), (-1, 1), (1, 1) + for (int i = 1; i <= 4; i++) { + int x = i % 2 == 0 ? 1 : -1; + int y = i > 2 ? 1 : -1; + + Answer a = new Answer(); + a.setScore(i); + answers.put(x + ".00," + y + ".00", a); + } + + mEngine = new QuadScoringEngine(); + } + + /** + * Test a point inside each quadrant. + */ + @Test + public void testEachQuadrant() { + Score score; + RecordedAnswer answer = new RecordedAnswer(); + + // 0.5,0.5 == 4 + score = new Score(); + answer.setAnswerId("0.5,0.5"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(4, score.getSum(), DELTA); + assertEquals(1, score.getCount()); + + // 0.5,-0.5 == 2 + score = new Score(); + answer.setAnswerId("0.5,-0.5"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(2, score.getSum(), DELTA); + assertEquals(1, score.getCount()); + + // -0.5,0.5 == 3 + score = new Score(); + answer.setAnswerId("-0.5,0.5"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(3, score.getSum(), DELTA); + assertEquals(1, score.getCount()); + + // -0.5,-0.5 == 0.5 + score = new Score(); + answer.setAnswerId("-0.5,-0.5"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(1, score.getSum(), DELTA); + assertEquals(1, score.getCount()); + } + +} diff --git a/tst/com/p4square/grow/model/QuestionTest.java b/tst/com/p4square/grow/model/QuestionTest.java new file mode 100644 index 0000000..d09d2d8 --- /dev/null +++ b/tst/com/p4square/grow/model/QuestionTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for the Question class. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuestionTest { + public static void main(String... args) { + org.junit.runner.JUnitCore.main(QuestionTest.class.getName()); + } + + /** + * Verify that all the getters and setters function. + */ + @Test + public void testGetAndSet() { + TextQuestion q = new TextQuestion(); + + q.setId("123"); + assertEquals("123", q.getId()); + + q.setQuestion("Hello World"); + assertEquals("Hello World", q.getQuestion()); + + q.setPreviousQuestion("122"); + assertEquals("122", q.getPreviousQuestion()); + + q.setNextQuestion("124"); + assertEquals("124", q.getNextQuestion()); + } + + /** + * Verify the correct next question is returned. + */ + @Test + public void testGetNextQuestion() { + // Setup the Question + TextQuestion q = new TextQuestion(); + q.setNextQuestion("defaultNext"); + + Answer answerWithNext = new Answer(); + answerWithNext.setNextQuestion("answerNext"); + + q.getAnswers().put("withNext", answerWithNext); + q.getAnswers().put("withoutNext", new Answer()); + + // Answer without a nextQuestion should return default. + assertEquals("defaultNext", q.getNextQuestion("withoutNext")); + + // Answer with a nextQuestion should return it's next question. + assertEquals("answerNext", q.getNextQuestion("withNext")); + + // Unknown answer should also return the default + assertEquals("defaultNext", q.getNextQuestion("unknownAnswer")); + } + + /** + * Validate the toString() results for the enum. + * + * This may seem like an odd test, but it is very important for these to be + * lowercase to match the values in the JSON files. + */ + @Test + public void testToString() { + assertEquals("text", Question.QuestionType.TEXT.toString()); + assertEquals("image", Question.QuestionType.IMAGE.toString()); + assertEquals("slider", Question.QuestionType.SLIDER.toString()); + assertEquals("quad", Question.QuestionType.QUAD.toString()); + assertEquals("circle", Question.QuestionType.CIRCLE.toString()); + } +} diff --git a/tst/com/p4square/grow/model/SimpleScoringEngineTest.java b/tst/com/p4square/grow/model/SimpleScoringEngineTest.java new file mode 100644 index 0000000..1a1bc95 --- /dev/null +++ b/tst/com/p4square/grow/model/SimpleScoringEngineTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test the SimpleScoringEngine. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SimpleScoringEngineTest { + private static final double DELTA = 1e-15; + + public static void main(String... args) { + org.junit.runner.JUnitCore.main(SimpleScoringEngineTest.class.getName()); + } + + private Question mQuestion; + private ScoringEngine mEngine; + + @Before + public void setup() { + // Setup the Question + mQuestion = new TextQuestion(); + + for (int i = 0; i <= 4; i++) { + Answer a = new Answer(); + a.setScore(i); + mQuestion.getAnswers().put("a" + i, a); + } + + mEngine = new SimpleScoringEngine(); + } + + /** + * Test that each individual answer is scored correctly. + */ + @Test + public void testAllAnswers() { + for (int i = 1; i <= 4; i++) { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("a" + i); + + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + + assertEquals(1, score.count); + assertEquals(i, score.sum, DELTA); + } + } + + /** + * Test that each answer score forms an increasing sum. + */ + @Test + public void testAllAnswersIncremental() { + Score score = new Score(); + + for (int i = 1; i <= 4; i++) { + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("a" + i); + + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + } + + assertEquals(4, score.count); + assertEquals(10, score.sum, DELTA); + } + + /** + * Verify exception is thrown for undefined answer. + */ + @Test(expected = IllegalArgumentException.class) + public void testUnknownAnswer() { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("unknown"); + mEngine.scoreAnswer(score, mQuestion, answer); + } +} diff --git a/tst/com/p4square/grow/model/SliderScoringEngineTest.java b/tst/com/p4square/grow/model/SliderScoringEngineTest.java new file mode 100644 index 0000000..fdbfd6f --- /dev/null +++ b/tst/com/p4square/grow/model/SliderScoringEngineTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test the SliderScoringEngine. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SliderScoringEngineTest { + private static final double DELTA = 1e-4; + + public static void main(String... args) { + org.junit.runner.JUnitCore.main(SliderScoringEngineTest.class.getName()); + } + + private Question mQuestion; + private ScoringEngine mEngine; + + @Before + public void setup() { + // Setup the Question + mQuestion = new SliderQuestion(); + mEngine = new SliderScoringEngine(); + } + + /** + * Test the scoreAnswer() method. + */ + @Test + public void testScoreAnswer() { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + + // Test 0 + answer.setAnswerId("0"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(1, score.count); + assertEquals(1, score.sum, DELTA); + + // Test 1 + answer.setAnswerId("1"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(2, score.count); + assertEquals(5, score.sum, DELTA); + + // Test fraction (0.33) + answer.setAnswerId("0.33333"); + assertTrue(mEngine.scoreAnswer(score, mQuestion, answer)); + assertEquals(3, score.count); + assertEquals(7, score.sum, DELTA); + } + + /** + * Verify exception is thrown for non-numeric answer. + */ + @Test(expected = IllegalArgumentException.class) + public void testNonNumericAnswer() { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("unknown"); + mEngine.scoreAnswer(score, mQuestion, answer); + } + + /** + * Verify exception is thrown for negative answer. + */ + @Test(expected = IllegalArgumentException.class) + public void testNegativeAnswer() { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("-1"); + mEngine.scoreAnswer(score, mQuestion, answer); + } + + /** + * Verify exception is thrown for out of bounds answer. + */ + @Test(expected = IllegalArgumentException.class) + public void testAnswerOutOfBounds() { + Score score = new Score(); + RecordedAnswer answer = new RecordedAnswer(); + answer.setAnswerId("1.1"); + mEngine.scoreAnswer(score, mQuestion, answer); + } +} |