diff options
Diffstat (limited to 'src/com/p4square/grow/backend')
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); + } +} |