summaryrefslogtreecommitdiff
path: root/src/main/java/com/p4square/grow/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/p4square/grow/frontend')
-rw-r--r--src/main/java/com/p4square/grow/frontend/AccountRedirectResource.java113
-rw-r--r--src/main/java/com/p4square/grow/frontend/AssessmentResetPage.java99
-rw-r--r--src/main/java/com/p4square/grow/frontend/AssessmentResultsPage.java145
-rw-r--r--src/main/java/com/p4square/grow/frontend/AuthenticatedResource.java18
-rw-r--r--src/main/java/com/p4square/grow/frontend/ChapterCompletePage.java209
-rw-r--r--src/main/java/com/p4square/grow/frontend/ErrorPage.java77
-rw-r--r--src/main/java/com/p4square/grow/frontend/FeedData.java105
-rw-r--r--src/main/java/com/p4square/grow/frontend/FeedResource.java101
-rw-r--r--src/main/java/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java26
-rw-r--r--src/main/java/com/p4square/grow/frontend/GrowFrontend.java230
-rw-r--r--src/main/java/com/p4square/grow/frontend/IntegrationDriver.java26
-rw-r--r--src/main/java/com/p4square/grow/frontend/JsonRequestProvider.java96
-rw-r--r--src/main/java/com/p4square/grow/frontend/LoginFormAuthenticator.java146
-rw-r--r--src/main/java/com/p4square/grow/frontend/LoginPageResource.java77
-rw-r--r--src/main/java/com/p4square/grow/frontend/LogoutResource.java40
-rw-r--r--src/main/java/com/p4square/grow/frontend/NewAccountResource.java135
-rw-r--r--src/main/java/com/p4square/grow/frontend/NewBelieverResource.java72
-rw-r--r--src/main/java/com/p4square/grow/frontend/NotFoundException.java13
-rw-r--r--src/main/java/com/p4square/grow/frontend/ProgressReporter.java30
-rw-r--r--src/main/java/com/p4square/grow/frontend/SurveyPageResource.java343
-rw-r--r--src/main/java/com/p4square/grow/frontend/TrainingPageResource.java268
-rw-r--r--src/main/java/com/p4square/grow/frontend/VideosResource.java133
22 files changed, 2502 insertions, 0 deletions
diff --git a/src/main/java/com/p4square/grow/frontend/AccountRedirectResource.java b/src/main/java/com/p4square/grow/frontend/AccountRedirectResource.java
new file mode 100644
index 0000000..be2ae65
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/AccountRedirectResource.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.model.UserRecord;
+import com.p4square.grow.provider.Provider;
+import com.p4square.grow.provider.DelegateProvider;
+import com.p4square.grow.provider.JsonEncodedProvider;
+
+/**
+ * This resource simply redirects the user to either the assessment
+ * or the training page.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class AccountRedirectResource extends ServerResource {
+ private static final Logger LOG = Logger.getLogger(AccountRedirectResource.class);
+
+ private Config mConfig;
+ private Provider<String, UserRecord> mUserRecordProvider;
+
+ // Fields pertaining to this request.
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+
+ mUserRecordProvider = new DelegateProvider<String, String, UserRecord>(
+ new JsonRequestProvider<UserRecord>(getContext().getClientDispatcher(),
+ UserRecord.class)) {
+ @Override
+ public String makeKey(String userid) {
+ return getBackendEndpoint() + "/accounts/" + userid;
+ }
+ };
+
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Redirect to the correct landing.
+ */
+ @Override
+ protected Representation get() {
+ if (mUserId == null || mUserId.length() == 0) {
+ // This shouldn't happen, but I want to be safe because of the DB insert below.
+ setStatus(Status.CLIENT_ERROR_FORBIDDEN);
+ return new ErrorPage("Not Authenticated!");
+ }
+
+ try {
+ // Fetch account Map.
+ UserRecord user = null;
+ try {
+ user = mUserRecordProvider.get(mUserId);
+ } catch (NotFoundException e) {
+ // User record doesn't exist, so create a new one.
+ user = new UserRecord(getRequest().getClientInfo().getUser());
+ mUserRecordProvider.put(mUserId, user);
+ }
+
+ // Check for the new believers cookie
+ String cookie = getRequest().getCookies().getFirstValue(NewBelieverResource.COOKIE_NAME);
+ if (cookie != null && cookie.length() != 0) {
+ user.setLanding("training");
+ user.setNewBeliever(true);
+ mUserRecordProvider.put(mUserId, user);
+ }
+
+ String landing = user.getLanding();
+ if (landing == null) {
+ landing = "assessment";
+ }
+
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/" + landing;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/AssessmentResetPage.java b/src/main/java/com/p4square/grow/frontend/AssessmentResetPage.java
new file mode 100644
index 0000000..519b135
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/AssessmentResetPage.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.ClientException;
+
+import com.p4square.grow.config.Config;
+
+/**
+ * This page delete's the current user's assessment.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class AssessmentResetPage extends FreeMarkerPageResource {
+ private static final Logger LOG = Logger.getLogger(AssessmentResetPage.class);
+
+ private GrowFrontend mGrowFrontend;
+ private Config mConfig;
+ private JsonRequestClient mJsonClient;
+
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ mConfig = mGrowFrontend.getConfig();
+
+ mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ // Get the assessment results
+ JsonResponse response = backendDelete("/accounts/" + mUserId + "/assessment");
+ if (!response.getStatus().isSuccess()) {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.BACKEND_ERROR;
+ }
+
+ String nextPage = mConfig.getString("dynamicRoot", "")
+ + "/account/assessment/question/first";
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendDelete(final String uri) {
+ LOG.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.delete(getBackendEndpoint() + uri);
+ final Status status = response.getStatus();
+ if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+ LOG.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString());
+ }
+
+ return response;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/AssessmentResultsPage.java b/src/main/java/com/p4square/grow/frontend/AssessmentResultsPage.java
new file mode 100644
index 0000000..f1c924b
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/AssessmentResultsPage.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Date;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.p4square.f1oauth.FellowshipOneIntegrationDriver;
+import freemarker.template.Template;
+
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.ClientException;
+
+import com.p4square.f1oauth.Attribute;
+import com.p4square.f1oauth.F1API;
+import com.p4square.f1oauth.F1User;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.provider.JsonEncodedProvider;
+import org.restlet.security.User;
+
+/**
+ * This page fetches the user's final score and displays the transitional page between
+ * the assessment and the videos.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class AssessmentResultsPage extends FreeMarkerPageResource {
+ private static final Logger LOG = Logger.getLogger(AssessmentResultsPage.class);
+
+ private GrowFrontend mGrowFrontend;
+ private Config mConfig;
+ private JsonRequestClient mJsonClient;
+
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ mConfig = mGrowFrontend.getConfig();
+
+ mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("templates/assessment-results.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+
+ Map<String, Object> root = getRootObject();
+
+ // Get the assessment results
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/assessment");
+ if (!response.getStatus().isSuccess()) {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.BACKEND_ERROR;
+ }
+
+ final String score = (String) response.getMap().get("result");
+ if (score == null) {
+ // Odd... send them to the first questions
+ String nextPage = mConfig.getString("dynamicRoot", "")
+ + "/account/assessment/question/first";
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+ // Publish results in F1
+ publishScoreInF1(response.getMap());
+
+ root.put("stage", score);
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ private void publishScoreInF1(Map results) {
+ final ProgressReporter reporter = mGrowFrontend.getThirdPartyIntegrationFactory().getProgressReporter();
+
+ try {
+ final User user = getRequest().getClientInfo().getUser();
+ final String level = results.get("result").toString();
+ final Date completionDate = new Date();
+ final String data = JsonEncodedProvider.MAPPER.writeValueAsString(results);
+
+ reporter.reportAssessmentComplete(user, level, completionDate, data);
+
+ } catch (JsonProcessingException e) {
+ LOG.error("Failed to generate json " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ LOG.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ final Status status = response.getStatus();
+ if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+ LOG.warn("Error making backend request for '" + uri + "'. status = "
+ + response.getStatus().toString());
+ }
+
+ return response;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/AuthenticatedResource.java b/src/main/java/com/p4square/grow/frontend/AuthenticatedResource.java
new file mode 100644
index 0000000..800eb83
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/AuthenticatedResource.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class AuthenticatedResource extends ServerResource {
+ protected Representation post() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/ChapterCompletePage.java b/src/main/java/com/p4square/grow/frontend/ChapterCompletePage.java
new file mode 100644
index 0000000..35abc43
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/ChapterCompletePage.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Date;
+import java.util.Map;
+
+import com.p4square.f1oauth.FellowshipOneIntegrationDriver;
+import freemarker.template.Template;
+
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.ClientException;
+
+import com.p4square.f1oauth.Attribute;
+import com.p4square.f1oauth.F1API;
+import com.p4square.f1oauth.F1User;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.model.TrainingRecord;
+import com.p4square.grow.provider.Provider;
+import com.p4square.grow.provider.TrainingRecordProvider;
+import org.restlet.security.User;
+
+/**
+ * This resource displays the transitional page between chapters.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ChapterCompletePage extends FreeMarkerPageResource {
+ private static final Logger LOG = Logger.getLogger(ChapterCompletePage.class);
+
+ private GrowFrontend mGrowFrontend;
+ private Config mConfig;
+ private JsonRequestClient mJsonClient;
+ private Provider<String, TrainingRecord> mTrainingRecordProvider;
+
+ private String mUserId;
+ private String mChapter;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ 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();
+
+ mChapter = getAttribute("chapter");
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ Map<String, Object> root = getRootObject();
+
+ // Get the training summary
+ 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...
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/training/" + mChapter;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+ // Publish the training chapter complete attribute.
+ assignAttribute();
+
+ // Find the next chapter
+ String nextChapter = null;
+ {
+ int min = Integer.MAX_VALUE;
+ for (Map.Entry<String, Boolean> chapter : chapters.entrySet()) {
+ int index = chapterIndex(chapter.getKey());
+ if (!chapter.getValue() && index < min) {
+ min = index;
+ nextChapter = chapter.getKey();
+ }
+ }
+ }
+
+ String nextOverride = getQueryValue("next");
+ if (nextOverride != null) {
+ nextChapter = nextOverride;
+ }
+
+ root.put("stage", mChapter);
+ root.put("nextstage", nextChapter);
+
+ /*
+ * We will display one of two transitional pages:
+ *
+ * If the next chapter has a forward page, display the forward page.
+ * Else, if this chapter is not "Introduction", display the chapter
+ * complete message.
+ */
+ Template t = mGrowFrontend.getTemplate("templates/stage-"
+ + nextChapter + "-forward.ftl");
+
+ if (t == null) {
+ // Skip the chapter complete message for "Introduction"
+ if ("introduction".equals(mChapter)) {
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/training/" + nextChapter;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+ t = mGrowFrontend.getTemplate("templates/stage-complete.ftl");
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+ }
+
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ private void assignAttribute() {
+ final ProgressReporter reporter = mGrowFrontend.getThirdPartyIntegrationFactory().getProgressReporter();
+
+ final User user = getRequest().getClientInfo().getUser();
+ final Date completionDate = new Date();
+
+ reporter.reportChapterComplete(user, mChapter, completionDate);
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ LOG.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ final Status status = response.getStatus();
+ if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+ LOG.warn("Error making backend request for '" + uri
+ + "'. status = " + response.getStatus().toString());
+ }
+
+ return response;
+ }
+
+ int chapterIndex(String chapter) {
+ if ("leader".equals(chapter)) {
+ return 5;
+ } else if ("teacher".equals(chapter)) {
+ return 4;
+ } else if ("disciple".equals(chapter)) {
+ return 3;
+ } else if ("believer".equals(chapter)) {
+ return 2;
+ } else if ("seeker".equals(chapter)) {
+ return 1;
+ } else {
+ return Integer.MAX_VALUE;
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/ErrorPage.java b/src/main/java/com/p4square/grow/frontend/ErrorPage.java
new file mode 100644
index 0000000..81abe74
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/ErrorPage.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import freemarker.template.Template;
+
+import org.restlet.data.MediaType;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.WriterRepresentation;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * ErrorPage wraps a String or Template Representation and displays the given
+ * error message.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ErrorPage extends WriterRepresentation {
+ public static final ErrorPage TEMPLATE_NOT_FOUND =
+ new ErrorPage("Could not find the requested page template.");
+
+ public static final ErrorPage RENDER_ERROR =
+ new ErrorPage("Error rendering page.");
+
+ public static final ErrorPage BACKEND_ERROR =
+ new ErrorPage("Error communicating with backend.");
+
+ public static final ErrorPage NOT_FOUND =
+ new ErrorPage("The requested URL could not be found.");
+
+ private static Template cTemplate = null;
+ private static Map<String, Object> cRoot = null;
+
+ private final String mMessage;
+
+ public ErrorPage(String msg) {
+ this(msg, MediaType.TEXT_HTML);
+ }
+
+ public ErrorPage(String msg, MediaType mediaType) {
+ super(mediaType);
+
+ mMessage = msg;
+ }
+
+ public static synchronized void setTemplate(Template template, Map<String, Object> root) {
+ cTemplate = template;
+ cRoot = root;
+ }
+
+ protected Representation getRepresentation() {
+ if (cTemplate == null) {
+ return new StringRepresentation(mMessage);
+
+ } else {
+ Map<String, Object> root = new HashMap<String, Object>(cRoot);
+ root.put("errorMessage", mMessage);
+ return new TemplateRepresentation(cTemplate, root, MediaType.TEXT_HTML);
+ }
+ }
+
+ @Override
+ public void write(Writer writer) throws IOException {
+ getRepresentation().write(writer);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/FeedData.java b/src/main/java/com/p4square/grow/frontend/FeedData.java
new file mode 100644
index 0000000..feb03a1
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/FeedData.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import org.restlet.Context;
+import org.restlet.Restlet;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.frontend.JsonRequestProvider;
+import com.p4square.grow.model.Message;
+import com.p4square.grow.model.MessageThread;
+import com.p4square.grow.provider.JsonEncodedProvider;
+import com.p4square.grow.provider.Provider;
+
+/**
+ * Fetch feed data for a topic.
+ */
+public class FeedData {
+
+ /**
+ * Allowed Topics.
+ */
+ public static final HashSet<String> TOPICS = new HashSet(Arrays.asList("seeker", "believer",
+ "disciple", "teacher", "leader"));
+
+
+ private final Config mConfig;
+ private final String mBackendURI;
+
+ // TODO: Elegantly merge the List and individual providers.
+ private final JsonRequestProvider<List<MessageThread>> mThreadsProvider;
+ private final JsonRequestProvider<MessageThread> mThreadProvider;
+
+ private final JsonRequestProvider<List<Message>> mMessagesProvider;
+ private final JsonRequestProvider<Message> mMessageProvider;
+
+ public FeedData(final Context context, final Config config) {
+ mConfig = config;
+ mBackendURI = mConfig.getString("backendUri", "riap://component/backend") + "/feed";
+
+ Restlet clientDispatcher = context.getClientDispatcher();
+
+ TypeFactory factory = JsonEncodedProvider.MAPPER.getTypeFactory();
+
+ JavaType threadType = factory.constructCollectionType(List.class, MessageThread.class);
+ mThreadsProvider = new JsonRequestProvider<List<MessageThread>>(clientDispatcher, threadType);
+ mThreadProvider = new JsonRequestProvider<MessageThread>(clientDispatcher, MessageThread.class);
+
+ JavaType messageType = factory.constructCollectionType(List.class, Message.class);
+ mMessagesProvider = new JsonRequestProvider<List<Message>>(clientDispatcher, messageType);
+ mMessageProvider = new JsonRequestProvider<Message>(clientDispatcher, Message.class);
+ }
+
+ /**
+ * Get the threads for a topic.
+ *
+ * @param topic The topic to request threads for.
+ * @param limit The maximum number of threads.
+ * @return A list of MessageThread objects.
+ */
+ public List<MessageThread> getThreads(final String topic, final int limit) throws IOException {
+ return mThreadsProvider.get(makeUrl(limit, topic));
+ }
+
+ public List<Message> getMessages(final String topic, final String threadId) throws IOException {
+ return mMessagesProvider.get(makeUrl(topic, threadId));
+ }
+
+ public void createThread(final String topic, final Message message) throws IOException {
+ MessageThread thread = new MessageThread();
+ thread.setMessage(message);
+
+ mThreadProvider.post(makeUrl(topic), thread);
+ }
+
+ public void createResponse(final String topic, final String thread, final Message message)
+ throws IOException {
+
+ mMessageProvider.post(makeUrl(topic, thread), message);
+ }
+
+ private String makeUrl(String... parts) {
+ String url = mBackendURI;
+ for (String part : parts) {
+ url += "/" + part;
+ }
+
+ return url;
+ }
+
+ private String makeUrl(int limit, String... parts) {
+ return makeUrl(parts) + "?limit=" + limit;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/FeedResource.java b/src/main/java/com/p4square/grow/frontend/FeedResource.java
new file mode 100644
index 0000000..13d0fa0
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/FeedResource.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+import org.restlet.data.Form;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.model.Message;
+import com.p4square.grow.model.UserRecord;
+
+/**
+ * This resource handles user interactions with the feed.
+ */
+public class FeedResource extends ServerResource {
+ private static final Logger LOG = Logger.getLogger(FeedResource.class);
+
+ private Config mConfig;
+
+ private FeedData mFeedData;
+
+ // Fields pertaining to this request.
+ protected String mTopic;
+ protected String mThread;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+
+ mFeedData = new FeedData(getContext(), mConfig);
+
+ mTopic = getAttribute("topic");
+ if (mTopic != null) {
+ mTopic = mTopic.trim();
+ }
+
+ mThread = getAttribute("thread");
+ if (mThread != null) {
+ mThread = mThread.trim();
+ }
+ }
+
+ /**
+ * Create a new MessageThread.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ try {
+ if (mTopic == null || mTopic.length() == 0 || !FeedData.TOPICS.contains(mTopic)) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.NOT_FOUND;
+ }
+
+ Form form = new Form(entity);
+
+ String question = form.getFirstValue("question");
+
+ Message message = new Message();
+ message.setMessage(question);
+
+ UserRecord user = new UserRecord(getRequest().getClientInfo().getUser());
+ message.setAuthor(user);
+
+ if (mThread != null && mThread.length() != 0) {
+ // Post a response
+ mFeedData.createResponse(mTopic, mThread, message);
+
+ } else {
+ // Post a new thread
+ mFeedData.createThread(mTopic, message);
+ }
+
+ /*
+ * Can't trust the referrer, so we'll send them to the
+ * appropriate part of the training page
+ * TODO: This could be better done.
+ */
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/training/" + mTopic;
+ getResponse().redirectSeeOther(nextPage);
+ return null;
+
+ } catch (IOException e) {
+ LOG.fatal("Could not save message: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.BACKEND_ERROR;
+
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java b/src/main/java/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java
new file mode 100644
index 0000000..3ab140e
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+/**
+ * Display the Group Leader training videos.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class GroupLeaderTrainingPageResource extends TrainingPageResource {
+ private static final String[] CHAPTERS = { "leader" };
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mChapter = "leader";
+ }
+
+ @Override
+ public String[] getChaptersInOrder() {
+ return CHAPTERS;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/GrowFrontend.java b/src/main/java/com/p4square/grow/frontend/GrowFrontend.java
new file mode 100644
index 0000000..b5f62fb
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/GrowFrontend.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2013 Jesse Morgan <jesse@jesterpm.net>
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+
+import freemarker.template.Template;
+
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.Context;
+import org.restlet.Restlet;
+import org.restlet.data.Protocol;
+import org.restlet.resource.Directory;
+import org.restlet.routing.Redirector;
+import org.restlet.routing.Router;
+import org.restlet.security.Authenticator;
+
+import com.codahale.metrics.MetricRegistry;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FMFacade;
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.grow.config.Config;
+
+import com.p4square.restlet.metrics.MetricRouter;
+
+import com.p4square.session.SessionCheckingAuthenticator;
+import com.p4square.session.SessionCreatingAuthenticator;
+import org.restlet.security.Verifier;
+
+/**
+ * This is the Restlet Application implementing the Grow project front-end.
+ * It's implemented as an extension of FMFacade that connects interactive pages
+ * with various ServerResources. This class provides a main method to start a
+ * Jetty instance for testing.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class GrowFrontend extends FMFacade {
+ private static Logger LOG = Logger.getLogger(GrowFrontend.class);
+
+ private final Config mConfig;
+ private final MetricRegistry mMetricRegistry;
+
+ private IntegrationDriver mIntegrationFactory;
+
+ public GrowFrontend() {
+ this(new Config(), new MetricRegistry());
+ }
+
+ public GrowFrontend(Config config, MetricRegistry metricRegistry) {
+ mConfig = config;
+ mMetricRegistry = metricRegistry;
+ }
+
+ public Config getConfig() {
+ return mConfig;
+ }
+
+ public MetricRegistry getMetrics() {
+ return mMetricRegistry;
+ }
+
+ @Override
+ public synchronized void start() throws Exception {
+ Template errorTemplate = getTemplate("templates/error.ftl");
+ if (errorTemplate != null) {
+ ErrorPage.setTemplate(errorTemplate,
+ FreeMarkerPageResource.baseRootObject(getContext(), this));
+ }
+
+ getContext().getAttributes().put("com.p4square.grow.config", mConfig);
+ getContext().getAttributes().put("com.p4square.grow.metrics", mMetricRegistry);
+
+ super.start();
+ }
+
+ public synchronized IntegrationDriver getThirdPartyIntegrationFactory() {
+ if (mIntegrationFactory == null) {
+ final String driverClassName = getConfig().getString("integrationDriver",
+ "com.p4square.f1oauth.FellowshipOneIntegrationDriver");
+ try {
+ Class<?> clazz = Class.forName(driverClassName);
+ Constructor<?> constructor = clazz.getConstructor(Context.class);
+ mIntegrationFactory = (IntegrationDriver) constructor.newInstance(getContext());
+ } catch (Exception e) {
+ LOG.error("Failed to instantiate IntegrationDriver " + driverClassName);
+ }
+ }
+
+ return mIntegrationFactory;
+ }
+
+ @Override
+ protected Router createRouter() {
+ Router router = new MetricRouter(getContext(), mMetricRegistry);
+
+ final Authenticator defaultGuard = new SessionCheckingAuthenticator(getContext(), true);
+ defaultGuard.setNext(FreeMarkerPageResource.class);
+ router.attachDefault(defaultGuard);
+ router.attach("/", new Redirector(getContext(), "index.html", Redirector.MODE_CLIENT_PERMANENT));
+ router.attach("/login.html", LoginPageResource.class);
+ router.attach("/newaccount.html", NewAccountResource.class);
+ router.attach("/newbeliever", NewBelieverResource.class);
+
+ final Router accountRouter = new MetricRouter(getContext(), mMetricRegistry);
+ accountRouter.attach("/authenticate", AuthenticatedResource.class);
+ accountRouter.attach("/logout", LogoutResource.class);
+
+ accountRouter.attach("", AccountRedirectResource.class);
+ accountRouter.attach("/assessment/question/{questionId}", SurveyPageResource.class);
+ accountRouter.attach("/assessment/results", AssessmentResultsPage.class);
+ accountRouter.attach("/assessment/reset", AssessmentResetPage.class);
+ accountRouter.attach("/assessment", SurveyPageResource.class);
+ accountRouter.attach("/training/{chapter}/completed", ChapterCompletePage.class);
+ accountRouter.attach("/training/{chapter}/videos/{videoId}.json", VideosResource.class);
+ accountRouter.attach("/training/{chapter}", TrainingPageResource.class);
+ accountRouter.attach("/training", TrainingPageResource.class);
+ accountRouter.attach("/feed/{topic}", FeedResource.class);
+ accountRouter.attach("/feed/{topic}/{thread}", FeedResource.class);
+
+ final Authenticator accountGuard = createAuthenticatorChain(accountRouter);
+ router.attach("/account", accountGuard);
+
+ return router;
+ }
+
+ private Authenticator createAuthenticatorChain(Restlet last) {
+ final Context context = getContext();
+ final String loginPage = getConfig().getString("dynamicRoot", "") + "/login.html";
+ final String loginPost = getConfig().getString("dynamicRoot", "") + "/account/authenticate";
+ final String defaultPage = getConfig().getString("dynamicRoot", "") + "/account";
+
+ // This is used to check for an existing session
+ SessionCheckingAuthenticator sessionChk = new SessionCheckingAuthenticator(context, true);
+
+ // This is used to authenticate the user
+ Verifier verifier = getThirdPartyIntegrationFactory().newUserAuthenticationVerifier();
+ LoginFormAuthenticator loginAuth = new LoginFormAuthenticator(context, false, verifier);
+ loginAuth.setLoginFormUrl(loginPage);
+ loginAuth.setLoginPostUrl(loginPost);
+ loginAuth.setDefaultPage(defaultPage);
+
+ // This is used to create a new session for a newly authenticated user.
+ SessionCreatingAuthenticator sessionCreate = new SessionCreatingAuthenticator(context);
+
+ sessionChk.setNext(loginAuth);
+ loginAuth.setNext(sessionCreate);
+
+ sessionCreate.setNext(last);
+
+ return sessionChk;
+ }
+
+ /**
+ * Stand-alone main for testing.
+ */
+ public static void main(String[] args) {
+ // Start the HTTP Server
+ final Component component = new Component();
+ component.getServers().add(Protocol.HTTP, 8085);
+ component.getClients().add(Protocol.HTTP);
+ component.getClients().add(Protocol.HTTPS);
+ component.getClients().add(Protocol.FILE);
+ //component.getClients().add(new Client(null, Arrays.asList(Protocol.HTTPS), "org.restlet.ext.httpclient.HttpClientHelper"));
+
+ // Static content
+ try {
+ component.getDefaultHost().attach("/images/", new FileServingApp("./build/root/images/"));
+ component.getDefaultHost().attach("/scripts", new FileServingApp("./build/root/scripts"));
+ component.getDefaultHost().attach("/style.css", new FileServingApp("./build/root/style.css"));
+ component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/root/favicon.ico"));
+ component.getDefaultHost().attach("/notfound.html", new FileServingApp("./build/root/notfound.html"));
+ component.getDefaultHost().attach("/error.html", new FileServingApp("./build/root/error.html"));
+ } catch (IOException e) {
+ LOG.error("Could not create directory for static resources: "
+ + e.getMessage(), e);
+ }
+
+ // Setup App
+ GrowFrontend app = new GrowFrontend();
+
+ // Load an optional config file from the first argument.
+ app.getConfig().setDomain("dev");
+ if (args.length == 1) {
+ app.getConfig().updateConfig(args[0]);
+ }
+
+ component.getDefaultHost().attach(app);
+
+ // Setup shutdown hook
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ public void run() {
+ try {
+ component.stop();
+ } catch (Exception e) {
+ LOG.error("Exception during cleanup", e);
+ }
+ }
+ });
+
+ LOG.info("Starting server...");
+
+ try {
+ component.start();
+ } catch (Exception e) {
+ LOG.fatal("Could not start: " + e.getMessage(), e);
+ }
+ }
+
+ private static class FileServingApp extends Application {
+ private final String mPath;
+
+ public FileServingApp(String path) throws IOException {
+ mPath = new File(path).getAbsolutePath();
+ }
+
+ @Override
+ public Restlet createInboundRoot() {
+ return new Directory(getContext(), "file://" + mPath);
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/IntegrationDriver.java b/src/main/java/com/p4square/grow/frontend/IntegrationDriver.java
new file mode 100644
index 0000000..b9c3508
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/IntegrationDriver.java
@@ -0,0 +1,26 @@
+package com.p4square.grow.frontend;
+
+import org.restlet.security.Verifier;
+
+/**
+ * An IntegrationDriver is used to create implementations of various objects
+ * used to integration Grow with a particular Church Management System.
+ */
+public interface IntegrationDriver {
+
+ /**
+ * Create a new Restlet Verifier to authenticate users when they login to the site.
+ *
+ * @return A Verifier.
+ */
+ Verifier newUserAuthenticationVerifier();
+
+ /**
+ * Return a ProgressReporter for this Church Management System.
+ *
+ * The ProgressReporter should be thread-safe.
+ *
+ * @return The ProgressReporter.
+ */
+ ProgressReporter getProgressReporter();
+}
diff --git a/src/main/java/com/p4square/grow/frontend/JsonRequestProvider.java b/src/main/java/com/p4square/grow/frontend/JsonRequestProvider.java
new file mode 100644
index 0000000..bf3b2b3
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/JsonRequestProvider.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.JavaType;
+
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+import com.p4square.grow.provider.Provider;
+import com.p4square.grow.provider.JsonEncodedProvider;
+
+/**
+ * Fetch a JSON object via a Request.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class JsonRequestProvider<V> extends JsonEncodedProvider<V> implements Provider<String, V> {
+
+ private final Restlet mDispatcher;
+
+ public JsonRequestProvider(Restlet dispatcher, Class<V> clazz) {
+ super(clazz);
+
+ mDispatcher = dispatcher;
+ }
+
+ public JsonRequestProvider(Restlet dispatcher, JavaType type) {
+ super(type);
+
+ mDispatcher = dispatcher;
+ }
+
+ @Override
+ public V get(String url) throws IOException {
+ Request request = new Request(Method.GET, url);
+ Response response = mDispatcher.handle(request);
+ Representation representation = response.getEntity();
+
+ if (!response.getStatus().isSuccess()) {
+ if (representation != null) {
+ representation.release();
+ }
+
+ if (Status.CLIENT_ERROR_NOT_FOUND.equals(response.getStatus())) {
+ throw new NotFoundException("Could not get object. " + response.getStatus());
+ } else {
+ throw new IOException("Could not get object. " + response.getStatus());
+ }
+ }
+
+ return decode(representation.getText());
+ }
+
+ @Override
+ public void put(String url, V obj) throws IOException {
+ final Request request = new Request(Method.PUT, url);
+ request.setEntity(new StringRepresentation(encode(obj)));
+
+ final Response response = mDispatcher.handle(request);
+
+ if (!response.getStatus().isSuccess()) {
+ throw new IOException("Could not put object. " + response.getStatus());
+ }
+ }
+
+ /**
+ * Variant of put() which makes a POST request to the url.
+ *
+ * This method may eventually be incorporated into Provider for
+ * creating new objects with auto-generated IDs.
+ *
+ * @param url The url to make the request to.
+ * @param obj The post to post.
+ * @throws IOException on failure.
+ */
+ public void post(String url, V obj) throws IOException {
+ final Request request = new Request(Method.POST, url);
+ request.setEntity(new StringRepresentation(encode(obj)));
+
+ final Response response = mDispatcher.handle(request);
+
+ if (!response.getStatus().isSuccess()) {
+ throw new IOException("Could not put object. " + response.getStatus());
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/LoginFormAuthenticator.java b/src/main/java/com/p4square/grow/frontend/LoginFormAuthenticator.java
new file mode 100644
index 0000000..21c9097
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/LoginFormAuthenticator.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Form;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.security.Authenticator;
+import org.restlet.security.Verifier;
+
+/**
+ * LoginFormAuthenticator changes
+ *
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class LoginFormAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(LoginFormAuthenticator.class);
+
+ private final Verifier mVerifier;
+
+ private String mLoginPage = "/login.html";
+ private String mLoginPostUrl = "/authenticate";
+ private String mDefaultRedirect = "/index.html";
+
+ public LoginFormAuthenticator(Context context, boolean optional, Verifier verifier) {
+ super(context, false, optional, null);
+
+ mVerifier = verifier;
+ }
+
+ public void setLoginFormUrl(String url) {
+ mLoginPage = url;
+ }
+
+ public void setLoginPostUrl(String url) {
+ mLoginPostUrl = url;
+ }
+
+ public void setDefaultPage(String url) {
+ mDefaultRedirect = url;
+ }
+
+ @Override
+ protected int beforeHandle(Request request, Response response) {
+ if (!isLoginAttempt(request) && request.getClientInfo().isAuthenticated()) {
+ // TODO: Logout
+ LOG.debug("Already authenticated. Skipping");
+ return CONTINUE;
+
+ } else {
+ return super.beforeHandle(request, response);
+ }
+ }
+
+
+ @Override
+ protected boolean authenticate(Request request, Response response) {
+ boolean isLoginAttempt = isLoginAttempt(request);
+
+ Form query = request.getOriginalRef().getQueryAsForm();
+ String redirect = query.getFirstValue("redirect");
+ if (redirect == null || redirect.length() == 0) {
+ if (isLoginAttempt) {
+ redirect = mDefaultRedirect;
+ } else {
+ redirect = request.getResourceRef().getPath();
+ }
+ }
+
+ boolean authenticationFailed = false;
+
+ if (isLoginAttempt) {
+ LOG.debug("Attempting authentication");
+
+ // Process login form
+ final Form form = new Form(request.getEntity());
+ final String email = form.getFirstValue("email");
+ final String password = form.getFirstValue("password");
+
+ boolean authenticated = false;
+
+ if (email != null && !"".equals(email) &&
+ password != null && !"".equals(password)) {
+
+ LOG.debug("Got login request from " + email);
+
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, email, password.toCharArray()));
+
+ // We expect the verifier to setup the User object.
+ int result = mVerifier.verify(request, response);
+ if (result == Verifier.RESULT_VALID) {
+ return true;
+ }
+ }
+
+ authenticationFailed = true;
+ }
+
+ if (!isOptional() || authenticationFailed) {
+ Reference ref = new Reference(mLoginPage);
+ ref.addQueryParameter("redirect", redirect);
+
+ if (authenticationFailed) {
+ ref.addQueryParameter("retry", "t");
+ }
+
+ LOG.debug("Redirecting to " + ref);
+ response.redirectSeeOther(ref.toString());
+ }
+ LOG.debug("Failing authentication.");
+ return false;
+ }
+
+ @Override
+ protected int authenticated(Request request, Response response) {
+ super.authenticated(request, response);
+
+ Form query = request.getOriginalRef().getQueryAsForm();
+ String redirect = query.getFirstValue("redirect");
+ if (redirect == null || redirect.length() == 0) {
+ redirect = mDefaultRedirect;
+ }
+
+ // TODO: Ensure redirect is a relative url.
+ LOG.debug("Redirecting to " + redirect);
+ response.redirectSeeOther(redirect);
+
+ return CONTINUE;
+ }
+
+ private boolean isLoginAttempt(Request request) {
+ String requestPath = request.getResourceRef().getPath();
+ return request.getMethod() == Method.POST && mLoginPostUrl.equals(requestPath);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/LoginPageResource.java b/src/main/java/com/p4square/grow/frontend/LoginPageResource.java
new file mode 100644
index 0000000..38eba07
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/LoginPageResource.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * LoginPageResource presents a login page template and processes the response.
+ * Upon successful authentication, the user is redirected to another page and
+ * a cookie is set.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class LoginPageResource extends FreeMarkerPageResource {
+ private static Logger cLog = Logger.getLogger(LoginPageResource.class);
+
+ private GrowFrontend mGrowFrontend;
+
+ private String mErrorMessage;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+
+ mErrorMessage = null;
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("pages/login.html.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ Map<String, Object> root = getRootObject();
+
+ Form query = getRequest().getOriginalRef().getQueryAsForm();
+ String redirect = query.getFirstValue("redirect");
+ root.put("redirect", redirect);
+ String retry = query.getFirstValue("retry");
+ if ("t".equals(retry)) {
+ root.put("errorMessage", "Invalid email or password.");
+ }
+
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ cLog.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/com/p4square/grow/frontend/LogoutResource.java b/src/main/java/com/p4square/grow/frontend/LogoutResource.java
new file mode 100644
index 0000000..e26dcb7
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/LogoutResource.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.resource.ServerResource;
+
+import com.p4square.session.Sessions;
+
+import com.p4square.grow.config.Config;
+
+/**
+ * This Resource removes a user's session and session cookies.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class LogoutResource extends ServerResource {
+ private Config mConfig;
+
+ @Override
+ protected void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+ }
+
+ @Override
+ protected Representation get() {
+ Sessions.getInstance().delete(getRequest(), getResponse());
+
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/index.html";
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/NewAccountResource.java b/src/main/java/com/p4square/grow/frontend/NewAccountResource.java
new file mode 100644
index 0000000..5c13017
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/NewAccountResource.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Map;
+
+import com.p4square.f1oauth.FellowshipOneIntegrationDriver;
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.f1oauth.F1Access;
+import com.p4square.restlet.oauth.OAuthException;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * This resource creates a new InFellowship account.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class NewAccountResource extends FreeMarkerPageResource {
+ private static Logger LOG = Logger.getLogger(NewAccountResource.class);
+
+ private GrowFrontend mGrowFrontend;
+ private F1Access mHelper;
+
+ private String mErrorMessage;
+
+ private String mLoginPageUrl;
+ private String mVerificationPage;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+
+ final IntegrationDriver driver = mGrowFrontend.getThirdPartyIntegrationFactory();
+ if (driver instanceof FellowshipOneIntegrationDriver) {
+ mHelper = ((FellowshipOneIntegrationDriver) driver).getF1Access();
+ } else {
+ LOG.error("NewAccountResource only works with F1!");
+ mHelper = null;
+ }
+
+ mErrorMessage = "";
+
+ mLoginPageUrl = mGrowFrontend.getConfig().getString("postAccountCreationPage",
+ getRequest().getRootRef().toString());
+ mVerificationPage = mGrowFrontend.getConfig().getString("dynamicRoot", "")
+ + "/verification.html";
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("pages/newaccount.html.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+
+ Map<String, Object> root = getRootObject();
+ if (mErrorMessage.length() > 0) {
+ root.put("errorMessage", mErrorMessage);
+ }
+
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ @Override
+ protected Representation post(Representation rep) {
+ if (mHelper == null) {
+ mErrorMessage += "F1 support is not enabled! ";
+ return get();
+ }
+
+ Form form = new Form(rep);
+
+ String firstname = form.getFirstValue("firstname");
+ String lastname = form.getFirstValue("lastname");
+ String email = form.getFirstValue("email");
+
+ if (isEmpty(firstname)) {
+ mErrorMessage += "First Name is a required field. ";
+ }
+ if (isEmpty(lastname)) {
+ mErrorMessage += "Last Name is a required field. ";
+ }
+ if (isEmpty(email)) {
+ mErrorMessage += "Email is a required field. ";
+ }
+
+ if (mErrorMessage.length() > 0) {
+ return get();
+ }
+
+ try {
+ if (!mHelper.createAccount(firstname, lastname, email, mLoginPageUrl)) {
+ mErrorMessage = "An account with that address already exists.";
+ return get();
+ }
+
+ getResponse().redirectSeeOther(mVerificationPage);
+ return new StringRepresentation("Redirecting to " + mVerificationPage);
+
+ } catch (OAuthException e) {
+ return new ErrorPage(e.getStatus().getDescription());
+ }
+ }
+
+ private boolean isEmpty(String s) {
+ return s == null || s.trim().length() == 0;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/NewBelieverResource.java b/src/main/java/com/p4square/grow/frontend/NewBelieverResource.java
new file mode 100644
index 0000000..8fe078a
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/NewBelieverResource.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import freemarker.template.Template;
+
+import org.restlet.data.CookieSetting;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * This resource displays the transitional page between chapters.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class NewBelieverResource extends FreeMarkerPageResource {
+ private static final Logger LOG = Logger.getLogger(NewBelieverResource.class);
+
+ public static final String COOKIE_NAME = "seeker";
+
+ private GrowFrontend mGrowFrontend;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ }
+
+ /**
+ * Display the New Believer page.
+ *
+ * The New Believer page creates a cookie to remember the user,
+ * explains what's going on, and then asks the user to go to the login
+ * page.
+ *
+ * When the user hits the {@link AccountRedirectResource} the cookie
+ * is read and the user is moved ahead to the training section.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("templates/newbeliever.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+
+ // Set the new believer cookie
+ CookieSetting cookie = new CookieSetting(COOKIE_NAME, "true");
+ cookie.setPath("/");
+ getRequest().getCookies().add(cookie);
+ getResponse().getCookieSettings().add(cookie);
+
+ return new TemplateRepresentation(t, getRootObject(), MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/NotFoundException.java b/src/main/java/com/p4square/grow/frontend/NotFoundException.java
new file mode 100644
index 0000000..dfa2a4c
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/NotFoundException.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+public class NotFoundException extends IOException {
+ public NotFoundException(final String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/ProgressReporter.java b/src/main/java/com/p4square/grow/frontend/ProgressReporter.java
new file mode 100644
index 0000000..2f36832
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/ProgressReporter.java
@@ -0,0 +1,30 @@
+package com.p4square.grow.frontend;
+
+import org.restlet.security.User;
+
+import java.util.Date;
+
+/**
+ * A ProgressReporter is used to record a User's progress in a Church Management System.
+ */
+public interface ProgressReporter {
+
+ /**
+ * Report that the User completed the assessment.
+ *
+ * @param user The user who completed the assessment.
+ * @param level The assessment level.
+ * @param date The completion date.
+ * @param results Result information (e.g. json of the results).
+ */
+ void reportAssessmentComplete(User user, String level, Date date, String results);
+
+ /**
+ * Report that the User completed the chapter.
+ *
+ * @param user The user who completed the chapter.
+ * @param chapter The chapter completed.
+ * @param date The completion date.
+ */
+ void reportChapterComplete(User user, String chapter, Date date);
+}
diff --git a/src/main/java/com/p4square/grow/frontend/SurveyPageResource.java b/src/main/java/com/p4square/grow/frontend/SurveyPageResource.java
new file mode 100644
index 0000000..3575fe3
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/SurveyPageResource.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.io.IOException;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.ClientException;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.grow.config.Config;
+import com.p4square.grow.model.Question;
+import com.p4square.grow.model.UserRecord;
+import com.p4square.grow.provider.DelegateProvider;
+import com.p4square.grow.provider.JsonEncodedProvider;
+import com.p4square.grow.provider.Provider;
+
+/**
+ * SurveyPageResource handles rendering the survey and processing user's answers.
+ *
+ * This resource expects the user to be authenticated and the ClientInfo User object
+ * to be populated. Each question is requested from the backend along with the
+ * user's previous answer. Each answer is sent to the backend and the user is redirected
+ * to the next question. After the last question the user is sent to his results.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SurveyPageResource extends FreeMarkerPageResource {
+ private static final Logger LOG = Logger.getLogger(SurveyPageResource.class);
+
+ private Config mConfig;
+ private Template mSurveyTemplate;
+ private JsonRequestClient mJsonClient;
+ private Provider<String, Question> mQuestionProvider;
+ private Provider<String, UserRecord> mUserRecordProvider;
+
+ // Fields pertaining to this request.
+ private String mQuestionId;
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+ mSurveyTemplate = growFrontend.getTemplate("templates/survey.ftl");
+ if (mSurveyTemplate == null) {
+ LOG.fatal("Could not find survey template.");
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ }
+
+ mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+ mQuestionProvider = new DelegateProvider<String, String, Question>(
+ new JsonRequestProvider<Question>(getContext().getClientDispatcher(),
+ Question.class)) {
+ @Override
+ public String makeKey(String questionId) {
+ return getBackendEndpoint() + "/assessment/question/" + questionId;
+ }
+ };
+
+ mUserRecordProvider = new DelegateProvider<String, String, UserRecord>(
+ new JsonRequestProvider<UserRecord>(getContext().getClientDispatcher(),
+ UserRecord.class)) {
+ @Override
+ public String makeKey(String userid) {
+ return getBackendEndpoint() + "/accounts/" + userid;
+ }
+ };
+
+ mQuestionId = getAttribute("questionId");
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Return a page with a survey question.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ // Get the current question.
+ if (mQuestionId == null) {
+ // Get user's current question
+ mQuestionId = getCurrentQuestionId();
+
+ if (mQuestionId != null) {
+ Question lastQuestion = getQuestion(mQuestionId);
+ return redirectToNextQuestion(lastQuestion, getAnswer(mQuestionId));
+ }
+ }
+
+ // If we don't have a current question, get the first one.
+ if (mQuestionId == null) {
+ mQuestionId = "first";
+ }
+
+ Question question = getQuestion(mQuestionId);
+ if (question == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return new ErrorPage("Could not find the question.");
+ }
+
+ // Set the real question id if a meta-id was used (i.e. first)
+ mQuestionId = question.getId();
+
+ // Get any previous answer to the question
+ String selectedAnswer = getAnswer(mQuestionId);
+
+ Map root = getRootObject();
+ root.put("question", question);
+ root.put("selectedAnswerId", selectedAnswer);
+
+ // Get the question count and compute progress
+ {
+ JsonResponse response = backendGet("/assessment/question/count");
+ if (response.getStatus().isSuccess()) {
+ Map countData = response.getMap();
+ if (countData != null) {
+ response = backendGet("/accounts/" + mUserId + "/assessment");
+ if (response.getStatus().isSuccess()) {
+ Integer completed = (Integer) response.getMap().get("totalAnswers");
+ Integer total = (Integer) countData.get("count");
+
+ if (completed != null && total != null && total != 0) {
+ root.put("percentComplete", String.valueOf((int) (100.0 * completed) / total));
+ }
+ }
+ }
+ }
+ }
+
+ return new TemplateRepresentation(mSurveyTemplate, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ /**
+ * Record a survey answer and redirect to the next question.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ final Form form = new Form(entity);
+ final String answerId = form.getFirstValue("answer");
+ final String direction = form.getFirstValue("direction");
+ boolean justGoBack = false; // FIXME: Ugly hack
+
+ if (mQuestionId == null || answerId == null || answerId.length() == 0) {
+ if ("previous".equals(direction)) {
+ // Just go back
+ justGoBack = true;
+
+ } else {
+ // Something is wrong.
+ setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+ return new ErrorPage("Question or answer messing.");
+ }
+ }
+
+ try {
+ // Find the question
+ Question question = getQuestion(mQuestionId);
+ if (question == null) {
+ // User is answering a question which doesn't exist
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return new ErrorPage("Question not found.");
+ }
+
+ // Store answer
+ if (!justGoBack) {
+ Map<String, String> answer = new HashMap<String, String>();
+ answer.put("answerId", answerId);
+ JsonResponse response = backendPut("/accounts/" + mUserId +
+ "/assessment/answers/" + mQuestionId, answer);
+
+ if (!response.getStatus().isSuccess()) {
+ // Something went wrong talking to the backend, error out.
+ LOG.fatal("Error recording survey answer " + response.getStatus());
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.BACKEND_ERROR;
+ }
+ }
+
+ // Find the next question or finish the assessment.
+ if ("previous".equals(direction)) {
+ return redirectToPreviousQuestion(question);
+
+ } else {
+ return redirectToNextQuestion(question, answerId);
+ }
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ private Question getQuestion(String id) {
+ try {
+ return mQuestionProvider.get(id);
+
+ } catch (IOException e) {
+ LOG.warn("Error fetching question.", e);
+ return null;
+ }
+ }
+
+ private String getAnswer(String questionId) {
+ try {
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/assessment/answers/" + questionId);
+ if (response.getStatus().isSuccess()) {
+ return (String) response.getMap().get("answerId");
+ }
+
+ } catch (ClientException e) {
+ LOG.warn("Error fetching answer to question " + questionId, e);
+ }
+
+ return null;
+ }
+
+ private Representation redirectToNextQuestion(Question question, String answerid) {
+ String nextQuestionId = question.getNextQuestion(answerid);
+
+ if (nextQuestionId == null) {
+ // Just finished the last question. Update the user's account
+ try {
+ UserRecord account = null;
+ try {
+ account = mUserRecordProvider.get(mUserId);
+ } catch (NotFoundException e) {
+ // User record doesn't exist, so create a new one.
+ account = new UserRecord(getRequest().getClientInfo().getUser());
+ }
+ account.setLanding("training");
+ mUserRecordProvider.put(mUserId, account);
+ } catch (IOException e) {
+ LOG.warn("IOException updating landing for " + mUserId, e);
+ }
+
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/assessment/results";
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+ return redirectToQuestion(nextQuestionId);
+ }
+
+ private Representation redirectToPreviousQuestion(Question question) {
+ String nextQuestionId = question.getPreviousQuestion();
+
+ if (nextQuestionId == null) {
+ nextQuestionId = (String) question.getId();
+ }
+
+ return redirectToQuestion(nextQuestionId);
+ }
+
+ private Representation redirectToQuestion(String id) {
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/assessment/question/" + id;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+ private String getCurrentQuestionId() {
+ String id = null;
+ try {
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/assessment");
+
+ if (response.getStatus().isSuccess()) {
+ return (String) response.getMap().get("lastAnswered");
+
+ } else {
+ LOG.warn("Failed to get assessment results: " + response.getStatus());
+ }
+
+ } catch (ClientException e) {
+ LOG.error("Exception getting assessment results.", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ LOG.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ final Status status = response.getStatus();
+ if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+ LOG.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString());
+ }
+
+ return response;
+ }
+
+ protected JsonResponse backendPut(final String uri, final Map data) {
+ LOG.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)) {
+ LOG.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString());
+ }
+
+ return response;
+ }
+}
diff --git a/src/main/java/com/p4square/grow/frontend/TrainingPageResource.java b/src/main/java/com/p4square/grow/frontend/TrainingPageResource.java
new file mode 100644
index 0000000..a1e7789
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/TrainingPageResource.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.CookieSetting;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+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 {
+ private static final Logger LOG = Logger.getLogger(TrainingPageResource.class);
+
+ private static final String[] CHAPTERS = { "introduction", "seeker", "believer", "disciple", "teacher", "leader" };
+ private static final Comparator<Map<String, Object>> VIDEO_COMPARATOR = new Comparator<Map<String, Object>>() {
+ @Override
+ public int compare(Map<String, Object> left, Map<String, Object> right) {
+ String leftNumberStr = (String) left.get("number");
+ String rightNumberStr = (String) right.get("number");
+
+ if (leftNumberStr == null || rightNumberStr == null) {
+ return -1;
+ }
+
+ double leftNumber = Double.valueOf(leftNumberStr);
+ double rightNumber = Double.valueOf(rightNumberStr);
+
+ return Double.compare(leftNumber, rightNumber);
+ }
+ };
+
+ private Config mConfig;
+ private Template mTrainingTemplate;
+ private JsonRequestClient mJsonClient;
+
+ private Provider<String, TrainingRecord> mTrainingRecordProvider;
+ private FeedData mFeedData;
+
+ // Fields pertaining to this request.
+ protected String mChapter;
+ protected String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+ mTrainingTemplate = growFrontend.getTemplate("templates/training.ftl");
+ if (mTrainingTemplate == null) {
+ LOG.fatal("Could not find training template.");
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ }
+
+ 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";
+ }
+ };
+
+ mFeedData = new FeedData(getContext(), mConfig);
+
+ mChapter = getAttribute("chapter");
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Return a page of videos.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ // Get the training summary
+ 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();
+ Map<String, Boolean> allowedChapters = new LinkedHashMap<String, Boolean>();
+
+ // The user is not allowed to view chapters after his highest completed chapter.
+ // In this loop we find which chapters are allowed and check if the user tried
+ // to skip ahead.
+ boolean allowUserToSkip = mConfig.getBoolean("allowUserToSkip", false) || getQueryValue("magicskip") != null;
+ String defaultChapter = null;
+ boolean userTriedToSkip = false;
+ int overallProgress = 0;
+
+ boolean foundRequired = false;
+ for (String chapterId : getChaptersInOrder()) {
+ boolean allowed = true;
+
+ Boolean completed = chapters.get(chapterId);
+ if (completed != null) {
+ if (!foundRequired) {
+ if (!completed) {
+ // The first incomplete chapter is the highest allowed chapter.
+ foundRequired = true;
+ defaultChapter = chapterId;
+ }
+
+ } else {
+ allowed = allowUserToSkip;
+
+ if (!allowUserToSkip && chapterId.equals(mChapter)) {
+ userTriedToSkip = true;
+ }
+ }
+
+ allowedChapters.put(chapterId, allowed);
+
+ if (completed) {
+ overallProgress++;
+ }
+ }
+ }
+
+ // Overall progress is the percentage of chapters complete
+ overallProgress = (int) ((double) overallProgress / getChaptersInOrder().length * 100);
+
+ if (defaultChapter == null) {
+ // Everything is completed... send them back to introduction.
+ defaultChapter = "introduction";
+ }
+
+ if (mChapter == null || userTriedToSkip) {
+ // No chapter was specified or the user tried to skip ahead.
+ // Either case, redirect.
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/training/" + defaultChapter;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
+ }
+
+
+ // Get videos for the chapter.
+ List<Map<String, Object>> videos = null;
+ {
+ JsonResponse response = backendGet("/training/" + mChapter);
+ if (!response.getStatus().isSuccess()) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+ videos = (List<Map<String, Object>>) response.getMap().get("videos");
+ Collections.sort(videos, VIDEO_COMPARATOR);
+ }
+
+ // Mark the completed videos as completed
+ int chapterProgress = 0;
+ for (Map<String, Object> video : videos) {
+ boolean completed = false;
+ VideoRecord record = playlist.find((String) video.get("id"));
+ LOG.info("VideoId: " + video.get("id"));
+ if (record != null) {
+ LOG.info("VideoRecord: " + record.getComplete());
+ completed = record.getComplete();
+ }
+ video.put("completed", completed);
+
+ if (completed) {
+ chapterProgress++;
+ }
+ }
+ chapterProgress = chapterProgress * 100 / videos.size();
+
+ Map root = getRootObject();
+ root.put("chapter", mChapter);
+ root.put("chapters", allowedChapters.keySet());
+ root.put("isChapterAllowed", allowedChapters);
+ root.put("chapterProgress", chapterProgress);
+ root.put("overallProgress", overallProgress);
+ root.put("videos", videos);
+ root.put("allowUserToSkip", allowUserToSkip);
+
+ // Determine if we should show the feed.
+ boolean showfeed = true;
+
+ // Don't show the feed if the topic isn't allowed.
+ if (!FeedData.TOPICS.contains(mChapter)) {
+ showfeed = false;
+ }
+
+ root.put("showfeed", showfeed);
+ if (showfeed) {
+ root.put("feeddata", mFeedData);
+ }
+
+ return new TemplateRepresentation(mTrainingTemplate, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ /**
+ * This method returns a list of chapters in the correct order.
+ */
+ protected String[] getChaptersInOrder() {
+ return CHAPTERS;
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ LOG.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ final Status status = response.getStatus();
+ if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+ LOG.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString());
+ }
+
+ return response;
+ }
+
+}
diff --git a/src/main/java/com/p4square/grow/frontend/VideosResource.java b/src/main/java/com/p4square/grow/frontend/VideosResource.java
new file mode 100644
index 0000000..2099a77
--- /dev/null
+++ b/src/main/java/com/p4square/grow/frontend/VideosResource.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
+
+import com.p4square.grow.config.Config;
+
+/**
+ * VideosResource returns JSON blobs with video information and records watched
+ * videos.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class VideosResource extends ServerResource {
+ private static Logger cLog = Logger.getLogger(VideosResource.class);
+
+ private Config mConfig;
+ private JsonRequestClient mJsonClient;
+
+ // Fields pertaining to this request.
+ private String mChapter;
+ private String mVideoId;
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+
+ mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+
+ mChapter = getAttribute("chapter");
+ mVideoId = getAttribute("videoId");
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Fetch a video record from the backend.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ JsonResponse response = backendGet("/training/" + mChapter + "/videos/" + mVideoId);
+
+ if (response.getStatus().isSuccess()) {
+ return new JacksonRepresentation<Map>(response.getMap());
+
+ } else {
+ setStatus(response.getStatus());
+ return null;
+ }
+
+ } catch (Exception e) {
+ cLog.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+ /**
+ * Mark a video as completed.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ Map<String, Object> data = new HashMap<String, Object>();
+ data.put("complete", "true");
+ JsonResponse response = backendPut("/accounts/" + mUserId + "/training/videos/" + mVideoId, data);
+
+ if (!response.getStatus().isSuccess()) {
+ // Something went wrong talking to the backend, error out.
+ cLog.fatal("Error recording completed video " + response.getStatus());
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.BACKEND_ERROR;
+ }
+
+ setStatus(Status.SUCCESS_NO_CONTENT);
+ return null;
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ cLog.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ 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;
+ }
+
+ 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;
+ }
+}