diff options
| author | Jesse Morgan <jesse@jesterpm.net> | 2014-02-01 11:18:41 -0800 | 
|---|---|---|
| committer | Jesse Morgan <jesse@jesterpm.net> | 2014-02-01 11:18:41 -0800 | 
| commit | 710880b563734d71e9a29276bb259d53218ea67c (patch) | |
| tree | 5645601ccd5b886e4360e208e6c37ebf6456b739 /src/com | |
| parent | a7e5eb45f68c7c6862b3ad29361114059f5dae3f (diff) | |
Adding the feed backend support.
Diffstat (limited to 'src/com')
9 files changed, 554 insertions, 1 deletions
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; +}  | 
