summaryrefslogtreecommitdiff
path: root/src/com/p4square/grow/backend
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/p4square/grow/backend')
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java138
-rw-r--r--src/com/p4square/grow/backend/db/CassandraDatabase.java192
-rw-r--r--src/com/p4square/grow/backend/resources/Answer.java57
-rw-r--r--src/com/p4square/grow/backend/resources/Point.java52
-rw-r--r--src/com/p4square/grow/backend/resources/Question.java72
-rw-r--r--src/com/p4square/grow/backend/resources/Score.java34
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResource.java70
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResultsResource.java252
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingRecordResource.java155
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingResource.java88
10 files changed, 1110 insertions, 0 deletions
diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java
new file mode 100644
index 0000000..515cd1b
--- /dev/null
+++ b/src/com/p4square/grow/backend/GrowBackend.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package com.p4square.grow.backend;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.data.Protocol;
+import org.restlet.Restlet;
+import org.restlet.routing.Router;
+
+import com.p4square.grow.config.Config;
+
+import com.p4square.grow.backend.db.CassandraDatabase;
+import com.p4square.grow.backend.resources.SurveyResource;
+import com.p4square.grow.backend.resources.SurveyResultsResource;
+import com.p4square.grow.backend.resources.TrainingResource;
+import com.p4square.grow.backend.resources.TrainingRecordResource;
+
+/**
+ * Main class for the backend application.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class GrowBackend extends Application {
+ private final static Logger cLog = Logger.getLogger(GrowBackend.class);
+
+ private final Config mConfig;
+ private final CassandraDatabase mDatabase;
+
+ public GrowBackend() {
+ mConfig = new Config();
+ mDatabase = new CassandraDatabase();
+ }
+
+ @Override
+ public Restlet createInboundRoot() {
+ Router router = new Router(getContext());
+
+ // Survey API
+ router.attach("/assessment/question/{questionId}", SurveyResource.class);
+
+ router.attach("/accounts/{userId}/assessment", SurveyResultsResource.class);
+ router.attach("/accounts/{userId}/assessment/answers/{questionId}",
+ SurveyResultsResource.class);
+
+ // Training API
+ router.attach("/training/{level}", TrainingResource.class);
+ router.attach("/training/{level}/videos/{videoId}", TrainingResource.class);
+
+ router.attach("/accounts/{userId}/training", TrainingRecordResource.class);
+ router.attach("/accounts/{userId}/training/videos/{videoId}",
+ TrainingRecordResource.class);
+
+
+ return router;
+ }
+
+ /**
+ * Open the database.
+ */
+ @Override
+ public void start() throws Exception {
+ super.start();
+
+ // Load config
+ final String configDomain =
+ getContext().getParameters().getFirstValue("configDomain");
+ if (configDomain != null) {
+ mConfig.setDomain(configDomain);
+ }
+
+ mConfig.updateConfig(this.getClass().getResourceAsStream("/grow.properties"));
+
+ final String configFilename =
+ getContext().getParameters().getFirstValue("configFile");
+
+ if (configFilename != null) {
+ mConfig.updateConfig(configFilename);
+ }
+
+ // Setup database
+ mDatabase.setClusterName(mConfig.getString("clusterName", "Dev Cluster"));
+ mDatabase.setKeyspaceName(mConfig.getString("keyspace", "GROW"));
+ mDatabase.init();
+ }
+
+ /**
+ * Close the database.
+ */
+ @Override
+ public void stop() throws Exception {
+ cLog.info("Shutting down...");
+ mDatabase.close();
+
+ super.stop();
+ }
+
+ /**
+ * @return the current database.
+ */
+ public CassandraDatabase getDatabase() {
+ return mDatabase;
+ }
+
+ /**
+ * Stand-alone main for testing.
+ */
+ public static void main(String[] args) throws Exception {
+ // Start the HTTP Server
+ final Component component = new Component();
+ component.getServers().add(Protocol.HTTP, 9095);
+ component.getClients().add(Protocol.HTTP);
+ component.getDefaultHost().attach(new GrowBackend());
+
+ // Setup shutdown hook
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ public void run() {
+ try {
+ component.stop();
+ } catch (Exception e) {
+ cLog.error("Exception during cleanup", e);
+ }
+ }
+ });
+
+ cLog.info("Starting server...");
+
+ try {
+ component.start();
+ } catch (Exception e) {
+ cLog.fatal("Could not start: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/com/p4square/grow/backend/db/CassandraDatabase.java b/src/com/p4square/grow/backend/db/CassandraDatabase.java
new file mode 100644
index 0000000..e40c251
--- /dev/null
+++ b/src/com/p4square/grow/backend/db/CassandraDatabase.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.db;
+
+import com.netflix.astyanax.AstyanaxContext;
+import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
+import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl;
+import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor;
+import com.netflix.astyanax.connectionpool.NodeDiscoveryType;
+import com.netflix.astyanax.connectionpool.OperationResult;
+import com.netflix.astyanax.impl.AstyanaxConfigurationImpl;
+import com.netflix.astyanax.Keyspace;
+import com.netflix.astyanax.ColumnMutation;
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnFamily;
+import com.netflix.astyanax.model.ColumnList;
+import com.netflix.astyanax.MutationBatch;
+import com.netflix.astyanax.serializers.StringSerializer;
+import com.netflix.astyanax.thrift.ThriftFamilyFactory;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Cassandra Database Abstraction for the Backend.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CassandraDatabase {
+ private static Logger cLog = Logger.getLogger(CassandraDatabase.class);
+
+ // Configuration fields.
+ private String mClusterName;
+ private String mKeyspaceName;
+ private String mSeedEndpoint = "127.0.0.1:9160";
+ private int mPort = 9160;
+
+ private AstyanaxContext<Keyspace> mContext;
+ private Keyspace mKeyspace;
+
+ /**
+ * Connect to Cassandra.
+ *
+ * Cluster and Keyspace must be set before calling init().
+ */
+ public void init() {
+ mContext = new AstyanaxContext.Builder()
+ .forCluster(mClusterName)
+ .forKeyspace(mKeyspaceName)
+ .withAstyanaxConfiguration(new AstyanaxConfigurationImpl()
+ .setDiscoveryType(NodeDiscoveryType.RING_DESCRIBE)
+ )
+ .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("MyConnectionPool")
+ .setPort(mPort)
+ .setMaxConnsPerHost(1)
+ .setSeeds(mSeedEndpoint)
+ )
+ .withConnectionPoolMonitor(new CountingConnectionPoolMonitor())
+ .buildKeyspace(ThriftFamilyFactory.getInstance());
+
+ mContext.start();
+ mKeyspace = mContext.getClient();
+ }
+
+ /**
+ * Close the database connection.
+ */
+ public void close() {
+ mContext.shutdown();
+ }
+
+ /**
+ * Set the cluster name to connect to.
+ */
+ public void setClusterName(final String cluster) {
+ mClusterName = cluster;
+ }
+
+ /**
+ * Set the name of the keyspace to open.
+ */
+ public void setKeyspaceName(final String keyspace) {
+ mKeyspaceName = keyspace;
+ }
+
+ /**
+ * Change the seed endpoint.
+ * The default is 127.0.0.1:9160.
+ */
+ public void setSeedEndpoint(final String endpoint) {
+ mSeedEndpoint = endpoint;
+ }
+
+ /**
+ * Change the port to connect to.
+ * The default is 9160.
+ */
+ public void setPort(final int port) {
+ mPort = port;
+ }
+
+ /**
+ * @return The entire row associated with this key.
+ */
+ public ColumnList<String> getRow(final String cfName, final String key) {
+ try {
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ OperationResult<ColumnList<String>> result =
+ mKeyspace.prepareQuery(cf)
+ .getKey(key)
+ .execute();
+
+ return result.getResult();
+
+ } catch (ConnectionException e) {
+ cLog.error("getRow failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @return The value associated with the given key.
+ */
+ public String getKey(final String cfName, final String key) {
+ return getKey(cfName, key, "value");
+ }
+
+ /**
+ * @return The value associated with the given key, column pair.
+ */
+ public String getKey(final String cfName, final String key, final String column) {
+ final ColumnList<String> row = getRow(cfName, key);
+
+ if (row != null) {
+ final Column rowColumn = row.getColumnByName(column);
+ if (rowColumn != null) {
+ return rowColumn.getStringValue();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Assign value to key.
+ */
+ public void putKey(final String cfName, final String key, final String value) {
+ putKey(cfName, key, "value", value);
+ }
+
+ /**
+ * Assign value to the key, column pair.
+ */
+ public void putKey(final String cfName, final String key,
+ final String column, final String value) {
+
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ MutationBatch m = mKeyspace.prepareMutationBatch();
+ m.withRow(cf, key).putColumn(column, value);
+
+ try {
+ m.execute();
+ } catch (ConnectionException e) {
+ cLog.error("putKey failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Remove a key, column pair.
+ */
+ public void deleteKey(final String cfName, final String key, final String column) {
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ try {
+ ColumnMutation m = mKeyspace.prepareColumnMutation(cf, key, column);
+ m.deleteColumn().execute();
+ } catch (ConnectionException e) {
+ cLog.error("deleteKey failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Answer.java b/src/com/p4square/grow/backend/resources/Answer.java
new file mode 100644
index 0000000..5ba1bce
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/Answer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Map;
+
+/**
+ * This is the model of an assessment question's answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class Answer {
+ public static enum ScoreType {
+ NONE, AVERAGE, TRUMP;
+ }
+
+ private final String mAnswerId;
+ private final String mAnswerText;
+ private final ScoreType mType;
+ private final float mScoreFactor;
+
+ 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());
+ }
+
+ if (mType != ScoreType.NONE) {
+ mScoreFactor = Float.valueOf((String) answer.get("score"));
+ } else {
+ mScoreFactor = 0;
+ }
+
+ }
+
+ public String getId() {
+ return mAnswerId;
+ }
+
+ public String getText() {
+ return mAnswerText;
+ }
+
+ public ScoreType getType() {
+ return mType;
+ }
+
+ public float getScoreFactor() {
+ return mScoreFactor;
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Point.java b/src/com/p4square/grow/backend/resources/Point.java
new file mode 100644
index 0000000..e1b15a8
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/Point.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+/**
+ * Simple double based point class.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class Point {
+ public static Point valueOf(String str) {
+ final int comma = str.indexOf(',');
+ if (comma == -1) {
+ throw new IllegalArgumentException("Malformed point string");
+ }
+
+ final String sX = str.substring(0, comma);
+ final String sY = str.substring(comma + 1);
+
+ return new Point(Double.valueOf(sX), Double.valueOf(sY));
+ }
+
+ private final double mX;
+ private final double mY;
+
+ public Point(double x, double y) {
+ mX = x;
+ mY = y;
+ }
+
+ public double distance(Point other) {
+ final double dx = mX - other.mX;
+ final double dy = mY - other.mY;
+
+ return Math.sqrt(dx*dx + dy*dy);
+ }
+
+ public double getX() {
+ return mX;
+ }
+
+ public double getY() {
+ return mY;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%.2f,%.2f", mX, mY);
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Question.java b/src/com/p4square/grow/backend/resources/Question.java
new file mode 100644
index 0000000..c53883c
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/Question.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Model of an assessment question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class Question {
+ public static enum QuestionType {
+ TEXT, IMAGE, SLIDER, QUAD;
+ }
+
+ private final String mQuestionId;
+ private final QuestionType mType;
+ private final String mQuestionText;
+ private Map<String, Answer> mAnswers;
+
+ private final String mPreviousQuestionId;
+ private final String mNextQuestionId;
+
+ public Question(final Map<String, Object> 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");
+
+ 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);
+ }
+ }
+
+ public String getId() {
+ return mQuestionId;
+ }
+
+ public QuestionType getType() {
+ return mType;
+ }
+
+ public String getText() {
+ return mQuestionText;
+ }
+
+ public String getPrevious() {
+ return mPreviousQuestionId;
+ }
+
+ public String getNext() {
+ return mNextQuestionId;
+ }
+
+ public Map<String, Answer> getAnswers() {
+ return Collections.unmodifiableMap(mAnswers);
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/backend/resources/Score.java
new file mode 100644
index 0000000..c7e5ecc
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/Score.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+/**
+ * Simple structure containing a score's sum and count.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class Score {
+ double sum;
+ int count;
+
+ @Override
+ public String toString() {
+ final double score = sum / count;
+
+ if (score >= 4) {
+ return "teacher";
+
+ } else if (score >= 3) {
+ return "disciple";
+
+ } else if (score >= 2) {
+ return "believer";
+
+ } else {
+ return "seeker";
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/backend/resources/SurveyResource.java b/src/com/p4square/grow/backend/resources/SurveyResource.java
new file mode 100644
index 0000000..d22d763
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/SurveyResource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Map;
+import java.util.HashMap;
+
+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.apache.log4j.Logger;
+
+import com.p4square.grow.backend.GrowBackend;
+import com.p4square.grow.backend.db.CassandraDatabase;
+
+/**
+ * This resource manages assessment questions.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SurveyResource extends ServerResource {
+ private final static Logger cLog = Logger.getLogger(SurveyResource.class);
+
+ private CassandraDatabase mDb;
+
+ private String mQuestionId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ final GrowBackend backend = (GrowBackend) getApplication();
+ mDb = backend.getDatabase();
+
+ mQuestionId = getAttribute("questionId");
+ }
+
+ /**
+ * Handle GET Requests.
+ */
+ @Override
+ protected Representation get() {
+ String result = "";
+
+ if (mQuestionId == null) {
+ // TODO: List all question ids
+
+ } else if (mQuestionId.equals("first")) {
+ // TODO: Get the first question id from db?
+ result = "1";
+
+ } else {
+ // Get a question by id
+ result = mDb.getKey("strings", "/questions/" + mQuestionId);
+
+ if (result == null) {
+ // 404
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+ }
+
+ return new StringRepresentation(result);
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
new file mode 100644
index 0000000..db7dad0
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnList;
+
+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.apache.log4j.Logger;
+
+import com.p4square.grow.backend.GrowBackend;
+import com.p4square.grow.backend.db.CassandraDatabase;
+
+/**
+ * Store the user's answers to the assessment and generate their score.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SurveyResultsResource extends ServerResource {
+ private final static Logger cLog = Logger.getLogger(SurveyResultsResource.class);
+
+ private final static ObjectMapper cMapper = new ObjectMapper();
+
+ static enum RequestType {
+ ASSESSMENT, ANSWER
+ }
+
+ private CassandraDatabase mDb;
+
+ private RequestType mRequestType;
+ private String mUserId;
+ private String mQuestionId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ final GrowBackend backend = (GrowBackend) getApplication();
+ mDb = backend.getDatabase();
+
+ mUserId = getAttribute("userId");
+ mQuestionId = getAttribute("questionId");
+
+ mRequestType = RequestType.ASSESSMENT;
+ if (mQuestionId != null) {
+ mRequestType = RequestType.ANSWER;
+ }
+ }
+
+ /**
+ * Handle GET Requests.
+ */
+ @Override
+ protected Representation get() {
+ String result = null;
+
+ switch (mRequestType) {
+ case ANSWER:
+ result = mDb.getKey("assessments", mUserId, mQuestionId);
+ break;
+
+ case ASSESSMENT:
+ result = buildAssessment();
+ break;
+ }
+
+ if (result == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ return new StringRepresentation(result);
+ }
+
+ /**
+ * Handle PUT requests
+ */
+ @Override
+ protected Representation put(Representation entity) {
+ boolean success = false;
+
+ switch (mRequestType) {
+ case ANSWER:
+ try {
+ mDb.putKey("assessments", mUserId, mQuestionId, entity.getText());
+ mDb.putKey("assessments", mUserId, "lastAnswered", mQuestionId);
+ mDb.deleteKey("assessments", mUserId, "summary");
+ success = true;
+
+ } catch (Exception e) {
+ cLog.warn("Caught exception putting answer: " + e.getMessage(), e);
+ }
+ break;
+
+ default:
+ setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+ }
+
+ if (success) {
+ setStatus(Status.SUCCESS_NO_CONTENT);
+
+ } else {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ }
+
+ return null;
+ }
+
+ /**
+ * This method compiles assessment results.
+ */
+ private String buildAssessment() {
+ StringBuilder sb = new StringBuilder("{ ");
+
+ // Last question answered
+ final String lastAnswered = mDb.getKey("assessments", mUserId, "lastAnswered");
+ if (lastAnswered != null) {
+ sb.append("\"lastAnswered\": \"" + lastAnswered + "\"");
+ }
+
+ // Compute score
+ ColumnList<String> row = mDb.getRow("assessments", mUserId);
+ if (!row.isEmpty()) {
+ Score score = new Score();
+ for (Column<String> c : row) {
+ if (c.getName().equals("lastAnswered")) {
+ continue;
+ }
+
+ final String questionId = c.getName();
+ final String answerId = c.getStringValue();
+ if (!scoreQuestion(score, questionId, answerId)) {
+ break;
+ }
+ }
+
+ sb.append(", \"score\":" + score.sum / score.count);
+ sb.append(", \"sum\":" + score.sum);
+ sb.append(", \"count\":" + score.count);
+ sb.append(", \"result\":\"" + score.toString() + "\"");
+ }
+
+ sb.append(" }");
+ return sb.toString();
+ }
+
+ private boolean scoreQuestion(final Score score, final String questionId,
+ final String answerJson) {
+
+ final String data = mDb.getKey("strings", "/questions/" + questionId);
+
+ try {
+ final Map<?,?> questionMap = cMapper.readValue(data, Map.class);
+ final Map<?,?> answerMap = cMapper.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 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
new file mode 100644
index 0000000..93f4fbc
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnList;
+
+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.apache.log4j.Logger;
+
+import com.p4square.grow.backend.GrowBackend;
+import com.p4square.grow.backend.db.CassandraDatabase;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TrainingRecordResource extends ServerResource {
+ private final static Logger cLog = Logger.getLogger(TrainingRecordResource.class);
+
+ static enum RequestType {
+ SUMMARY, VIDEO
+ }
+
+ private GrowBackend mBackend;
+ private CassandraDatabase mDb;
+
+ private RequestType mRequestType;
+ private String mUserId;
+ private String mVideoId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mBackend = (GrowBackend) getApplication();
+ mDb = mBackend.getDatabase();
+
+ mUserId = getAttribute("userId");
+ mVideoId = getAttribute("videoId");
+
+ mRequestType = RequestType.SUMMARY;
+ if (mVideoId != null) {
+ mRequestType = RequestType.VIDEO;
+ }
+ }
+
+ /**
+ * Handle GET Requests.
+ */
+ @Override
+ protected Representation get() {
+ String result = null;
+
+ switch (mRequestType) {
+ case VIDEO:
+ result = mDb.getKey("training", mUserId, mVideoId);
+ break;
+
+ case SUMMARY:
+ result = buildSummary();
+ break;
+ }
+
+ if (result == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ return new StringRepresentation(result);
+ }
+
+ /**
+ * Handle PUT requests
+ */
+ @Override
+ protected Representation put(Representation entity) {
+ boolean success = false;
+
+ switch (mRequestType) {
+ case VIDEO:
+ try {
+ mDb.putKey("training", mUserId, mVideoId, entity.getText());
+ mDb.putKey("training", mUserId, "lastVideo", mVideoId);
+ success = true;
+
+ } catch (Exception e) {
+ cLog.warn("Caught exception updating training record: " + e.getMessage(), e);
+ }
+ break;
+
+ default:
+ setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+ }
+
+ if (success) {
+ setStatus(Status.SUCCESS_NO_CONTENT);
+
+ } else {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ }
+
+ return null;
+ }
+
+ /**
+ * This method compiles the summary of the training completed.
+ */
+ private String buildSummary() {
+ StringBuilder sb = new StringBuilder("{ ");
+
+ // Last question answered
+ final String lastVideo = mDb.getKey("training", mUserId, "lastVideo");
+ if (lastVideo != null) {
+ sb.append("\"lastVideo\": \"" + lastVideo + "\", ");
+ }
+
+ // List of videos watched
+ sb.append("\"videos\": { ");
+ ColumnList<String> row = mDb.getRow("training", mUserId);
+ if (!row.isEmpty()) {
+ boolean first = true;
+ for (Column<String> c : row) {
+ if ("lastVideo".equals(c.getName())) {
+ continue;
+ }
+
+ if (first) {
+ sb.append("\"" + c.getName() + "\": ");
+ first = false;
+ } else {
+ sb.append(", \"" + c.getName() + "\": ");
+ }
+
+ sb.append(c.getStringValue());
+ }
+ }
+ sb.append(" }");
+
+
+ sb.append(" }");
+ return sb.toString();
+ }
+
+}
diff --git a/src/com/p4square/grow/backend/resources/TrainingResource.java b/src/com/p4square/grow/backend/resources/TrainingResource.java
new file mode 100644
index 0000000..85d08c1
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/TrainingResource.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnList;
+
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.grow.backend.GrowBackend;
+import com.p4square.grow.backend.db.CassandraDatabase;
+
+/**
+ * This resource returns a listing of training items for a particular level.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TrainingResource extends ServerResource {
+ private final static Logger cLog = Logger.getLogger(TrainingResource.class);
+
+ private GrowBackend mBackend;
+ private CassandraDatabase mDb;
+
+ private String mLevel;
+ private String mVideoId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mBackend = (GrowBackend) getApplication();
+ mDb = mBackend.getDatabase();
+
+ mLevel = getAttribute("level");
+ mVideoId = getAttribute("videoId");
+ }
+
+ /**
+ * Handle GET Requests.
+ */
+ @Override
+ protected Representation get() {
+ String result = null;
+
+ if (mLevel == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ if (mVideoId == null) {
+ // Get all videos
+ ColumnList<String> row = mDb.getRow("strings", "/training/" + mLevel);
+ if (!row.isEmpty()) {
+ StringBuilder sb = new StringBuilder("{ \"level\": \"" + mLevel + "\"");
+ sb.append(", \"videos\": [");
+ boolean first = true;
+ for (Column<String> c : row) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(c.getStringValue());
+ first = false;
+ }
+ sb.append("] }");
+ result = sb.toString();
+ }
+
+ } else {
+ // Get single video
+ result = mDb.getKey("strings", "/training/" + mLevel, mVideoId);
+ }
+
+ if (result == null) {
+ // 404
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ return new StringRepresentation(result);
+ }
+}