summaryrefslogtreecommitdiff
path: root/src/main/java/com/p4square/grow/model
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2016-04-09 15:53:24 -0700
committerJesse Morgan <jesse@jesterpm.net>2016-04-09 15:53:24 -0700
commit371ccae3d1f31ec38f4af77fb7fcd175d49b3cd5 (patch)
tree38c4f1e8828f9af9c4b77a173bee0d312b321698 /src/main/java/com/p4square/grow/model
parentbbf907e51dfcf157bdee24dead1d531122aa25db (diff)
parent3102d8bce3426d9cf41aeaf201c360d342677770 (diff)
Merge pull request #10 from PuyallupFoursquare/maven
Switching from Ivy+Ant to Maven.
Diffstat (limited to 'src/main/java/com/p4square/grow/model')
-rw-r--r--src/main/java/com/p4square/grow/model/Answer.java142
-rw-r--r--src/main/java/com/p4square/grow/model/Banner.java20
-rw-r--r--src/main/java/com/p4square/grow/model/Chapter.java112
-rw-r--r--src/main/java/com/p4square/grow/model/CircleQuestion.java89
-rw-r--r--src/main/java/com/p4square/grow/model/ImageQuestion.java24
-rw-r--r--src/main/java/com/p4square/grow/model/Message.java103
-rw-r--r--src/main/java/com/p4square/grow/model/MessageThread.java60
-rw-r--r--src/main/java/com/p4square/grow/model/Playlist.java192
-rw-r--r--src/main/java/com/p4square/grow/model/Point.java79
-rw-r--r--src/main/java/com/p4square/grow/model/QuadQuestion.java89
-rw-r--r--src/main/java/com/p4square/grow/model/QuadScoringEngine.java49
-rw-r--r--src/main/java/com/p4square/grow/model/Question.java165
-rw-r--r--src/main/java/com/p4square/grow/model/RecordedAnswer.java34
-rw-r--r--src/main/java/com/p4square/grow/model/Score.java119
-rw-r--r--src/main/java/com/p4square/grow/model/ScoringEngine.java26
-rw-r--r--src/main/java/com/p4square/grow/model/SimpleScoringEngine.java26
-rw-r--r--src/main/java/com/p4square/grow/model/SliderQuestion.java24
-rw-r--r--src/main/java/com/p4square/grow/model/SliderScoringEngine.java35
-rw-r--r--src/main/java/com/p4square/grow/model/TextQuestion.java24
-rw-r--r--src/main/java/com/p4square/grow/model/TrainingRecord.java49
-rw-r--r--src/main/java/com/p4square/grow/model/UserRecord.java183
-rw-r--r--src/main/java/com/p4square/grow/model/VideoRecord.java85
22 files changed, 1729 insertions, 0 deletions
diff --git a/src/main/java/com/p4square/grow/model/Answer.java b/src/main/java/com/p4square/grow/model/Answer.java
new file mode 100644
index 0000000..a818365
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Answer.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.apache.log4j.Logger;
+
+/**
+ * This is the model of an assessment question's answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Answer {
+ private static final Logger LOG = Logger.getLogger(Answer.class);
+
+ /**
+ * ScoreType determines how the answer will be scored.
+ *
+ */
+ public static enum ScoreType {
+ /**
+ * This question has no effect on the score.
+ */
+ NONE,
+
+ /**
+ * The score of this question is part of the average.
+ */
+ AVERAGE,
+
+ /**
+ * The score of this question is the total score, no other questions
+ * matter after this point.
+ */
+ TRUMP;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ private String mAnswerText;
+ private ScoreType mType;
+ private float mScoreFactor;
+ private String mNextQuestionId;
+
+ public Answer() {
+ mType = ScoreType.AVERAGE;
+ }
+
+ /**
+ * @return The text associated with the answer.
+ */
+ public String getText() {
+ return mAnswerText;
+ }
+
+ /**
+ * Set the text associated with the answer.
+ * @param text The new text.
+ */
+ public void setText(String text) {
+ mAnswerText = text;
+ }
+
+ /**
+ * @return the ScoreType for the Answer.
+ */
+ public ScoreType getType() {
+ return mType;
+ }
+
+ /**
+ * Set the ScoreType for the answer.
+ * @param type The new ScoreType.
+ */
+ public void setType(ScoreType type) {
+ mType = type;
+ }
+
+ /**
+ * @return the delta of the score if this answer is selected.
+ */
+ public float getScore() {
+ if (mType == ScoreType.NONE) {
+ return 0;
+ }
+
+ return mScoreFactor;
+ }
+
+ /**
+ * Set the score delta for this answer.
+ * @param score The new delta.
+ */
+ public void setScore(float score) {
+ mScoreFactor = score;
+ }
+
+ /**
+ * @return the id of the next question if this answer is selected, or null
+ * if selecting this answer has no effect.
+ */
+ public String getNextQuestion() {
+ return mNextQuestionId;
+ }
+
+ /**
+ * Set the id of the next question when this answer is selected.
+ * @param id The next question id or null to proceed as usual.
+ */
+ public void setNextQuestion(String id) {
+ mNextQuestionId = id;
+ }
+
+ /**
+ * Adjust the running score for the selection of this answer.
+ * @param score The running score to adjust.
+ * @return true if scoring should continue, false if this answer trumps all.
+ */
+ public boolean score(final Score score) {
+ switch (getType()) {
+ case TRUMP:
+ score.sum = getScore();
+ score.count = 1;
+ return false; // Quit scoring.
+
+ case AVERAGE:
+ LOG.debug("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }");
+ score.sum += getScore();
+ score.count++;
+ break;
+
+ case NONE:
+ break;
+ }
+
+ return true; // Continue scoring
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Banner.java b/src/main/java/com/p4square/grow/model/Banner.java
new file mode 100644
index 0000000..b786b36
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Banner.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Page Banner Data.
+ */
+public class Banner {
+ private String mHtml;
+
+ public String getHtml() {
+ return mHtml;
+ }
+
+ public void setHtml(final String html) {
+ mHtml = html;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Chapter.java b/src/main/java/com/p4square/grow/model/Chapter.java
new file mode 100644
index 0000000..3a08e4c
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Chapter.java
@@ -0,0 +1,112 @@
+/*
+ * 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 String mName;
+ private Map<String, VideoRecord> mVideos;
+
+ public Chapter(String name) {
+ mName = name;
+ mVideos = new HashMap<String, VideoRecord>();
+ }
+
+ /**
+ * Private constructor for JSON decoding.
+ */
+ private Chapter() {
+ this(null);
+ }
+
+ /**
+ * @return The Chapter name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Set the chapter name.
+ *
+ * @param name The name of the chapter.
+ */
+ public void setName(final String name) {
+ mName = name;
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * Remove the VideoRecord for a video id.
+ * @param videoId The id to remove.
+ */
+ public void removeVideoRecord(String videoId) {
+ mVideos.remove(videoId);
+ }
+
+ /**
+ * @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(mName);
+ for (Map.Entry<String, VideoRecord> videoEntry : mVideos.entrySet()) {
+ c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone());
+ }
+ return c;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/CircleQuestion.java b/src/main/java/com/p4square/grow/model/CircleQuestion.java
new file mode 100644
index 0000000..71acc14
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/CircleQuestion.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Circle Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CircleQuestion extends Question {
+ private static final ScoringEngine ENGINE = new QuadScoringEngine();
+
+ private String mTopLeft;
+ private String mTopRight;
+ private String mBottomLeft;
+ private String mBottomRight;
+
+ /**
+ * @return the Top Left label.
+ */
+ public String getTopLeft() {
+ return mTopLeft;
+ }
+
+ /**
+ * Set the Top Left label.
+ * @param s The new top left label.
+ */
+ public void setTopLeft(String s) {
+ mTopLeft = s;
+ }
+
+ /**
+ * @return the Top Right label.
+ */
+ public String getTopRight() {
+ return mTopRight;
+ }
+
+ /**
+ * Set the Top Right label.
+ * @param s The new top left label.
+ */
+ public void setTopRight(String s) {
+ mTopRight = s;
+ }
+
+ /**
+ * @return the Bottom Left label.
+ */
+ public String getBottomLeft() {
+ return mBottomLeft;
+ }
+
+ /**
+ * Set the Bottom Left label.
+ * @param s The new top left label.
+ */
+ public void setBottomLeft(String s) {
+ mBottomLeft = s;
+ }
+
+ /**
+ * @return the Bottom Right label.
+ */
+ public String getBottomRight() {
+ return mBottomRight;
+ }
+
+ /**
+ * Set the Bottom Right label.
+ * @param s The new top left label.
+ */
+ public void setBottomRight(String s) {
+ mBottomRight = s;
+ }
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.CIRCLE;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/ImageQuestion.java b/src/main/java/com/p4square/grow/model/ImageQuestion.java
new file mode 100644
index 0000000..d94c32c
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/ImageQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Image Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ImageQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SimpleScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.IMAGE;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Message.java b/src/main/java/com/p4square/grow/model/Message.java
new file mode 100644
index 0000000..9d33320
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Message.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * A feed message.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Message {
+ private String mThreadId;
+ private String mId;
+ private UserRecord mAuthor;
+ private Date mCreated;
+ private String mMessage;
+
+ /**
+ * @return a new message id.
+ */
+ public static String generateId() {
+ return String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString());
+ }
+
+ /**
+ * @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 UserRecord getAuthor() {
+ return mAuthor;
+ }
+
+ /**
+ * Set the author of the message.
+ * @param author The new author.
+ */
+ public void setAuthor(UserRecord 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/main/java/com/p4square/grow/model/MessageThread.java b/src/main/java/com/p4square/grow/model/MessageThread.java
new file mode 100644
index 0000000..9542a18
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/MessageThread.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+ private Message mMessage;
+
+ /**
+ * Create a new thread with a probably unique id.
+ *
+ * @return the new thread.
+ */
+ public static MessageThread createNew() {
+ MessageThread t = new MessageThread();
+ // IDs are keyed to sort lexicographically from latest to oldest.
+ t.setId(String.format("%016x-%s", Long.MAX_VALUE - 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;
+ }
+
+ /**
+ * @return The original message.
+ */
+ public Message getMessage() {
+ return mMessage;
+ }
+
+ /**
+ * Set the original message.
+ * @param id The new message.
+ */
+ public void setMessage(Message message) {
+ mMessage = message;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Playlist.java b/src/main/java/com/p4square/grow/model/Playlist.java
new file mode 100644
index 0000000..3e77ada
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Playlist.java
@@ -0,0 +1,192 @@
+/*
+ * 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;
+ }
+
+ /**
+ * @param videoId The video to search for.
+ * @return the Chapter containing videoId.
+ */
+ private Chapter findChapter(String videoId) {
+ for (Chapter chapter : mPlaylist.values()) {
+ VideoRecord r = chapter.getVideoRecord(videoId);
+
+ if (r != null) {
+ return chapter;
+ }
+ }
+
+ 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(chapterId);
+ 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) {
+ chapter.setName(chapterId);
+ 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 new chapter
+ myChapter = new Chapter(chapterName);
+ addChapter(chapterName, myChapter);
+ }
+
+ // 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) {
+ myVideo = find(videoId);
+ if (myVideo == null) {
+ // New Video
+ try {
+ myVideo = videoEntry.getValue().clone();
+ myChapter.setVideoRecord(videoId, myVideo);
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e); // Unexpected...
+ }
+ } else {
+ // Video moved
+ findChapter(videoId).removeVideoRecord(videoId);
+ myChapter.setVideoRecord(videoId, myVideo);
+ }
+ }
+ }
+ }
+
+ mLastUpdated = source.getLastUpdated();
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Point.java b/src/main/java/com/p4square/grow/model/Point.java
new file mode 100644
index 0000000..e9fc0ca
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Point.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Simple double based point class.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Point {
+ /**
+ * Parse a comma separated x,y pair into a point.
+ *
+ * @return The point represented by the string.
+ * @throws IllegalArgumentException if the input is malformed.
+ */
+ public static Point valueOf(String str) {
+ final int comma = str.indexOf(',');
+ if (comma == -1 || comma == 0 || comma == str.length() - 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;
+
+ /**
+ * Create a new point with the given coordinates.
+ *
+ * @param x The x coordinate.
+ * @param y The y coordinate.
+ */
+ public Point(double x, double y) {
+ mX = x;
+ mY = y;
+ }
+
+ /**
+ * Compute the distance between this point and another.
+ *
+ * @param other The other point.
+ * @return The distance between this point and other.
+ */
+ public double distance(Point other) {
+ final double dx = mX - other.mX;
+ final double dy = mY - other.mY;
+
+ return Math.sqrt(dx*dx + dy*dy);
+ }
+
+ /**
+ * @return The x coordinate.
+ */
+ public double getX() {
+ return mX;
+ }
+
+ /**
+ * @return The y coordinate.
+ */
+ public double getY() {
+ return mY;
+ }
+
+ /**
+ * @return The point represented as a comma separated pair.
+ */
+ @Override
+ public String toString() {
+ return String.format("%.2f,%.2f", mX, mY);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/QuadQuestion.java b/src/main/java/com/p4square/grow/model/QuadQuestion.java
new file mode 100644
index 0000000..a7b4179
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/QuadQuestion.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Two-dimensional Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class QuadQuestion extends Question {
+ private static final ScoringEngine ENGINE = new QuadScoringEngine();
+
+ private String mTop;
+ private String mRight;
+ private String mBottom;
+ private String mLeft;
+
+ /**
+ * @return the top label.
+ */
+ public String getTop() {
+ return mTop;
+ }
+
+ /**
+ * Set the top label.
+ * @param s The new top label.
+ */
+ public void setTop(String s) {
+ mTop = s;
+ }
+
+ /**
+ * @return the right label.
+ */
+ public String getRight() {
+ return mRight;
+ }
+
+ /**
+ * Set the right label.
+ * @param s The new right label.
+ */
+ public void setRight(String s) {
+ mRight = s;
+ }
+
+ /**
+ * @return the bottom label.
+ */
+ public String getBottom() {
+ return mBottom;
+ }
+
+ /**
+ * Set the bottom label.
+ * @param s The new bottom label.
+ */
+ public void setBottom(String s) {
+ mBottom = s;
+ }
+
+ /**
+ * @return the left label.
+ */
+ public String getLeft() {
+ return mLeft;
+ }
+
+ /**
+ * Set the left label.
+ * @param s The new left label.
+ */
+ public void setLeft(String s) {
+ mLeft = s;
+ }
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.QUAD;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/QuadScoringEngine.java b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java
new file mode 100644
index 0000000..33403b5
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import com.p4square.grow.model.Point;
+
+/**
+ * QuadScoringEngine expects the user's answer to be a Point string. We find
+ * the closest answer Point to the user's answer and treat that as the answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class QuadScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ // Find all of the answer points.
+ Point[] answers = new Point[question.getAnswers().size()];
+ {
+ int i = 0;
+ for (String answerStr : question.getAnswers().keySet()) {
+ answers[i++] = Point.valueOf(answerStr);
+ }
+ }
+
+ // Parse the user's answer.
+ Point userPoint = Point.valueOf(userAnswer.getAnswerId());
+
+ // Find the closest answer point to the user's answer.
+ double minDistance = Double.MAX_VALUE;
+ int answerIndex = 0;
+ for (int i = 0; i < answers.length; i++) {
+ final double distance = userPoint.distance(answers[i]);
+ if (distance < minDistance) {
+ minDistance = distance;
+ answerIndex = i;
+ }
+ }
+
+ LOG.debug("Quad " + question.getId() + ": Got answer "
+ + answers[answerIndex].toString() + " for user point " + userAnswer);
+
+ // Get the answer and update the score.
+ final Answer answer = question.getAnswers().get(answers[answerIndex].toString());
+ return answer.score(score);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Question.java b/src/main/java/com/p4square/grow/model/Question.java
new file mode 100644
index 0000000..f4b9458
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Question.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * Model of an assessment question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type")
+@JsonSubTypes({
+ @Type(value = TextQuestion.class, name = "text"),
+ @Type(value = ImageQuestion.class, name = "image"),
+ @Type(value = SliderQuestion.class, name = "slider"),
+ @Type(value = QuadQuestion.class, name = "quad"),
+ @Type(value = CircleQuestion.class, name = "circle"),
+})
+public abstract class Question {
+ /**
+ * QuestionType indicates the type of Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+ public enum QuestionType {
+ TEXT,
+ IMAGE,
+ SLIDER,
+ QUAD,
+ CIRCLE;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ private String mQuestionId;
+ private QuestionType mType;
+ private String mQuestionText;
+ private Map<String, Answer> mAnswers;
+
+ private String mPreviousQuestionId;
+ private String mNextQuestionId;
+
+ public Question() {
+ mAnswers = new HashMap<String, Answer>();
+ }
+
+ /**
+ * @return the id String for this question.
+ */
+ public String getId() {
+ return mQuestionId;
+ }
+
+ /**
+ * Set the id String for this question.
+ * @param id New id
+ */
+ public void setId(String id) {
+ mQuestionId = id;
+ }
+
+ /**
+ * @return The Question text.
+ */
+ public String getQuestion() {
+ return mQuestionText;
+ }
+
+ /**
+ * Set the question text.
+ * @param value The new question text.
+ */
+ public void setQuestion(String value) {
+ mQuestionText = value;
+ }
+
+ /**
+ * @return The id String of the previous question or null if no previous question exists.
+ */
+ public String getPreviousQuestion() {
+ return mPreviousQuestionId;
+ }
+
+ /**
+ * Set the id string of the previous question.
+ * @param id Previous question id or null if there is no previous question.
+ */
+ public void setPreviousQuestion(String id) {
+ mPreviousQuestionId = id;
+ }
+
+ /**
+ * @return The id String of the next question or null if no next question exists.
+ */
+ public String getNextQuestion() {
+ return mNextQuestionId;
+ }
+
+ /**
+ * Set the id string of the next question.
+ * @param id next question id or null if there is no next question.
+ */
+ public void setNextQuestion(String id) {
+ mNextQuestionId = id;
+ }
+
+ /**
+ * @return a map of Answer id Strings to Answer objects.
+ */
+ public Map<String, Answer> getAnswers() {
+ return mAnswers;
+ }
+
+ /**
+ * Determine the id of the next question based on the answer to this
+ * question.
+ *
+ * @param answerid
+ * The id of the selected answer.
+ * @return a question id or null if this is the last question.
+ */
+ public String getNextQuestion(String answerid) {
+ String nextQuestion = null;
+
+ Answer a = mAnswers.get(answerid);
+ if (a != null) {
+ nextQuestion = a.getNextQuestion();
+ }
+
+ if (nextQuestion == null) {
+ nextQuestion = mNextQuestionId;
+ }
+
+ return nextQuestion;
+ }
+
+ /**
+ * Update the score based on the answer to this question.
+ *
+ * @param score The running score to update.
+ * @param answer The answer give to this question.
+ * @return true if scoring should continue, false if this answer trumps everything else.
+ */
+ public abstract boolean scoreAnswer(Score score, RecordedAnswer answer);
+
+ /**
+ * @return the QuestionType of this question.
+ */
+ public abstract QuestionType getType();
+
+}
diff --git a/src/main/java/com/p4square/grow/model/RecordedAnswer.java b/src/main/java/com/p4square/grow/model/RecordedAnswer.java
new file mode 100644
index 0000000..7d9905d
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/RecordedAnswer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Simple model for a user's assessment answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class RecordedAnswer {
+ private String mAnswerId;
+
+ /**
+ * @return The user's answer.
+ */
+ public String getAnswerId() {
+ return mAnswerId;
+ }
+
+ /**
+ * Set the answer id field.
+ * @param id The new id.
+ */
+ public void setAnswerId(String id) {
+ mAnswerId = id;
+ }
+
+ @Override
+ public String toString() {
+ return mAnswerId;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/Score.java b/src/main/java/com/p4square/grow/model/Score.java
new file mode 100644
index 0000000..031c309
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/Score.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Simple structure containing a score's sum and count.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Score {
+ /**
+ * Return the decimal value for the given Score String.
+ *
+ * This method satisfies the invariant for Score x:
+ * numericScore(x.toString()) <= x.getScore()
+ */
+ public static double numericScore(String score) {
+ score = score.toLowerCase();
+
+ if ("teacher".equals(score)) {
+ return 3.5;
+ } else if ("disciple".equals(score)) {
+ return 2.5;
+ } else if ("believer".equals(score)) {
+ return 1.5;
+ } else if ("seeker".equals(score)) {
+ return 0;
+ } else {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ double sum;
+ int count;
+
+ public Score() {
+ sum = 0;
+ count = 0;
+ }
+
+ public Score(double sum, int count) {
+ this.sum = sum;
+ this.count = count;
+ }
+
+ /**
+ * Copy Constructor.
+ */
+ public Score(Score other) {
+ sum = other.sum;
+ count = other.count;
+ }
+
+ /**
+ * @return The sum of all the points.
+ */
+ public double getSum() {
+ return sum;
+ }
+
+ /**
+ * @return The number of questions included in the score.
+ */
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * @return The final score.
+ */
+ public double getScore() {
+ if (count == 0) {
+ return 0;
+ }
+
+ return sum / count;
+ }
+
+ /**
+ * @return the lowest score in the same category as this score.
+ */
+ public double floor() {
+ final double score = getScore();
+
+ if (score >= 3.5) {
+ return 3.5; // teacher
+
+ } else if (score >= 2.5) {
+ return 2.5; // disciple
+
+ } else if (score >= 1.5) {
+ return 1.5; // believer
+
+ } else {
+ return 0; // seeker
+ }
+ }
+
+ @Override
+ public String toString() {
+ final double score = getScore();
+
+ if (score >= 3.5) {
+ return "teacher";
+
+ } else if (score >= 2.5) {
+ return "disciple";
+
+ } else if (score >= 1.5) {
+ return "believer";
+
+ } else {
+ return "seeker";
+ }
+ }
+
+}
diff --git a/src/main/java/com/p4square/grow/model/ScoringEngine.java b/src/main/java/com/p4square/grow/model/ScoringEngine.java
new file mode 100644
index 0000000..8ff18b3
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/ScoringEngine.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.apache.log4j.Logger;
+
+/**
+ * ScoringEngine computes the score for a question and a given answer.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class ScoringEngine {
+ protected static final Logger LOG = Logger.getLogger(ScoringEngine.class);
+
+ /**
+ * Update the score based on the given question and answer.
+ *
+ * @param score The running score to update.
+ * @param question The question to compute the score for.
+ * @param answer The answer give to this question.
+ * @return true if scoring should continue, false if this answer trumps everything else.
+ */
+ public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer);
+}
diff --git a/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java
new file mode 100644
index 0000000..6ef2dbb
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * SimpleScoringEngine expects the user's answer to a valid answer id and
+ * scores accordingly.
+ *
+ * If the answer id is not valid an Exception is thrown.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SimpleScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ final Answer answer = question.getAnswers().get(userAnswer.getAnswerId());
+ if (answer == null) {
+ throw new IllegalArgumentException("Not a valid answer.");
+ }
+
+ return answer.score(score);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/SliderQuestion.java b/src/main/java/com/p4square/grow/model/SliderQuestion.java
new file mode 100644
index 0000000..f0861e3
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/SliderQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Slider Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SliderQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SliderScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.SLIDER;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/SliderScoringEngine.java b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java
new file mode 100644
index 0000000..2961e95
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * SliderScoringEngine expects the user's answer to be a decimal value in the
+ * range [0, 1]. The value is scaled to the range [1, 4] and added to the
+ * score.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SliderScoringEngine extends ScoringEngine {
+
+ @Override
+ public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) {
+ int numberOfAnswers = question.getAnswers().size();
+ if (numberOfAnswers == 0) {
+ throw new IllegalArgumentException("Question has no answers.");
+ }
+
+ double answer = Double.valueOf(userAnswer.getAnswerId());
+ if (answer < 0 || answer > 1) {
+ throw new IllegalArgumentException("Answer out of bounds.");
+ }
+
+ double delta = Math.max(1, Math.ceil(answer * numberOfAnswers) / numberOfAnswers * 4);
+
+ score.sum += delta;
+ score.count++;
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/TextQuestion.java b/src/main/java/com/p4square/grow/model/TextQuestion.java
new file mode 100644
index 0000000..88c2a34
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/TextQuestion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Text Question.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TextQuestion extends Question {
+ private static final ScoringEngine ENGINE = new SimpleScoringEngine();
+
+ @Override
+ public boolean scoreAnswer(Score score, RecordedAnswer answer) {
+ return ENGINE.scoreAnswer(score, this, answer);
+ }
+
+ @Override
+ public QuestionType getType() {
+ return QuestionType.TEXT;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/TrainingRecord.java b/src/main/java/com/p4square/grow/model/TrainingRecord.java
new file mode 100644
index 0000000..bc3ffa9
--- /dev/null
+++ b/src/main/java/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/main/java/com/p4square/grow/model/UserRecord.java b/src/main/java/com/p4square/grow/model/UserRecord.java
new file mode 100644
index 0000000..4399282
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/UserRecord.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.apache.commons.codec.binary.Hex;
+
+import org.restlet.security.User;
+
+/**
+ * A simple user representation without any secrets.
+ */
+public class UserRecord {
+ private String mId;
+ private String mFirstName;
+ private String mLastName;
+ private String mEmail;
+ private String mLanding;
+ private boolean mNewBeliever;
+
+ // Backend Access
+ private String mBackendPasswordHash;
+
+ /**
+ * Create an empty UserRecord.
+ */
+ public UserRecord() {
+ }
+
+ /**
+ * Create a new UserRecord with the information from a User.
+ */
+ public UserRecord(final User user) {
+ mId = user.getIdentifier();
+ mFirstName = user.getFirstName();
+ mLastName = user.getLastName();
+ mEmail = user.getEmail();
+ }
+
+ /**
+ * @return The user's identifier.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Set the user's identifier.
+ * @param value The new id.
+ */
+ public void setId(final String value) {
+ mId = value;
+ }
+
+ /**
+ * @return The user's email.
+ */
+ public String getEmail() {
+ return mEmail;
+ }
+
+ /**
+ * Set the user's email.
+ * @param value The new email.
+ */
+ public void setEmail(final String value) {
+ mEmail = value;
+ }
+
+ /**
+ * @return The user's first name.
+ */
+ public String getFirstName() {
+ return mFirstName;
+ }
+
+ /**
+ * Set the user's first name.
+ * @param value The new first name.
+ */
+ public void setFirstName(final String value) {
+ mFirstName = value;
+ }
+
+ /**
+ * @return The user's last name.
+ */
+ public String getLastName() {
+ return mLastName;
+ }
+
+ /**
+ * Set the user's last name.
+ * @param value The new last name.
+ */
+ public void setLastName(final String value) {
+ mLastName = value;
+ }
+
+ /**
+ * @return The user's landing page.
+ */
+ public String getLanding() {
+ return mLanding;
+ }
+
+ /**
+ * Set the user's landing page.
+ * @param value The new landing page.
+ */
+ public void setLanding(final String value) {
+ mLanding = value;
+ }
+
+ /**
+ * @return true if the user came from the New Believer's landing.
+ */
+ public boolean getNewBeliever() {
+ return mNewBeliever;
+ }
+
+ /**
+ * Set the user's new believer flag.
+ * @param value The new flag.
+ */
+ public void setNewBeliever(final boolean value) {
+ mNewBeliever = value;
+ }
+
+ /**
+ * @return The user's backend password hash, null if he doesn't have
+ * access.
+ */
+ public String getBackendPasswordHash() {
+ return mBackendPasswordHash;
+ }
+
+ /**
+ * Set the user's backend password hash.
+ * @param value The new backend password hash or null to remove
+ * access.
+ */
+ public void setBackendPasswordHash(final String value) {
+ mBackendPasswordHash = value;
+ }
+
+ /**
+ * Set the user's backend password to the clear-text value given.
+ * @param value The new backend password.
+ */
+ public void setBackendPassword(final String value) {
+ try {
+ mBackendPasswordHash = hashPassword(value);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Hash the given secret.
+ */
+ public static String hashPassword(final String secret) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+
+ // Convert the char[] to byte[]
+ // FIXME This approach is incorrectly truncating multibyte
+ // characters.
+ byte[] b = new byte[secret.length()];
+ for (int i = 0; i < secret.length(); i++) {
+ b[i] = (byte) secret.charAt(i);
+ }
+
+ md.update(b);
+
+ byte[] hash = md.digest();
+ return new String(Hex.encodeHex(hash));
+ }
+}
diff --git a/src/main/java/com/p4square/grow/model/VideoRecord.java b/src/main/java/com/p4square/grow/model/VideoRecord.java
new file mode 100644
index 0000000..ec99d0d
--- /dev/null
+++ b/src/main/java/com/p4square/grow/model/VideoRecord.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+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>
+ */
+public class VideoRecord implements Cloneable {
+ private Boolean mComplete;
+ private Boolean mRequired;
+ private Date mCompletionDate;
+
+ public VideoRecord() {
+ mComplete = null;
+ mRequired = null;
+ mCompletionDate = null;
+ }
+
+ public boolean getComplete() {
+ if (mComplete == null) {
+ return false;
+ }
+ return mComplete;
+ }
+
+ public void setComplete(boolean complete) {
+ mComplete = complete;
+ }
+
+ @JsonIgnore
+ public boolean isCompleteSet() {
+ return mComplete != null;
+ }
+
+ public boolean getRequired() {
+ if (mRequired == null) {
+ return true;
+ }
+ return mRequired;
+ }
+
+ public void setRequired(boolean complete) {
+ mRequired = complete;
+ }
+
+ @JsonIgnore
+ public boolean isRequiredSet() {
+ return mRequired != null;
+ }
+
+ public Date getCompletionDate() {
+ return mCompletionDate;
+ }
+
+ public void setCompletionDate(Date date) {
+ mCompletionDate = date;
+ }
+
+ /**
+ * Convenience method to mark a video complete.
+ */
+ public void complete() {
+ 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;
+ }
+}