summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2014-02-01 11:18:41 -0800
committerJesse Morgan <jesse@jesterpm.net>2014-02-01 11:18:41 -0800
commit710880b563734d71e9a29276bb259d53218ea67c (patch)
tree5645601ccd5b886e4360e208e6c37ebf6456b739
parenta7e5eb45f68c7c6862b3ad29361114059f5dae3f (diff)
Adding the feed backend support.
-rw-r--r--devfiles/scripts/cassandra-bootstrap.cql10
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java33
-rw-r--r--src/com/p4square/grow/backend/db/CassandraCollectionProvider.java101
-rw-r--r--src/com/p4square/grow/backend/feed/FeedDataProvider.java33
-rw-r--r--src/com/p4square/grow/backend/feed/ThreadResource.java102
-rw-r--r--src/com/p4square/grow/backend/feed/TopicResource.java88
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java1
-rw-r--r--src/com/p4square/grow/model/Message.java95
-rw-r--r--src/com/p4square/grow/model/MessageThread.java43
-rw-r--r--src/com/p4square/grow/provider/CollectionProvider.java59
-rw-r--r--src/templates/templates/training.ftl10
11 files changed, 572 insertions, 3 deletions
diff --git a/devfiles/scripts/cassandra-bootstrap.cql b/devfiles/scripts/cassandra-bootstrap.cql
index db2e949..9348fcc 100644
--- a/devfiles/scripts/cassandra-bootstrap.cql
+++ b/devfiles/scripts/cassandra-bootstrap.cql
@@ -25,3 +25,13 @@ create column family training
with key_validation_class = 'UTF8Type'
and comparator = 'UTF8Type'
and default_validation_class = 'UTF8Type';
+
+create column family feedthreads
+ with key_validation_class = 'UTF8Type'
+ and comparator = 'UTF8Type'
+ and default_validation_class = 'UTF8Type';
+
+create column family feedmessages
+ with key_validation_class = 'UTF8Type'
+ and comparator = 'UTF8Type'
+ and default_validation_class = 'UTF8Type';
diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java
index 195554e..f844feb 100644
--- a/src/com/p4square/grow/backend/GrowBackend.java
+++ b/src/com/p4square/grow/backend/GrowBackend.java
@@ -19,12 +19,16 @@ 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.backend.db.CassandraCollectionProvider;
import com.p4square.grow.backend.db.CassandraTrainingRecordProvider;
import com.p4square.grow.model.Question;
import com.p4square.grow.model.TrainingRecord;
import com.p4square.grow.model.Playlist;
+import com.p4square.grow.model.MessageThread;
+import com.p4square.grow.model.Message;
+import com.p4square.grow.provider.CollectionProvider;
import com.p4square.grow.provider.Provider;
import com.p4square.grow.provider.ProvidesQuestions;
import com.p4square.grow.provider.ProvidesTrainingRecords;
@@ -37,13 +41,17 @@ import com.p4square.grow.backend.resources.SurveyResultsResource;
import com.p4square.grow.backend.resources.TrainingRecordResource;
import com.p4square.grow.backend.resources.TrainingResource;
+import com.p4square.grow.backend.feed.FeedDataProvider;
+import com.p4square.grow.backend.feed.ThreadResource;
+import com.p4square.grow.backend.feed.TopicResource;
+
/**
* Main class for the backend application.
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
public class GrowBackend extends Application
- implements ProvidesQuestions, ProvidesTrainingRecords {
+ implements ProvidesQuestions, ProvidesTrainingRecords, FeedDataProvider {
private static final String DEFAULT_COLUMN = "value";
private final static Logger LOG = Logger.getLogger(GrowBackend.class);
@@ -54,6 +62,9 @@ public class GrowBackend extends Application
private final Provider<String, Question> mQuestionProvider;
private final CassandraTrainingRecordProvider mTrainingRecordProvider;
+ private final CollectionProvider<String, String, MessageThread> mFeedThreadProvider;
+ private final CollectionProvider<String, String, Message> mFeedMessageProvider;
+
public GrowBackend() {
this(new Config());
}
@@ -69,6 +80,11 @@ public class GrowBackend extends Application
}
};
+ mFeedThreadProvider = new CassandraCollectionProvider<MessageThread>(mDatabase,
+ "feedthreads", MessageThread.class);
+ mFeedMessageProvider = new CassandraCollectionProvider<Message>(mDatabase,
+ "feedmessages", Message.class);
+
mTrainingRecordProvider = new CassandraTrainingRecordProvider(mDatabase);
}
@@ -97,6 +113,11 @@ public class GrowBackend extends Application
// Misc.
router.attach("/banner", BannerResource.class);
+ // Feed
+ router.attach("/feed/{topic}", TopicResource.class);
+ router.attach("/feed/{topic}/{thread}", ThreadResource.class);
+ //router.attach("/feed/{topic/{thread}/{message}", MessageResource.class);
+
return router;
}
@@ -148,6 +169,16 @@ public class GrowBackend extends Application
return mTrainingRecordProvider.getDefaultPlaylist();
}
+ @Override
+ public CollectionProvider<String, String, MessageThread> getThreadProvider() {
+ return mFeedThreadProvider;
+ }
+
+ @Override
+ public CollectionProvider<String, String, Message> getMessageProvider() {
+ return mFeedMessageProvider;
+ }
+
/**
* Stand-alone main for testing.
*/
diff --git a/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java b/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java
new file mode 100644
index 0000000..cc11828
--- /dev/null
+++ b/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.db;
+
+import java.io.IOException;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnList;
+
+import com.p4square.grow.provider.CollectionProvider;
+import com.p4square.grow.provider.JsonEncodedProvider;
+
+/**
+ * CollectionProvider implementation backed by a Cassandra ColumnFamily.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CassandraCollectionProvider<V> implements CollectionProvider<String, String, V> {
+ private final CassandraDatabase mDb;
+ private final String mCF;
+ private final Class<V> mClazz;
+
+ public CassandraCollectionProvider(CassandraDatabase db, String columnFamily, Class<V> clazz) {
+ mDb = db;
+ mCF = columnFamily;
+ mClazz = clazz;
+ }
+
+ @Override
+ public V get(String collection, String key) throws IOException {
+ String blob = mDb.getKey(mCF, collection, key);
+ return decode(blob);
+ }
+
+ @Override
+ public Map<String, V> query(String collection) throws IOException {
+ return query(collection, -1);
+ }
+
+ @Override
+ public Map<String, V> query(String collection, int limit) throws IOException {
+ Map<String, V> result = new HashMap<>();
+
+ ColumnList<String> row = mDb.getRow(mCF, collection);
+ if (!row.isEmpty()) {
+ int count = 0;
+ for (Column<String> c : row) {
+ String key = c.getName();
+ String blob = c.getStringValue();
+ V obj = decode(blob);
+
+ result.put(key, obj);
+
+ if (limit >= 0 && ++count > limit) {
+ break; // Limit reached.
+ }
+ }
+ }
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public void put(String collection, String key, V obj) throws IOException {
+ String blob = encode(obj);
+ mDb.putKey(mCF, collection, key, blob);
+ }
+
+ /**
+ * 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 JsonEncodedProvider.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 = JsonEncodedProvider.MAPPER.readValue(blob, mClazz);
+ return obj;
+ }
+}
diff --git a/src/com/p4square/grow/backend/feed/FeedDataProvider.java b/src/com/p4square/grow/backend/feed/FeedDataProvider.java
new file mode 100644
index 0000000..41b2dfa
--- /dev/null
+++ b/src/com/p4square/grow/backend/feed/FeedDataProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.feed;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.p4square.grow.model.MessageThread;
+import com.p4square.grow.model.Message;
+import com.p4square.grow.provider.CollectionProvider;
+
+/**
+ * Implementing this interface indicates you can provide a data source for the Feed.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface FeedDataProvider {
+ public static final Collection<String> TOPICS = Collections.unmodifiableCollection(
+ Arrays.asList(new String[] { "seeker", "believer", "disciple", "teacher" }));
+
+ /**
+ * @return a CollectionProvider of Threads.
+ */
+ CollectionProvider<String, String, MessageThread> getThreadProvider();
+
+ /**
+ * @return a CollectionProvider of Messages.
+ */
+ CollectionProvider<String, String, Message> getMessageProvider();
+}
diff --git a/src/com/p4square/grow/backend/feed/ThreadResource.java b/src/com/p4square/grow/backend/feed/ThreadResource.java
new file mode 100644
index 0000000..32a2f64
--- /dev/null
+++ b/src/com/p4square/grow/backend/feed/ThreadResource.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.feed;
+
+import java.io.IOException;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+
+import org.restlet.ext.jackson.JacksonRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.grow.model.Message;
+
+/**
+ * ThreadResource manages the messages that make up a thread.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ThreadResource extends ServerResource {
+ private static final Logger LOG = Logger.getLogger(ThreadResource.class);
+
+ private FeedDataProvider mBackend;
+ private String mTopic;
+ private String mThreadId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mBackend = (FeedDataProvider) getApplication();
+ mTopic = getAttribute("topic");
+ mThreadId = getAttribute("thread");
+ }
+
+ /**
+ * GET a list of messages in a thread.
+ */
+ @Override
+ protected Representation get() {
+ // If the topic or threadId are missing, return a 404.
+ if (mTopic == null || mTopic.length() == 0 ||
+ mThreadId == null || mThreadId.length() == 0) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ // TODO: Support limit query parameter.
+
+ try {
+ String collectionKey = mTopic + "/" + mThreadId;
+ Map<String, Message> messages = mBackend.getMessageProvider().query(collectionKey);
+ return new JacksonRepresentation(messages.values());
+
+ } catch (IOException e) {
+ LOG.error("Unexpected exception: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+ /**
+ * POST a new thread to the topic.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ // If the topic and thread are not provided, respond with not allowed.
+ // TODO: Check if the thread exists.
+ if (mTopic == null || !mBackend.TOPICS.contains(mTopic) ||
+ mThreadId == null || mThreadId.length() == 0) {
+ setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+ return null;
+ }
+
+ try {
+ JacksonRepresentation<Message> jsonRep = new JacksonRepresentation<Message>(entity, Message.class);
+ Message message = jsonRep.getObject();
+
+ // Force the thread id and message to be what we expect.
+ message.setThreadId(mThreadId);
+ message.setId(String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString()));
+
+ String collectionKey = mTopic + "/" + mThreadId;
+ mBackend.getMessageProvider().put(collectionKey, message.getId(), message);
+
+ setLocationRef(mThreadId + "/" + message.getId());
+ return new JacksonRepresentation(message);
+
+ } catch (IOException e) {
+ LOG.error("Unexpected exception: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+}
diff --git a/src/com/p4square/grow/backend/feed/TopicResource.java b/src/com/p4square/grow/backend/feed/TopicResource.java
new file mode 100644
index 0000000..0904baa
--- /dev/null
+++ b/src/com/p4square/grow/backend/feed/TopicResource.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.feed;
+
+import java.io.IOException;
+
+import java.util.Map;
+
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+
+import org.restlet.ext.jackson.JacksonRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.grow.model.MessageThread;
+
+/**
+ * TopicResource manages the threads contained in a topic.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TopicResource extends ServerResource {
+ private static final Logger LOG = Logger.getLogger(TopicResource.class);
+
+ private FeedDataProvider mBackend;
+ private String mTopic;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mBackend = (FeedDataProvider) getApplication();
+ mTopic = getAttribute("topic");
+ }
+
+ /**
+ * GET a list of threads in the topic.
+ */
+ @Override
+ protected Representation get() {
+ // If no topic is provided, return a list of topics.
+ if (mTopic == null || mTopic.length() == 0) {
+ return new JacksonRepresentation(FeedDataProvider.TOPICS);
+ }
+
+ // TODO: Support limit query parameter.
+
+ try {
+ Map<String, MessageThread> threads = mBackend.getThreadProvider().query(mTopic);
+ return new JacksonRepresentation(threads.values());
+
+ } catch (IOException e) {
+ LOG.error("Unexpected exception: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+ /**
+ * POST a new thread to the topic.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ // If no topic is provided, respond with not allowed.
+ if (mTopic == null || !mBackend.TOPICS.contains(mTopic)) {
+ setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+ return null;
+ }
+
+ try {
+ MessageThread newThread = MessageThread.createNew();
+ mBackend.getThreadProvider().put(mTopic, newThread.getId(), newThread);
+
+ setStatus(Status.SUCCESS_NO_CONTENT);
+ setLocationRef(mTopic + "/" + newThread.getId());
+ return null;
+
+ } catch (IOException e) {
+ LOG.error("Unexpected exception: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+}
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
index adad68c..0f2b284 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -209,6 +209,7 @@ public class TrainingPageResource extends FreeMarkerPageResource {
root.put("overallProgress", overallProgress);
root.put("videos", videos);
root.put("allowUserToSkip", allowUserToSkip);
+ root.put("showfeed", getQueryValue("showfeed") != null);
return new TemplateRepresentation(mTrainingTemplate, root, MediaType.TEXT_HTML);
diff --git a/src/com/p4square/grow/model/Message.java b/src/com/p4square/grow/model/Message.java
new file mode 100644
index 0000000..6e07150
--- /dev/null
+++ b/src/com/p4square/grow/model/Message.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.Date;
+
+/**
+ * A feed message.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Message {
+ private String mThreadId;
+ private String mId;
+ private String mAuthor;
+ private Date mCreated;
+ private String mMessage;
+
+ /**
+ * @return The id of the thread that the message belongs to.
+ */
+ public String getThreadId() {
+ return mThreadId;
+ }
+
+ /**
+ * Set the id of the thread that the message belongs to.
+ * @param id The new thread id.
+ */
+ public void setThreadId(String id) {
+ mThreadId = id;
+ }
+
+ /**
+ * @return The id the message.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Set the id of the message.
+ * @param id The new message id.
+ */
+ public void setId(String id) {
+ mId = id;
+ }
+
+ /**
+ * @return The author of the message.
+ */
+ public String getAuthor() {
+ return mAuthor;
+ }
+
+ /**
+ * Set the author of the message.
+ * @param author The new author.
+ */
+ public void setAuthor(String author) {
+ mAuthor = author;
+ }
+
+ /**
+ * @return The Date the message was created.
+ */
+ public Date getCreated() {
+ return mCreated;
+ }
+
+ /**
+ * Set the Date the message was created.
+ * @param date The new creation date.
+ */
+ public void setCreated(Date date) {
+ mCreated = date;
+ }
+
+ /**
+ * @return The message text.
+ */
+ public String getMessage() {
+ return mMessage;
+ }
+
+ /**
+ * Set the message text.
+ * @param text The message text.
+ */
+ public void setMessage(String text) {
+ mMessage = text;
+ }
+}
diff --git a/src/com/p4square/grow/model/MessageThread.java b/src/com/p4square/grow/model/MessageThread.java
new file mode 100644
index 0000000..f93ec13
--- /dev/null
+++ b/src/com/p4square/grow/model/MessageThread.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.UUID;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class MessageThread {
+ private String mId;
+
+ /**
+ * Create a new thread with a probably unique id.
+ *
+ * @return the new thread.
+ */
+ public static MessageThread createNew() {
+ MessageThread t = new MessageThread();
+ t.setId(String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString()));
+
+ return t;
+ }
+
+ /**
+ * @return The id the message.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Set the id of the message.
+ * @param id The new message id.
+ */
+ public void setId(String id) {
+ mId = id;
+ }
+
+}
diff --git a/src/com/p4square/grow/provider/CollectionProvider.java b/src/com/p4square/grow/provider/CollectionProvider.java
new file mode 100644
index 0000000..e4e9040
--- /dev/null
+++ b/src/com/p4square/grow/provider/CollectionProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * ListProvider is the logical extension of Provider for dealing with lists of
+ * items.
+ *
+ * @param C The type of the collection key.
+ * @param K The type of the item key.
+ * @param V The type of the value.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface CollectionProvider<C, K, V> {
+ /**
+ * Retrieve a specific object from the collection.
+ *
+ * @param collection The collection key.
+ * @param key The key for the object in the collection.
+ * @return The object or null if not found.
+ */
+ V get(C collection, K key) throws IOException;
+
+ /**
+ * Retrieve a collection.
+ *
+ * The returned map will never be null.
+ *
+ * @param collection The collection key.
+ * @return A Map of keys to values.
+ */
+ Map<K, V> query(C collection) throws IOException;
+
+ /**
+ * Retrieve a portion of a collection.
+ *
+ * The returned map will never be null.
+ *
+ * @param collection The collection key.
+ * @param limit Max number of items to return.
+ * @return A Map of keys to values.
+ */
+ Map<K, V> query(C collection, int limit) throws IOException;
+
+ /**
+ * Persist the object with the given key.
+ *
+ * @param collection The collection key.
+ * @param key The key for the object in the collection.
+ * @param obj The object to persist.
+ */
+ void put(C collection, K key, V obj) throws IOException;
+}
diff --git a/src/templates/templates/training.ftl b/src/templates/templates/training.ftl
index 3f5331e..7989d15 100644
--- a/src/templates/templates/training.ftl
+++ b/src/templates/templates/training.ftl
@@ -32,10 +32,12 @@
<div class="progresslabel" style="left:${chapterProgress}%">${chapterProgress}%</div>
</div>
- <div id="videos">
+ <#assign sidebar=showfeed>
+
+ <div id="videos" <#if sidebar>style="width: 70%"</#if>>
<#assign allowed = true>
<#list videos as video>
- <article>
+ <article <#if sidebar>style="margin-right: 30px"</#if>>
<div class="image <#if video.completed>completed</#if> <#if allowed>allowed</#if>" id="${video.id}"><a href="#" onclick="playVideo('${video.id}'); return false"><img src="${video.image!staticRoot+"/images/videoimage.jpg"}" alt="${video.title}" /></a></div>
<h2><#if video.number != "0">${video.number}. </#if>${video.title}</h2>
<span class="duration"><@hms seconds=video.length /></span>
@@ -48,6 +50,10 @@
</article>
</#list>
</div>
+
+ <#if showfeed>
+ <#include "/templates/communityfeed.ftl">
+ </#if>
</div>
<div id="videoplayer">