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); +    } +} | 
