diff options
author | Jesse Morgan <jesse@jesterpm.net> | 2016-04-09 14:22:20 -0700 |
---|---|---|
committer | Jesse Morgan <jesse@jesterpm.net> | 2016-04-09 15:48:01 -0700 |
commit | 3102d8bce3426d9cf41aeaf201c360d342677770 (patch) | |
tree | 38c4f1e8828f9af9c4b77a173bee0d312b321698 /src/main/java/com/p4square/grow/frontend | |
parent | bbf907e51dfcf157bdee24dead1d531122aa25db (diff) |
Switching from Ivy+Ant to Maven.
Diffstat (limited to 'src/main/java/com/p4square/grow/frontend')
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; + } +} |