diff options
Diffstat (limited to 'src/com/p4square')
20 files changed, 648 insertions, 329 deletions
| diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java index 45e0fa2..195554e 100644 --- a/src/com/p4square/grow/backend/GrowBackend.java +++ b/src/com/p4square/grow/backend/GrowBackend.java @@ -4,6 +4,8 @@  package com.p4square.grow.backend; +import java.io.IOException; +  import org.apache.log4j.Logger;  import org.restlet.Application; @@ -17,10 +19,15 @@ 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.CassandraTrainingRecordProvider;  import com.p4square.grow.model.Question; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.Playlist;  import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesQuestions; +import com.p4square.grow.provider.ProvidesTrainingRecords;  import com.p4square.grow.provider.QuestionProvider;  import com.p4square.grow.backend.resources.AccountResource; @@ -35,7 +42,8 @@ import com.p4square.grow.backend.resources.TrainingResource;   *   * @author Jesse Morgan <jesse@jesterpm.net>   */ -public class GrowBackend extends Application { +public class GrowBackend extends Application +        implements ProvidesQuestions, ProvidesTrainingRecords {      private static final String DEFAULT_COLUMN = "value";      private final static Logger LOG = Logger.getLogger(GrowBackend.class); @@ -44,6 +52,7 @@ public class GrowBackend extends Application {      private final CassandraDatabase mDatabase;      private final Provider<String, Question> mQuestionProvider; +    private final CassandraTrainingRecordProvider mTrainingRecordProvider;      public GrowBackend() {          this(new Config()); @@ -53,12 +62,14 @@ public class GrowBackend extends Application {          mConfig = config;          mDatabase = new CassandraDatabase(); -        mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, "strings", Question.class)) { +        mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, Question.class)) {              @Override              public CassandraKey makeKey(String questionId) { -                return new CassandraKey("/questions/" + questionId, DEFAULT_COLUMN); +                return new CassandraKey("strings", "/questions/" + questionId, DEFAULT_COLUMN);              }          }; + +        mTrainingRecordProvider = new CassandraTrainingRecordProvider(mDatabase);      }      @Override @@ -120,10 +131,23 @@ public class GrowBackend extends Application {          return mDatabase;      } +    @Override      public Provider<String, Question> getQuestionProvider() {          return mQuestionProvider;      } +    @Override +    public Provider<String, TrainingRecord> getTrainingRecordProvider() { +        return mTrainingRecordProvider; +    } + +    /** +     * @return the Default Playlist. +     */ +    public Playlist getDefaultPlaylist() throws IOException { +        return mTrainingRecordProvider.getDefaultPlaylist(); +    } +      /**       * Stand-alone main for testing.       */ diff --git a/src/com/p4square/grow/backend/db/CassandraKey.java b/src/com/p4square/grow/backend/db/CassandraKey.java index 8e23087..853fe96 100644 --- a/src/com/p4square/grow/backend/db/CassandraKey.java +++ b/src/com/p4square/grow/backend/db/CassandraKey.java @@ -10,14 +10,20 @@ package com.p4square.grow.backend.db;   * @author Jesse Morgan <jesse@jesterpm.net>   */  public class CassandraKey { +    private final String mColumnFamily;      private final String mId;      private final String mColumn; -    public CassandraKey(String id, String column) { +    public CassandraKey(String columnFamily, String id, String column) { +        mColumnFamily = columnFamily;          mId = id;          mColumn = column;      } +    public String getColumnFamily() { +        return mColumnFamily; +    } +      public String getId() {          return mId;      } diff --git a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java index fb6e34e..c1f6e6d 100644 --- a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java +++ b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java @@ -6,10 +6,6 @@ package com.p4square.grow.backend.db;  import java.io.IOException; -import org.codehaus.jackson.map.DeserializationConfig; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.SerializationConfig; -  import com.p4square.grow.provider.JsonEncodedProvider;  /** @@ -19,24 +15,22 @@ import com.p4square.grow.provider.JsonEncodedProvider;   */  public class CassandraProviderImpl<V> extends JsonEncodedProvider<CassandraKey, V> {      private final CassandraDatabase mDb; -    private final String mColumnFamily; -    public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class<V> clazz) { +    public CassandraProviderImpl(CassandraDatabase db, Class<V> clazz) {          super(clazz);          mDb = db; -        mColumnFamily = columnFamily;      }      @Override      public V get(CassandraKey key) throws IOException { -        String blob = mDb.getKey(mColumnFamily, key.getId(), key.getColumn()); +        String blob = mDb.getKey(key.getColumnFamily(), key.getId(), key.getColumn());          return decode(blob);      }      @Override      public void put(CassandraKey key, V obj) throws IOException {          String blob = encode(obj); -        mDb.putKey(mColumnFamily, key.getId(), key.getColumn(), blob); +        mDb.putKey(key.getColumnFamily(), key.getId(), key.getColumn(), blob);      }  } diff --git a/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java b/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java new file mode 100644 index 0000000..4face52 --- /dev/null +++ b/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.TrainingRecord; + +import com.p4square.grow.provider.JsonEncodedProvider; +import com.p4square.grow.provider.Provider; + +/** + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class CassandraTrainingRecordProvider implements Provider<String, TrainingRecord> { +    private static final CassandraKey DEFAULT_PLAYLIST_KEY = new CassandraKey("strings", "defaultPlaylist", "value"); + +    private static final String COLUMN_FAMILY = "training"; +    private static final String PLAYLIST_KEY = "playlist"; +    private static final String LAST_VIDEO_KEY = "lastVideo"; + +    private final CassandraDatabase mDb; +    private final Provider<CassandraKey, Playlist> mPlaylistProvider; + +    public CassandraTrainingRecordProvider(CassandraDatabase db) { +        mDb = db; +        mPlaylistProvider = new CassandraProviderImpl<>(db, Playlist.class); +    } + +    @Override +    public TrainingRecord get(String userid) throws IOException { +        Playlist playlist = mPlaylistProvider.get(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY)); + +        if (playlist == null) { +            // We consider no playlist to mean no record whatsoever. +            return null; +        } + +        TrainingRecord r = new TrainingRecord(); +        r.setPlaylist(playlist); +        r.setLastVideo(mDb.getKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY)); + +        return r; +    } + +    @Override +    public void put(String userid, TrainingRecord record) throws IOException { +        String lastVideo = record.getLastVideo(); +        Playlist playlist = record.getPlaylist(); + +        mDb.putKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY, lastVideo); +        mPlaylistProvider.put(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY), playlist); +    } + +    /** +     * @return the default playlist stored in the database. +     */ +    public Playlist getDefaultPlaylist() throws IOException { +        Playlist playlist = mPlaylistProvider.get(DEFAULT_PLAYLIST_KEY); + +        if (playlist == null) { +            playlist = new Playlist(); +        } + +        return playlist; +    } +} diff --git a/src/com/p4square/grow/backend/resources/Playlist.java b/src/com/p4square/grow/backend/resources/Playlist.java deleted file mode 100644 index f3d2f08..0000000 --- a/src/com/p4square/grow/backend/resources/Playlist.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; - -import java.util.HashMap; -import java.util.Map; - -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.type.TypeReference; - -import com.p4square.grow.backend.db.CassandraDatabase; - -/** - *  - * @author Jesse Morgan <jesse@jesterpm.net> - */ -class Playlist { -    private static final ObjectMapper MAPPER = new ObjectMapper(); - -    /** -     * Load a Playlist from the database. -     */ -    public static Playlist load(CassandraDatabase db, String userId) throws IOException { -        String playlistString = db.getKey("training", userId, "playlist"); - -        if (playlistString == null) { -            return null; -        } - -        Map<String, Map<String, VideoRecord>> playlist = -            MAPPER.readValue(playlistString, new TypeReference<Map<String, Map<String, VideoRecord>>>() { }); - -        return new Playlist(playlist); - -    } - -    /** -     * Persist the Playlist for the given user. -     * @return The String serialization of the playlist. -     */ -    public static String save(CassandraDatabase db, String userId, Playlist playlist) throws IOException { -        String playlistString = MAPPER.writeValueAsString(playlist.mPlaylist); -        db.putKey("training", userId, "playlist", playlistString); -        return playlistString; -    } - - -    private Map<String, Map<String, VideoRecord>> mPlaylist; - -    /** -     * Construct an empty playlist. -     */ -    public Playlist() { -        mPlaylist = new HashMap<String, Map<String, VideoRecord>>(); -    } - -    /** -     * Constructor for database initialization. -     */ -    private Playlist(Map<String, Map<String, VideoRecord>> playlist) { -        mPlaylist = playlist; -    } - -    public VideoRecord find(String videoId) { -        for (Map<String, VideoRecord> chapter : mPlaylist.values()) { -            VideoRecord r = chapter.get(videoId); - -            if (r != null) { -                return r; -            } -        } - -        return null; -    } - -    /** -     * Add a video to the playlist. -     */ -    public VideoRecord add(String chapter, String videoId) { -        Map<String, VideoRecord> chapterMap = mPlaylist.get(chapter); - -        if (chapterMap == null) { -            chapterMap = new HashMap<String, VideoRecord>(); -            mPlaylist.put(chapter, chapterMap); -        } - -        VideoRecord r = new VideoRecord(); -        chapterMap.put(videoId, r); -        return r; -    } - -    /** -     * @return The last chapter to be completed. -     */ -    public Map<String, Boolean> getChapterStatuses() { -        Map<String, Boolean> completed = new HashMap<String, Boolean>(); - -        for (String chapter : mPlaylist.keySet()) { -            completed.put(chapter, isChapterComplete(chapter)); -        } - -        return completed; -    } - -    public boolean isChapterComplete(String chapterId) { -        boolean complete = true; - -        Map<String, VideoRecord> chapter = mPlaylist.get(chapterId); -        if (chapter != null) { -            for (VideoRecord r : chapter.values()) { -                if (r.getRequired() && !r.getComplete()) { -                    return false; -                } -            } -        } - -        return complete; -    } - -    @Override -    public String toString() { -        try { -            return MAPPER.writeValueAsString(mPlaylist); - -        } catch (IOException e) { -            return super.toString(); -        } -    } - -} diff --git a/src/com/p4square/grow/backend/resources/SurveyResource.java b/src/com/p4square/grow/backend/resources/SurveyResource.java index 83c4cad..497978f 100644 --- a/src/com/p4square/grow/backend/resources/SurveyResource.java +++ b/src/com/p4square/grow/backend/resources/SurveyResource.java @@ -9,7 +9,7 @@ import java.io.IOException;  import java.util.Map;  import java.util.HashMap; -import org.codehaus.jackson.map.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper;  import org.restlet.data.MediaType;  import org.restlet.data.Status; diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java index 91d4d0f..e87126d 100644 --- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java +++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java @@ -10,7 +10,7 @@ import java.util.HashMap;  import com.netflix.astyanax.model.Column;  import com.netflix.astyanax.model.ColumnList; -import org.codehaus.jackson.map.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper;  import org.restlet.data.MediaType;  import org.restlet.data.Status; diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java index 6de9507..e42456e 100644 --- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java +++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java @@ -14,7 +14,7 @@ import java.util.HashMap;  import com.netflix.astyanax.model.Column;  import com.netflix.astyanax.model.ColumnList; -import org.codehaus.jackson.map.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper;  import org.restlet.data.MediaType;  import org.restlet.data.Status; @@ -22,11 +22,21 @@ import org.restlet.resource.ServerResource;  import org.restlet.representation.Representation;  import org.restlet.representation.StringRepresentation; +import org.restlet.ext.jackson.JacksonRepresentation; +  import org.apache.log4j.Logger;  import com.p4square.grow.backend.GrowBackend;  import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.VideoRecord; +import com.p4square.grow.model.TrainingRecord; + +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesTrainingRecords; +import com.p4square.grow.provider.JsonEncodedProvider; +  import com.p4square.grow.model.Score;  /** @@ -43,23 +53,41 @@ public class TrainingRecordResource extends ServerResource {          SUMMARY, VIDEO      } -    private GrowBackend mBackend;      private CassandraDatabase mDb; +    private Provider<String, TrainingRecord> mTrainingRecordProvider;      private RequestType mRequestType;      private String mUserId;      private String mVideoId; +    private TrainingRecord mRecord;      @Override      public void doInit() {          super.doInit(); -        mBackend = (GrowBackend) getApplication(); -        mDb = mBackend.getDatabase(); +        mDb = ((GrowBackend) getApplication()).getDatabase(); +        mTrainingRecordProvider = ((ProvidesTrainingRecords) getApplication()).getTrainingRecordProvider();          mUserId = getAttribute("userId");          mVideoId = getAttribute("videoId"); +        try { +            Playlist defaultPlaylist = ((GrowBackend) getApplication()).getDefaultPlaylist(); + +            mRecord = mTrainingRecordProvider.get(mUserId); +            if (mRecord == null) { +                mRecord = new TrainingRecord(); +                mRecord.setPlaylist(defaultPlaylist); +            } else { +                // Merge the playlist with the most recent version. +                mRecord.getPlaylist().merge(defaultPlaylist); +            } + +        } catch (IOException e) { +            LOG.error("IOException loading TrainingRecord: " + e.getMessage(), e); +            mRecord = null; +        } +          mRequestType = RequestType.SUMMARY;          if (mVideoId != null) {              mRequestType = RequestType.VIDEO; @@ -71,24 +99,35 @@ public class TrainingRecordResource extends ServerResource {       */      @Override      protected Representation get() { -        String result = null; +        JacksonRepresentation<?> rep = null; + +        if (mRecord == null) { +            setStatus(Status.SERVER_ERROR_INTERNAL); +            return null; +        }          switch (mRequestType) {              case VIDEO: -                result = mDb.getKey("training", mUserId, mVideoId); +                VideoRecord video = mRecord.getPlaylist().find(mVideoId); +                if (video == null) { +                    break; // Fall through and return 404 +                } +                rep = new JacksonRepresentation<VideoRecord>(video);                  break;              case SUMMARY: -                result = buildSummary(); +                rep = new JacksonRepresentation<TrainingRecord>(mRecord);                  break;          } -        if (result == null) { +        if (rep == null) {              setStatus(Status.CLIENT_ERROR_NOT_FOUND);              return null; -        } -        return new StringRepresentation(result); +        } else { +            rep.setObjectMapper(JsonEncodedProvider.MAPPER); +            return rep; +        }      }      /** @@ -96,27 +135,37 @@ public class TrainingRecordResource extends ServerResource {       */      @Override      protected Representation put(Representation entity) { -        boolean success = false; +        if (mRecord == null) { +            setStatus(Status.SERVER_ERROR_INTERNAL); +            return null; +        }          switch (mRequestType) {              case VIDEO:                  try { -                    mDb.putKey("training", mUserId, mVideoId, entity.getText()); -                    mDb.putKey("training", mUserId, "lastVideo", mVideoId); - -                    Playlist playlist = Playlist.load(mDb, mUserId); -                    if (playlist != null) { -                        VideoRecord r = playlist.find(mVideoId); -                        if (r != null && !r.getComplete()) { -                            r.complete(); -                            Playlist.save(mDb, mUserId, playlist); -                        } +                    JacksonRepresentation<VideoRecord> representation =  +                        new JacksonRepresentation<>(entity, VideoRecord.class); +                    representation.setObjectMapper(JsonEncodedProvider.MAPPER); +                    VideoRecord update = representation.getObject(); +                    VideoRecord video = mRecord.getPlaylist().find(mVideoId); + +                    if (video == null) { +                        // TODO: Video isn't on their playlist... +                        LOG.warn("Skipping video completion for video missing from playlist."); + +                    } else if (update.getComplete() && !video.getComplete()) { +                        // Video was newly completed +                        video.complete(); +                        mRecord.setLastVideo(mVideoId); + +                        mTrainingRecordProvider.put(mUserId, mRecord);                      } -                    success = true; +                    setStatus(Status.SUCCESS_NO_CONTENT);                  } catch (Exception e) {                      LOG.warn("Caught exception updating training record: " + e.getMessage(), e); +                    setStatus(Status.SERVER_ERROR_INTERNAL);                  }                  break; @@ -124,116 +173,7 @@ public class TrainingRecordResource extends ServerResource {                  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 watch video -        final String lastVideo = mDb.getKey("training", mUserId, "lastVideo"); -        if (lastVideo != null) { -            sb.append("\"lastVideo\": \"" + lastVideo + "\", "); -        } - -        // Get the user's video history -        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()) || -                    "playlist".equals(c.getName())) { -                    continue; -                } - -                if (first) { -                    sb.append("\"" + c.getName() + "\": "); -                    first = false; -                } else { -                    sb.append(", \"" + c.getName() + "\": "); -                } - -                sb.append(c.getStringValue()); -            } -        } -        sb.append(" }"); - -        // Get the user's playlist -        try { -            Playlist playlist = Playlist.load(mDb, mUserId); -            if (playlist == null) { -                playlist = createInitialPlaylist(); -            } - -            sb.append(", \"playlist\": "); -            sb.append(playlist.toString()); - -            // Last Completed Section -            Map<String, Boolean> chapters = playlist.getChapterStatuses(); -            String chaptersString = MAPPER.writeValueAsString(chapters); -            sb.append(", \"chapters\":"); -            sb.append(chaptersString); - - -        } catch (IOException e) { -            LOG.warn("IOException loading playlist for user " + mUserId, e); -        } - - -        sb.append(" }"); -        return sb.toString(); -    } - -    /** -     * Create the user's initial playlist. -     * -     * @return Returns the String representation of the initial playlist. -     */ -    private Playlist createInitialPlaylist() throws IOException { -        Playlist playlist = new Playlist(); - -        // Get assessment score -        String summaryString = mDb.getKey("assessments", mUserId, "summary"); -        if (summaryString == null) { -            return null; -        } -        Map<?,?> summary = MAPPER.readValue(summaryString, Map.class); -        double score = (Double) summary.get("score"); - -        // Get videos for each section and build playlist -        for (String chapter : CHAPTERS) { -            boolean required; - -            if ("introduction".equals(chapter)) { -                // Introduction chapter is always required -                required = true; -            } else { -                // Chapter required if the floor of the score is <= the chapter's numeric value. -                required = score < Score.numericScore(chapter) + 1; -            } - -            ColumnList<String> row = mDb.getRow("strings", "/training/" + chapter); -            if (!row.isEmpty()) { -                for (Column<String> c : row) { -                    VideoRecord r = playlist.add(chapter, c.getName()); -                    r.setRequired(required); -                } -            } -        } - -        Playlist.save(mDb, mUserId, playlist); - -        return playlist; -    }  } diff --git a/src/com/p4square/grow/frontend/ChapterCompletePage.java b/src/com/p4square/grow/frontend/ChapterCompletePage.java index 2f981ae..b0bfdd7 100644 --- a/src/com/p4square/grow/frontend/ChapterCompletePage.java +++ b/src/com/p4square/grow/frontend/ChapterCompletePage.java @@ -23,6 +23,9 @@ import com.p4square.fmfacade.json.JsonResponse;  import com.p4square.fmfacade.json.ClientException;  import com.p4square.grow.config.Config; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.provider.TrainingRecordProvider; +import com.p4square.grow.provider.Provider;  /**   * This resource displays the transitional page between chapters. @@ -35,6 +38,7 @@ public class ChapterCompletePage extends FreeMarkerPageResource {      private GrowFrontend mGrowFrontend;      private Config mConfig;      private JsonRequestClient mJsonClient; +    private Provider<String, TrainingRecord> mTrainingRecordProvider;      private String mUserId;      private String mChapter; @@ -47,6 +51,12 @@ public class ChapterCompletePage extends FreeMarkerPageResource {          mConfig = mGrowFrontend.getConfig();          mJsonClient = new JsonRequestClient(getContext().getClientDispatcher()); +        mTrainingRecordProvider = new TrainingRecordProvider<String>(new JsonRequestProvider<TrainingRecord>(getContext().getClientDispatcher(), TrainingRecord.class)) { +            @Override +            public String makeKey(String userid) { +                return getBackendEndpoint() + "/accounts/" + userid + "/training"; +            } +        };          mUserId = getRequest().getClientInfo().getUser().getIdentifier(); @@ -69,17 +79,15 @@ public class ChapterCompletePage extends FreeMarkerPageResource {              Map<String, Object> root = getRootObject();              // Get the training summary -            Map<String, Object> trainingRecord = null; -            Map<String, Boolean> chapters = null; -            { -                JsonResponse response = backendGet("/accounts/" + mUserId + "/training"); -                if (response.getStatus().isSuccess()) { -                    trainingRecord = response.getMap(); -                    chapters = (Map<String, Boolean>) trainingRecord.get("chapters"); -                } +            TrainingRecord trainingRecord = mTrainingRecordProvider.get(mUserId); +            if (trainingRecord == null) { +                // Wait. What? Everyone has a training record... +                setStatus(Status.SERVER_ERROR_INTERNAL); +                return new ErrorPage("Could not retrieve your training record.");              }              // Verify they completed the chapter. +            Map<String, Boolean> chapters = trainingRecord.getPlaylist().getChapterStatuses();              Boolean completed = chapters.get(mChapter);              if (completed == null || !completed) {                  // Redirect back to training page... diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java index b27d86e..bf08a1a 100644 --- a/src/com/p4square/grow/frontend/TrainingPageResource.java +++ b/src/com/p4square/grow/frontend/TrainingPageResource.java @@ -27,13 +27,18 @@ import com.p4square.fmfacade.json.JsonResponse;  import com.p4square.fmfacade.FreeMarkerPageResource;  import com.p4square.grow.config.Config; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.VideoRecord; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.provider.TrainingRecordProvider; +import com.p4square.grow.provider.Provider;  /**   * TrainingPageResource handles rendering the training page.   *   * This resource expects the user to be authenticated and the ClientInfo User object   * to be populated. - *  + *   * @author Jesse Morgan <jesse@jesterpm.net>   */  public class TrainingPageResource extends FreeMarkerPageResource { @@ -43,6 +48,8 @@ public class TrainingPageResource extends FreeMarkerPageResource {      private Template mTrainingTemplate;      private JsonRequestClient mJsonClient; +    private Provider<String, TrainingRecord> mTrainingRecordProvider; +      // Fields pertaining to this request.      private String mChapter;      private String mUserId; @@ -60,6 +67,12 @@ public class TrainingPageResource extends FreeMarkerPageResource {          }          mJsonClient = new JsonRequestClient(getContext().getClientDispatcher()); +        mTrainingRecordProvider = new TrainingRecordProvider<String>(new JsonRequestProvider<TrainingRecord>(getContext().getClientDispatcher(), TrainingRecord.class)) { +            @Override +            public String makeKey(String userid) { +                return getBackendEndpoint() + "/accounts/" + userid + "/training"; +            } +        };          mChapter = getAttribute("chapter");          mUserId = getRequest().getClientInfo().getUser().getIdentifier(); @@ -72,18 +85,15 @@ public class TrainingPageResource extends FreeMarkerPageResource {      protected Representation get() {          try {              // Get the training summary -            Map<String, Object> trainingRecord = null; -            Map<String, Object> completedVideos = new HashMap<String, Object>(); -            Map<String, Boolean> chapters = null; -            { -                JsonResponse response = backendGet("/accounts/" + mUserId + "/training"); -                if (response.getStatus().isSuccess()) { -                    trainingRecord = response.getMap(); -                    completedVideos = (Map<String, Object>) trainingRecord.get("videos"); -                    chapters = (Map<String, Boolean>) trainingRecord.get("chapters"); -                } +            TrainingRecord trainingRecord = mTrainingRecordProvider.get(mUserId); +            if (trainingRecord == null) { +                setStatus(Status.SERVER_ERROR_INTERNAL); +                return new ErrorPage("Could not retrieve TrainingRecord.");              } +            Playlist playlist = trainingRecord.getPlaylist(); +            Map<String, Boolean> chapters = playlist.getChapterStatuses(); +              // Get the current chapter (the lowest, incomplete chapter)              if (mChapter == null) {                  int min = Integer.MAX_VALUE; @@ -120,7 +130,13 @@ public class TrainingPageResource extends FreeMarkerPageResource {              // Mark the completed videos as completed              int chapterProgress = 0;              for (Map<String, Object> video : videos) { -                boolean completed = (null != completedVideos.get(video.get("id"))); +                boolean completed = false; +                VideoRecord record = playlist.find((String) video.get("id")); +                cLog.info("VideoId: " + video.get("id")); +                if (record != null) { +                    cLog.info("VideoRecord: " + record.getComplete()); +                    completed = record.getComplete(); +                }                  video.put("completed", completed);                  if (completed) { @@ -133,7 +149,6 @@ public class TrainingPageResource extends FreeMarkerPageResource {              root.put("chapter", mChapter);              root.put("chapterProgress", chapterProgress);              root.put("videos", videos); -            root.put("completedVideos", completedVideos);              return new TemplateRepresentation(mTrainingTemplate, root, MediaType.TEXT_HTML); @@ -166,18 +181,6 @@ public class TrainingPageResource extends FreeMarkerPageResource {          return response;      } -    private JsonResponse backendPut(final String uri, final Map data) { -        cLog.debug("Sending backend PUT " + uri); - -        final JsonResponse response = mJsonClient.put(getBackendEndpoint() + uri, data); -        final Status status = response.getStatus(); -        if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) { -            cLog.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString()); -        } - -        return response; -    } -      int chapterIndex(String chapter) {          if ("teacher".equals(chapter)) {              return 4; diff --git a/src/com/p4square/grow/frontend/VideosResource.java b/src/com/p4square/grow/frontend/VideosResource.java index caf8dc1..2099a77 100644 --- a/src/com/p4square/grow/frontend/VideosResource.java +++ b/src/com/p4square/grow/frontend/VideosResource.java @@ -83,7 +83,7 @@ public class VideosResource extends ServerResource {      @Override      protected Representation post(Representation entity) {          Map<String, Object> data = new HashMap<String, Object>(); -        data.put("completed", "t"); +        data.put("complete", "true");          JsonResponse response = backendPut("/accounts/" + mUserId + "/training/videos/" + mVideoId, data);          if (!response.getStatus().isSuccess()) { diff --git a/src/com/p4square/grow/model/Chapter.java b/src/com/p4square/grow/model/Chapter.java new file mode 100644 index 0000000..4d59983 --- /dev/null +++ b/src/com/p4square/grow/model/Chapter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Chapter is a list of VideoRecords in a Playlist. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class Chapter implements Cloneable { +    private Map<String, VideoRecord> mVideos; + +    public Chapter() { +        mVideos = new HashMap<String, VideoRecord>(); +    } + +    /** +     * @return The VideoRecord for videoid or null if videoid is not in the chapter. +     */ +    public VideoRecord getVideoRecord(String videoid) { +        return mVideos.get(videoid); +    } + +    /** +     * @return A map of video ids to VideoRecords. +     */ +    @JsonAnyGetter +    public Map<String, VideoRecord> getVideos() { +        return mVideos; +    } + +    /** +     * Set the VideoRecord for a video id. +     * @param videoId the video id. +     * @param video the VideoRecord. +     */ +    @JsonAnySetter +    public void setVideoRecord(String videoId, VideoRecord video) { +        mVideos.put(videoId, video); +    } + +    /** +     * @return true if every required video has been completed. +     */ +    @JsonIgnore +    public boolean isComplete() { +        boolean complete = true; + +        for (VideoRecord r : mVideos.values()) { +            if (r.getRequired() && !r.getComplete()) { +                return false; +            } +        } + +        return complete; +    } + +    /** +     * Deeply clone a chapter. +     * +     * @return a new Chapter object identical but independent of this one. +     */ +    public Chapter clone() throws CloneNotSupportedException { +        Chapter c = new Chapter(); +        for (Map.Entry<String, VideoRecord> videoEntry : mVideos.entrySet()) { +            c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone()); +        } +        return c; +    } +} diff --git a/src/com/p4square/grow/model/Playlist.java b/src/com/p4square/grow/model/Playlist.java new file mode 100644 index 0000000..79ce68e --- /dev/null +++ b/src/com/p4square/grow/model/Playlist.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Representation of a user's playlist. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class Playlist { +    /** +     * Map of Chapter ID to map of Video ID to VideoRecord. +     */ +    private Map<String, Chapter> mPlaylist; + +    private Date mLastUpdated; + +    /** +     * Construct an empty playlist. +     */ +    public Playlist() { +        mPlaylist = new HashMap<String, Chapter>(); +        mLastUpdated = new Date(0); // Default to a prehistoric date if we don't have one. +    } + +    /** +     * Find the VideoRecord for a video id. +     */ +    public VideoRecord find(String videoId) { +        for (Chapter chapter : mPlaylist.values()) { +            VideoRecord r = chapter.getVideoRecord(videoId); + +            if (r != null) { +                return r; +            } +        } + +        return null; +    } + +    /** +     * @return The last modified date of the source playlist. +     */ +    public Date getLastUpdated() { +        return mLastUpdated; +    } + +    /** +     * Set the last updated date. +     * @param date the new last updated date. +     */ +    public void setLastUpdated(Date date) { +        mLastUpdated = date; +    } + +    /** +     * Add a video to the playlist. +     */ +    public VideoRecord add(String chapterId, String videoId) { +        Chapter chapter = mPlaylist.get(chapterId); + +        if (chapter == null) { +            chapter = new Chapter(); +            mPlaylist.put(chapterId, chapter); +        } + +        VideoRecord r = new VideoRecord(); +        chapter.setVideoRecord(videoId, r); +        return r; +    } + +    /** +     * Add a Chapter to the Playlist. +     * @param chapterId The name of the chapter. +     * @param chapter The Chapter object to add. +     */ +    @JsonAnySetter +    public void addChapter(String chapterId, Chapter chapter) { +        mPlaylist.put(chapterId, chapter); +    } + +    /** +     * @return a map of chapter id to chapter. +     */ +    @JsonAnyGetter +    public Map<String, Chapter> getChaptersMap() { +        return mPlaylist; +    } + +    /** +     * @return The last chapter to be completed. +     */ +    @JsonIgnore +    public Map<String, Boolean> getChapterStatuses() { +        Map<String, Boolean> completed = new HashMap<String, Boolean>(); + +        for (Map.Entry<String, Chapter> entry : mPlaylist.entrySet()) { +            completed.put(entry.getKey(), entry.getValue().isComplete()); +        } + +        return completed; +    } + +    /** +     * @return true if all required videos in the chapter have been watched. +     */ +    public boolean isChapterComplete(String chapterId) { +        Chapter chapter = mPlaylist.get(chapterId); +        if (chapter != null) { +            return chapter.isComplete(); +        } + +        return false; +    } + +    /** +     * Merge a playlist into this playlist. +     * +     * Merge is accomplished by adding all missing Chapters and VideoRecords to +     * this playlist. +     */ +    public void merge(Playlist source) { +        if (source.getLastUpdated().before(mLastUpdated)) { +            // Already up to date. +            return; +        } + +        for (Map.Entry<String, Chapter> entry : source.getChaptersMap().entrySet()) { +            String chapterName = entry.getKey(); +            Chapter theirChapter = entry.getValue(); +            Chapter myChapter = mPlaylist.get(entry.getKey()); + +            if (myChapter == null) { +                // Add entire chapter +                try { +                    mPlaylist.put(chapterName, theirChapter.clone()); +                } catch (CloneNotSupportedException e) { +                    throw new RuntimeException(e); // Unexpected... +                } + +            } else { +                // Check chapter for missing videos +                for (Map.Entry<String, VideoRecord> videoEntry : theirChapter.getVideos().entrySet()) { +                    String videoId = videoEntry.getKey(); +                    VideoRecord myVideo = myChapter.getVideoRecord(videoId); + +                    if (myVideo == null) { +                        try { +                            myVideo = videoEntry.getValue().clone(); +                            myChapter.setVideoRecord(videoId, myVideo); +                        } catch (CloneNotSupportedException e) { +                            throw new RuntimeException(e); // Unexpected... +                        } +                    } +                } +            } +        } + +        mLastUpdated = source.getLastUpdated(); +    } +} diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java index 37deffa..f4b9458 100644 --- a/src/com/p4square/grow/model/Question.java +++ b/src/com/p4square/grow/model/Question.java @@ -7,9 +7,9 @@ package com.p4square.grow.model;  import java.util.HashMap;  import java.util.Map; -import org.codehaus.jackson.annotate.JsonSubTypes; -import org.codehaus.jackson.annotate.JsonSubTypes.Type; -import org.codehaus.jackson.annotate.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo;  /**   * Model of an assessment question. diff --git a/src/com/p4square/grow/model/TrainingRecord.java b/src/com/p4square/grow/model/TrainingRecord.java new file mode 100644 index 0000000..bc3ffa9 --- /dev/null +++ b/src/com/p4square/grow/model/TrainingRecord.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Representation of a user's training record. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class TrainingRecord { +    private String mLastVideo; +    private Playlist mPlaylist; + +    public TrainingRecord() { +        mPlaylist = new Playlist(); +    } + +    /** +     * @return Video id of the last video watched. +     */ +    public String getLastVideo() { +        return mLastVideo; +    } + +    /** +     * Set the video id for the last video watched. +     * @param video The new video id. +     */ +    public void setLastVideo(String video) { +        mLastVideo = video; +    } + +    /** +     * @return the user's Playlist. +     */ +    public Playlist getPlaylist() { +        return mPlaylist; +    } + +    /** +     * Set the user's playlist. +     * @param playlist The new playlist. +     */ +    public void setPlaylist(Playlist playlist) { +        mPlaylist = playlist; +    } +} diff --git a/src/com/p4square/grow/backend/resources/VideoRecord.java b/src/com/p4square/grow/model/VideoRecord.java index 2ba28c3..ec99d0d 100644 --- a/src/com/p4square/grow/backend/resources/VideoRecord.java +++ b/src/com/p4square/grow/model/VideoRecord.java @@ -2,27 +2,32 @@   * Copyright 2013 Jesse Morgan   */ -package com.p4square.grow.backend.resources; +package com.p4square.grow.model;  import java.util.Date; +import com.fasterxml.jackson.annotation.JsonIgnore; +  /**   * Simple bean containing video completion data.   *   * @author Jesse Morgan <jesse@jesterpm.net>   */ -class VideoRecord { -    private boolean mComplete; -    private boolean mRequired; +public class VideoRecord implements Cloneable { +    private Boolean mComplete; +    private Boolean mRequired;      private Date mCompletionDate;      public VideoRecord() { -        mComplete = false; -        mRequired = true; +        mComplete = null; +        mRequired = null;          mCompletionDate = null;      }      public boolean getComplete() { +        if (mComplete == null) { +            return false; +        }          return mComplete;      } @@ -30,7 +35,15 @@ class VideoRecord {          mComplete = complete;      } +    @JsonIgnore +    public boolean isCompleteSet() { +        return mComplete != null; +    } +      public boolean getRequired() { +        if (mRequired == null) { +            return true; +        }          return mRequired;      } @@ -38,6 +51,11 @@ class VideoRecord {          mRequired = complete;      } +    @JsonIgnore +    public boolean isRequiredSet() { +        return mRequired != null; +    } +      public Date getCompletionDate() {          return mCompletionDate;      } @@ -53,4 +71,15 @@ class VideoRecord {          mComplete = true;          mCompletionDate = new Date();      } + +    /** +     * @return an identical clone of this record. +     */ +    public VideoRecord clone() throws CloneNotSupportedException { +        VideoRecord r = (VideoRecord) super.clone(); +        r.mComplete = mComplete; +        r.mRequired = mRequired; +        r.mCompletionDate = mCompletionDate; +        return r; +    }  } diff --git a/src/com/p4square/grow/provider/JsonEncodedProvider.java b/src/com/p4square/grow/provider/JsonEncodedProvider.java index 605b18c..655b531 100644 --- a/src/com/p4square/grow/provider/JsonEncodedProvider.java +++ b/src/com/p4square/grow/provider/JsonEncodedProvider.java @@ -6,9 +6,9 @@ package com.p4square.grow.provider;  import java.io.IOException; -import org.codehaus.jackson.map.DeserializationConfig; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.SerializationConfig; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature;  /**   * Provider provides a simple interface for loading and persisting @@ -19,9 +19,9 @@ import org.codehaus.jackson.map.SerializationConfig;  public abstract class JsonEncodedProvider<K, V> implements Provider<K, V> {      public static final ObjectMapper MAPPER = new ObjectMapper();      static { -        MAPPER.configure(SerializationConfig.Feature.WRITE_ENUMS_USING_TO_STRING, true); -        MAPPER.configure(DeserializationConfig.Feature.READ_ENUMS_USING_TO_STRING, true); -        MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); +        MAPPER.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); +        MAPPER.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); +        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);      }      private final Class<V> mClazz; diff --git a/src/com/p4square/grow/provider/ProvidesQuestions.java b/src/com/p4square/grow/provider/ProvidesQuestions.java new file mode 100644 index 0000000..b43f649 --- /dev/null +++ b/src/com/p4square/grow/provider/ProvidesQuestions.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.Question; + +/** + * Indicates the ability to provide a Question Provider. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public interface ProvidesQuestions { +    /** +     * @return A Provider of Questions keyed by question id. +     */ +    Provider<String, Question> getQuestionProvider(); +} diff --git a/src/com/p4square/grow/provider/ProvidesTrainingRecords.java b/src/com/p4square/grow/provider/ProvidesTrainingRecords.java new file mode 100644 index 0000000..27ffa3e --- /dev/null +++ b/src/com/p4square/grow/provider/ProvidesTrainingRecords.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.TrainingRecord; + +/** + * Indicates the ability to provide a TrainingRecord Provider. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public interface ProvidesTrainingRecords { +    /** +     * @return A Provider of Questions keyed by question id. +     */ +    Provider<String, TrainingRecord> getTrainingRecordProvider(); +} diff --git a/src/com/p4square/grow/provider/TrainingRecordProvider.java b/src/com/p4square/grow/provider/TrainingRecordProvider.java new file mode 100644 index 0000000..44dba87 --- /dev/null +++ b/src/com/p4square/grow/provider/TrainingRecordProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import com.p4square.grow.model.TrainingRecord; + +/** + * TrainingRecordProvider wraps an existing Provider to get and put TrainingRecords. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class TrainingRecordProvider<K> implements Provider<String, TrainingRecord> { + +    private Provider<K, TrainingRecord> mProvider; + +    public TrainingRecordProvider(Provider<K, TrainingRecord> provider) { +        mProvider = provider; +    } + +    @Override +    public TrainingRecord get(String key) throws IOException { +        return mProvider.get(makeKey(key)); +    } + +    @Override +    public void put(String key, TrainingRecord obj) throws IOException { +        mProvider.put(makeKey(key), obj); +    } + +    /** +     * Make a Key for a TrainingRecord.. +     * +     * @param userId The user id. +     * @return a key for the TrainingRecord of userid. +     */ +    protected abstract K makeKey(String userId); +} | 
