From 3102d8bce3426d9cf41aeaf201c360d342677770 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sat, 9 Apr 2016 14:22:20 -0700 Subject: Switching from Ivy+Ant to Maven. --- .gitignore | 3 +- .travis.yml | 2 - README.md | 23 +- build.xml | 62 -- ivy.xml | 42 -- ivysettings.xml | 20 - pom.xml | 226 +++++++ src/com/p4square/f1oauth/Attribute.java | 90 --- src/com/p4square/f1oauth/F1API.java | 56 -- src/com/p4square/f1oauth/F1Access.java | 594 ------------------ src/com/p4square/f1oauth/F1Exception.java | 15 - src/com/p4square/f1oauth/F1ProgressReporter.java | 57 -- src/com/p4square/f1oauth/F1User.java | 70 --- .../f1oauth/FellowshipOneIntegrationDriver.java | 55 -- .../p4square/f1oauth/SecondPartyAuthenticator.java | 52 -- src/com/p4square/f1oauth/SecondPartyVerifier.java | 72 --- src/com/p4square/fmfacade/FMFacade.java | 107 ---- .../p4square/fmfacade/FreeMarkerPageResource.java | 98 --- src/com/p4square/fmfacade/ftl/GetMethod.java | 94 --- .../p4square/fmfacade/json/ClientException.java | 20 - .../p4square/fmfacade/json/JsonRequestClient.java | 109 ---- src/com/p4square/fmfacade/json/JsonResponse.java | 87 --- src/com/p4square/grow/GrowProcessComponent.java | 166 ----- src/com/p4square/grow/backend/BackendVerifier.java | 92 --- .../p4square/grow/backend/CassandraGrowData.java | 172 ------ src/com/p4square/grow/backend/DynamoGrowData.java | 180 ------ src/com/p4square/grow/backend/GrowBackend.java | 211 ------- src/com/p4square/grow/backend/GrowData.java | 36 -- src/com/p4square/grow/backend/apiinfo.html | 41 -- .../backend/db/CassandraCollectionProvider.java | 109 ---- .../grow/backend/db/CassandraDatabase.java | 212 ------- src/com/p4square/grow/backend/db/CassandraKey.java | 34 - .../grow/backend/db/CassandraProviderImpl.java | 37 -- .../db/CassandraTrainingRecordProvider.java | 71 --- src/com/p4square/grow/backend/dynamo/DbTool.java | 481 -------------- .../dynamo/DynamoCollectionProviderImpl.java | 109 ---- .../grow/backend/dynamo/DynamoDatabase.java | 307 --------- .../p4square/grow/backend/dynamo/DynamoKey.java | 56 -- .../grow/backend/dynamo/DynamoProviderImpl.java | 37 -- .../grow/backend/feed/FeedDataProvider.java | 33 - .../p4square/grow/backend/feed/ThreadResource.java | 106 ---- .../p4square/grow/backend/feed/TopicResource.java | 117 ---- .../grow/backend/resources/AccountResource.java | 87 --- .../grow/backend/resources/BannerResource.java | 85 --- .../grow/backend/resources/SurveyResource.java | 115 ---- .../backend/resources/SurveyResultsResource.java | 253 -------- .../backend/resources/TrainingRecordResource.java | 235 ------- .../grow/backend/resources/TrainingResource.java | 97 --- src/com/p4square/grow/ccb/CCBProgressReporter.java | 104 ---- src/com/p4square/grow/ccb/CCBUser.java | 37 -- src/com/p4square/grow/ccb/CCBUserVerifier.java | 50 -- .../ChurchCommunityBuilderIntegrationDriver.java | 61 -- src/com/p4square/grow/ccb/CustomFieldCache.java | 126 ---- src/com/p4square/grow/ccb/MonitoredCCBAPI.java | 96 --- src/com/p4square/grow/config/Config.java | 203 ------ .../grow/frontend/AccountRedirectResource.java | 113 ---- .../grow/frontend/AssessmentResetPage.java | 99 --- .../grow/frontend/AssessmentResultsPage.java | 145 ----- .../grow/frontend/AuthenticatedResource.java | 18 - .../grow/frontend/ChapterCompletePage.java | 209 ------- src/com/p4square/grow/frontend/ErrorPage.java | 77 --- src/com/p4square/grow/frontend/FeedData.java | 105 ---- src/com/p4square/grow/frontend/FeedResource.java | 101 --- .../frontend/GroupLeaderTrainingPageResource.java | 26 - src/com/p4square/grow/frontend/GrowFrontend.java | 230 ------- .../p4square/grow/frontend/IntegrationDriver.java | 26 - .../grow/frontend/JsonRequestProvider.java | 96 --- .../grow/frontend/LoginFormAuthenticator.java | 146 ----- .../p4square/grow/frontend/LoginPageResource.java | 77 --- src/com/p4square/grow/frontend/LogoutResource.java | 40 -- .../p4square/grow/frontend/NewAccountResource.java | 135 ---- .../grow/frontend/NewBelieverResource.java | 72 --- .../p4square/grow/frontend/NotFoundException.java | 13 - .../p4square/grow/frontend/ProgressReporter.java | 30 - .../p4square/grow/frontend/SurveyPageResource.java | 343 ---------- .../grow/frontend/TrainingPageResource.java | 268 -------- src/com/p4square/grow/frontend/VideosResource.java | 133 ---- src/com/p4square/grow/model/Answer.java | 142 ----- src/com/p4square/grow/model/Banner.java | 20 - src/com/p4square/grow/model/Chapter.java | 112 ---- src/com/p4square/grow/model/CircleQuestion.java | 89 --- src/com/p4square/grow/model/ImageQuestion.java | 24 - src/com/p4square/grow/model/Message.java | 103 --- src/com/p4square/grow/model/MessageThread.java | 60 -- src/com/p4square/grow/model/Playlist.java | 192 ------ src/com/p4square/grow/model/Point.java | 79 --- src/com/p4square/grow/model/QuadQuestion.java | 89 --- src/com/p4square/grow/model/QuadScoringEngine.java | 49 -- src/com/p4square/grow/model/Question.java | 165 ----- src/com/p4square/grow/model/RecordedAnswer.java | 34 - src/com/p4square/grow/model/Score.java | 119 ---- src/com/p4square/grow/model/ScoringEngine.java | 26 - .../p4square/grow/model/SimpleScoringEngine.java | 26 - src/com/p4square/grow/model/SliderQuestion.java | 24 - .../p4square/grow/model/SliderScoringEngine.java | 35 -- src/com/p4square/grow/model/TextQuestion.java | 24 - src/com/p4square/grow/model/TrainingRecord.java | 49 -- src/com/p4square/grow/model/UserRecord.java | 183 ------ src/com/p4square/grow/model/VideoRecord.java | 85 --- .../p4square/grow/provider/CollectionProvider.java | 59 -- .../grow/provider/DelegateCollectionProvider.java | 69 --- .../p4square/grow/provider/DelegateProvider.java | 40 -- .../grow/provider/JsonEncodedProvider.java | 83 --- .../grow/provider/MapCollectionProvider.java | 74 --- src/com/p4square/grow/provider/MapProvider.java | 28 - src/com/p4square/grow/provider/Provider.java | 31 - .../grow/provider/ProvidesAssessments.java | 20 - .../p4square/grow/provider/ProvidesQuestions.java | 19 - .../p4square/grow/provider/ProvidesStrings.java | 19 - .../grow/provider/ProvidesTrainingRecords.java | 27 - .../grow/provider/ProvidesUserRecords.java | 19 - src/com/p4square/grow/provider/ProvidesVideos.java | 16 - .../grow/provider/TrainingRecordProvider.java | 41 -- src/com/p4square/grow/tools/AssessmentStats.java | 218 ------- .../p4square/grow/tools/AttributeBackfillTool.java | 268 -------- src/com/p4square/grow/tools/AttributeTool.java | 184 ------ src/com/p4square/restlet/metrics/MetricRouter.java | 61 -- .../restlet/metrics/MetricsApplication.java | 43 -- .../p4square/restlet/metrics/MetricsResource.java | 32 - .../p4square/restlet/oauth/OAuthAuthenticator.java | 95 --- .../restlet/oauth/OAuthAuthenticatorHelper.java | 177 ------ src/com/p4square/restlet/oauth/OAuthException.java | 25 - src/com/p4square/restlet/oauth/OAuthHelper.java | 149 ----- src/com/p4square/restlet/oauth/OAuthUser.java | 50 -- src/com/p4square/restlet/oauth/Token.java | 52 -- src/com/p4square/session/Session.java | 59 -- src/com/p4square/session/SessionAuthenticator.java | 36 -- .../session/SessionCheckingAuthenticator.java | 39 -- .../session/SessionCookieAuthenticator.java | 59 -- .../session/SessionCreatingAuthenticator.java | 46 -- src/com/p4square/session/Sessions.java | 155 ----- src/grow.properties | 15 - src/jetty-logging.properties | 1 - src/log4j.properties | 15 - src/main/java/com/p4square/f1oauth/Attribute.java | 90 +++ src/main/java/com/p4square/f1oauth/F1API.java | 56 ++ src/main/java/com/p4square/f1oauth/F1Access.java | 594 ++++++++++++++++++ .../java/com/p4square/f1oauth/F1Exception.java | 15 + .../com/p4square/f1oauth/F1ProgressReporter.java | 57 ++ src/main/java/com/p4square/f1oauth/F1User.java | 70 +++ .../f1oauth/FellowshipOneIntegrationDriver.java | 55 ++ .../p4square/f1oauth/SecondPartyAuthenticator.java | 52 ++ .../com/p4square/f1oauth/SecondPartyVerifier.java | 72 +++ src/main/java/com/p4square/fmfacade/FMFacade.java | 107 ++++ .../p4square/fmfacade/FreeMarkerPageResource.java | 98 +++ .../java/com/p4square/fmfacade/ftl/GetMethod.java | 94 +++ .../p4square/fmfacade/json/ClientException.java | 20 + .../p4square/fmfacade/json/JsonRequestClient.java | 109 ++++ .../com/p4square/fmfacade/json/JsonResponse.java | 87 +++ .../com/p4square/grow/GrowProcessComponent.java | 166 +++++ .../com/p4square/grow/backend/BackendVerifier.java | 92 +++ .../p4square/grow/backend/CassandraGrowData.java | 172 ++++++ .../com/p4square/grow/backend/DynamoGrowData.java | 180 ++++++ .../com/p4square/grow/backend/GrowBackend.java | 211 +++++++ .../java/com/p4square/grow/backend/GrowData.java | 36 ++ .../backend/db/CassandraCollectionProvider.java | 109 ++++ .../grow/backend/db/CassandraDatabase.java | 212 +++++++ .../com/p4square/grow/backend/db/CassandraKey.java | 34 + .../grow/backend/db/CassandraProviderImpl.java | 37 ++ .../db/CassandraTrainingRecordProvider.java | 71 +++ .../com/p4square/grow/backend/dynamo/DbTool.java | 481 ++++++++++++++ .../dynamo/DynamoCollectionProviderImpl.java | 109 ++++ .../grow/backend/dynamo/DynamoDatabase.java | 307 +++++++++ .../p4square/grow/backend/dynamo/DynamoKey.java | 56 ++ .../grow/backend/dynamo/DynamoProviderImpl.java | 37 ++ .../grow/backend/feed/FeedDataProvider.java | 33 + .../p4square/grow/backend/feed/ThreadResource.java | 106 ++++ .../p4square/grow/backend/feed/TopicResource.java | 117 ++++ .../grow/backend/resources/AccountResource.java | 87 +++ .../grow/backend/resources/BannerResource.java | 85 +++ .../grow/backend/resources/SurveyResource.java | 115 ++++ .../backend/resources/SurveyResultsResource.java | 253 ++++++++ .../backend/resources/TrainingRecordResource.java | 235 +++++++ .../grow/backend/resources/TrainingResource.java | 97 +++ .../com/p4square/grow/ccb/CCBProgressReporter.java | 104 ++++ src/main/java/com/p4square/grow/ccb/CCBUser.java | 37 ++ .../com/p4square/grow/ccb/CCBUserVerifier.java | 50 ++ .../ChurchCommunityBuilderIntegrationDriver.java | 61 ++ .../com/p4square/grow/ccb/CustomFieldCache.java | 126 ++++ .../com/p4square/grow/ccb/MonitoredCCBAPI.java | 96 +++ src/main/java/com/p4square/grow/config/Config.java | 203 ++++++ .../grow/frontend/AccountRedirectResource.java | 113 ++++ .../grow/frontend/AssessmentResetPage.java | 99 +++ .../grow/frontend/AssessmentResultsPage.java | 145 +++++ .../grow/frontend/AuthenticatedResource.java | 18 + .../grow/frontend/ChapterCompletePage.java | 209 +++++++ .../java/com/p4square/grow/frontend/ErrorPage.java | 77 +++ .../java/com/p4square/grow/frontend/FeedData.java | 105 ++++ .../com/p4square/grow/frontend/FeedResource.java | 101 +++ .../frontend/GroupLeaderTrainingPageResource.java | 26 + .../com/p4square/grow/frontend/GrowFrontend.java | 230 +++++++ .../p4square/grow/frontend/IntegrationDriver.java | 26 + .../grow/frontend/JsonRequestProvider.java | 96 +++ .../grow/frontend/LoginFormAuthenticator.java | 146 +++++ .../p4square/grow/frontend/LoginPageResource.java | 77 +++ .../com/p4square/grow/frontend/LogoutResource.java | 40 ++ .../p4square/grow/frontend/NewAccountResource.java | 135 ++++ .../grow/frontend/NewBelieverResource.java | 72 +++ .../p4square/grow/frontend/NotFoundException.java | 13 + .../p4square/grow/frontend/ProgressReporter.java | 30 + .../p4square/grow/frontend/SurveyPageResource.java | 343 ++++++++++ .../grow/frontend/TrainingPageResource.java | 268 ++++++++ .../com/p4square/grow/frontend/VideosResource.java | 133 ++++ src/main/java/com/p4square/grow/model/Answer.java | 142 +++++ src/main/java/com/p4square/grow/model/Banner.java | 20 + src/main/java/com/p4square/grow/model/Chapter.java | 112 ++++ .../com/p4square/grow/model/CircleQuestion.java | 89 +++ .../com/p4square/grow/model/ImageQuestion.java | 24 + src/main/java/com/p4square/grow/model/Message.java | 103 +++ .../com/p4square/grow/model/MessageThread.java | 60 ++ .../java/com/p4square/grow/model/Playlist.java | 192 ++++++ src/main/java/com/p4square/grow/model/Point.java | 79 +++ .../java/com/p4square/grow/model/QuadQuestion.java | 89 +++ .../com/p4square/grow/model/QuadScoringEngine.java | 49 ++ .../java/com/p4square/grow/model/Question.java | 165 +++++ .../com/p4square/grow/model/RecordedAnswer.java | 34 + src/main/java/com/p4square/grow/model/Score.java | 119 ++++ .../com/p4square/grow/model/ScoringEngine.java | 26 + .../p4square/grow/model/SimpleScoringEngine.java | 26 + .../com/p4square/grow/model/SliderQuestion.java | 24 + .../p4square/grow/model/SliderScoringEngine.java | 35 ++ .../java/com/p4square/grow/model/TextQuestion.java | 24 + .../com/p4square/grow/model/TrainingRecord.java | 49 ++ .../java/com/p4square/grow/model/UserRecord.java | 183 ++++++ .../java/com/p4square/grow/model/VideoRecord.java | 85 +++ .../p4square/grow/provider/CollectionProvider.java | 59 ++ .../grow/provider/DelegateCollectionProvider.java | 69 +++ .../p4square/grow/provider/DelegateProvider.java | 40 ++ .../grow/provider/JsonEncodedProvider.java | 83 +++ .../grow/provider/MapCollectionProvider.java | 74 +++ .../com/p4square/grow/provider/MapProvider.java | 28 + .../java/com/p4square/grow/provider/Provider.java | 31 + .../grow/provider/ProvidesAssessments.java | 20 + .../p4square/grow/provider/ProvidesQuestions.java | 19 + .../p4square/grow/provider/ProvidesStrings.java | 19 + .../grow/provider/ProvidesTrainingRecords.java | 27 + .../grow/provider/ProvidesUserRecords.java | 19 + .../com/p4square/grow/provider/ProvidesVideos.java | 16 + .../grow/provider/TrainingRecordProvider.java | 41 ++ .../com/p4square/grow/tools/AssessmentStats.java | 218 +++++++ .../p4square/grow/tools/AttributeBackfillTool.java | 268 ++++++++ .../com/p4square/grow/tools/AttributeTool.java | 184 ++++++ .../com/p4square/restlet/metrics/MetricRouter.java | 61 ++ .../restlet/metrics/MetricsApplication.java | 43 ++ .../p4square/restlet/metrics/MetricsResource.java | 32 + .../p4square/restlet/oauth/OAuthAuthenticator.java | 95 +++ .../restlet/oauth/OAuthAuthenticatorHelper.java | 177 ++++++ .../com/p4square/restlet/oauth/OAuthException.java | 25 + .../com/p4square/restlet/oauth/OAuthHelper.java | 149 +++++ .../java/com/p4square/restlet/oauth/OAuthUser.java | 50 ++ .../java/com/p4square/restlet/oauth/Token.java | 52 ++ src/main/java/com/p4square/session/Session.java | 59 ++ .../com/p4square/session/SessionAuthenticator.java | 36 ++ .../session/SessionCheckingAuthenticator.java | 39 ++ .../session/SessionCookieAuthenticator.java | 59 ++ .../session/SessionCreatingAuthenticator.java | 46 ++ src/main/java/com/p4square/session/Sessions.java | 155 +++++ .../com/p4square/grow/backend/apiinfo.html | 41 ++ src/main/resources/grow.properties | 15 + src/main/resources/jetty-logging.properties | 1 + src/main/resources/log4j.properties | 15 + .../resources/templates/macros/common-page.ftl | 29 + src/main/resources/templates/macros/common.ftl | 4 + src/main/resources/templates/macros/content.ftl | 7 + src/main/resources/templates/macros/hms.ftl | 25 + src/main/resources/templates/macros/noticebox.ftl | 10 + .../resources/templates/pages/assessment.html.ftl | 43 ++ .../resources/templates/pages/contact.html.ftl | 18 + .../templates/pages/deeper/believer.html.ftl | 51 ++ .../templates/pages/deeper/disciple.html.ftl | 49 ++ .../templates/pages/deeper/leader.html.ftl | 23 + .../templates/pages/deeper/seeker.html.ftl | 30 + .../templates/pages/deeper/teacher.html.ftl | 42 ++ src/main/resources/templates/pages/index.html.ftl | 50 ++ .../resources/templates/pages/learnmore.html.ftl | 107 ++++ src/main/resources/templates/pages/login.html.ftl | 41 ++ .../resources/templates/pages/newaccount.html.ftl | 27 + .../templates/pages/verification.html.ftl | 17 + src/main/resources/templates/pages/version.ftl | 1 + .../templates/templates/assessment-results.ftl | 34 + src/main/resources/templates/templates/banner.ftl | 6 + .../templates/templates/communityfeed.ftl | 41 ++ .../resources/templates/templates/deeperheader.ftl | 7 + src/main/resources/templates/templates/error.ftl | 17 + src/main/resources/templates/templates/footer.ftl | 16 + .../templates/templates/getstarted-button.ftl | 7 + .../resources/templates/templates/gitversion.ftl | 1 + src/main/resources/templates/templates/header.ftl | 15 + .../resources/templates/templates/index-hero.ftl | 8 + src/main/resources/templates/templates/nav.ftl | 21 + .../resources/templates/templates/newbeliever.ftl | 41 ++ .../templates/templates/question-circle.ftl | 23 + .../templates/templates/question-image.ftl | 17 + .../templates/templates/question-quad.ftl | 19 + .../templates/templates/question-slider.ftl | 18 + .../templates/templates/question-text.ftl | 17 + .../templates/templates/stage-complete.ftl | 58 ++ .../templates/templates/stage-teacher-forward.ftl | 47 ++ src/main/resources/templates/templates/survey.ftl | 53 ++ .../resources/templates/templates/training.ftl | 87 +++ src/main/resources/templates/utils/dump.ftl | 98 +++ src/main/webapp/WEB-INF/web.xml | 44 ++ src/main/webapp/error.html | 63 ++ src/main/webapp/favicon.ico | Bin 0 -> 4286 bytes src/main/webapp/images/02-a1-hover.jpg | Bin 0 -> 32906 bytes src/main/webapp/images/02-a1.jpg | Bin 0 -> 25513 bytes src/main/webapp/images/02-a2-hover.jpg | Bin 0 -> 28357 bytes src/main/webapp/images/02-a2.jpg | Bin 0 -> 21029 bytes src/main/webapp/images/02-a3-hover.jpg | Bin 0 -> 14410 bytes src/main/webapp/images/02-a3.jpg | Bin 0 -> 11069 bytes src/main/webapp/images/02-a4-hover.jpg | Bin 0 -> 29145 bytes src/main/webapp/images/02-a4.jpg | Bin 0 -> 21415 bytes src/main/webapp/images/02-a5-hover.jpg | Bin 0 -> 30555 bytes src/main/webapp/images/02-a5.jpg | Bin 0 -> 22376 bytes src/main/webapp/images/02-a6-hover.jpg | Bin 0 -> 30024 bytes src/main/webapp/images/02-a6.jpg | Bin 0 -> 21951 bytes src/main/webapp/images/08-a1-hover.jpg | Bin 0 -> 35555 bytes src/main/webapp/images/08-a1.jpg | Bin 0 -> 28795 bytes src/main/webapp/images/08-a2-hover.jpg | Bin 0 -> 36234 bytes src/main/webapp/images/08-a2.jpg | Bin 0 -> 30094 bytes src/main/webapp/images/08-a3-hover.jpg | Bin 0 -> 30761 bytes src/main/webapp/images/08-a3.jpg | Bin 0 -> 23219 bytes src/main/webapp/images/about-grow.png | Bin 0 -> 59408 bytes src/main/webapp/images/acts242.png | Bin 0 -> 10307 bytes src/main/webapp/images/close.png | Bin 0 -> 1303 bytes src/main/webapp/images/complete.png | Bin 0 -> 3559 bytes src/main/webapp/images/faux_right_column.png | Bin 0 -> 111 bytes src/main/webapp/images/foursquarechurchlogin.png | Bin 0 -> 6299 bytes src/main/webapp/images/foursquarelg.png | Bin 0 -> 5616 bytes src/main/webapp/images/foursquaresm.png | Bin 0 -> 1884 bytes src/main/webapp/images/grow-poster.png | Bin 0 -> 36091 bytes src/main/webapp/images/hero.png | Bin 0 -> 380869 bytes src/main/webapp/images/leadershipdev.png | Bin 0 -> 10233 bytes src/main/webapp/images/loginbg.png | Bin 0 -> 4906 bytes src/main/webapp/images/logo.png | Bin 0 -> 4017 bytes src/main/webapp/images/next.png | Bin 0 -> 1420 bytes src/main/webapp/images/noticeicon.png | Bin 0 -> 1230 bytes src/main/webapp/images/play.png | Bin 0 -> 1374 bytes src/main/webapp/images/previous.png | Bin 0 -> 1407 bytes src/main/webapp/images/quad.png | Bin 0 -> 12954 bytes src/main/webapp/images/quadselector.png | Bin 0 -> 963 bytes src/main/webapp/images/reply.png | Bin 0 -> 223 bytes src/main/webapp/images/slider.png | Bin 0 -> 1260 bytes src/main/webapp/images/videoimage.jpg | Bin 0 -> 16165 bytes src/main/webapp/notfound.html | 63 ++ src/main/webapp/scripts/growth.js | 315 ++++++++++ src/main/webapp/scripts/jquery-ui.js | 7 + src/main/webapp/scripts/jquery.min.js | 5 + src/main/webapp/style.css | 688 +++++++++++++++++++++ src/templates/macros/common-page.ftl | 29 - src/templates/macros/common.ftl | 4 - src/templates/macros/content.ftl | 7 - src/templates/macros/hms.ftl | 25 - src/templates/macros/noticebox.ftl | 10 - src/templates/pages/assessment.html.ftl | 43 -- src/templates/pages/contact.html.ftl | 18 - src/templates/pages/deeper/believer.html.ftl | 51 -- src/templates/pages/deeper/disciple.html.ftl | 49 -- src/templates/pages/deeper/leader.html.ftl | 23 - src/templates/pages/deeper/seeker.html.ftl | 30 - src/templates/pages/deeper/teacher.html.ftl | 42 -- src/templates/pages/index.html.ftl | 50 -- src/templates/pages/learnmore.html.ftl | 107 ---- src/templates/pages/login.html.ftl | 41 -- src/templates/pages/newaccount.html.ftl | 27 - src/templates/pages/verification.html.ftl | 17 - src/templates/pages/version.ftl | 1 - src/templates/templates/assessment-results.ftl | 34 - src/templates/templates/banner.ftl | 6 - src/templates/templates/communityfeed.ftl | 41 -- src/templates/templates/deeperheader.ftl | 7 - src/templates/templates/error.ftl | 17 - src/templates/templates/footer.ftl | 16 - src/templates/templates/getstarted-button.ftl | 7 - src/templates/templates/gitversion.ftl | 1 - src/templates/templates/header.ftl | 15 - src/templates/templates/index-hero.ftl | 8 - src/templates/templates/nav.ftl | 21 - src/templates/templates/newbeliever.ftl | 41 -- src/templates/templates/question-circle.ftl | 23 - src/templates/templates/question-image.ftl | 17 - src/templates/templates/question-quad.ftl | 19 - src/templates/templates/question-slider.ftl | 18 - src/templates/templates/question-text.ftl | 17 - src/templates/templates/stage-complete.ftl | 58 -- src/templates/templates/stage-teacher-forward.ftl | 47 -- src/templates/templates/survey.ftl | 53 -- src/templates/templates/training.ftl | 87 --- src/templates/utils/dump.ftl | 98 --- .../grow/backend/resources/ResourceTestBase.java | 101 +++ .../resources/TrainingRecordResourceTest.java | 142 +++++ .../p4square/grow/ccb/CCBProgressReporterTest.java | 231 +++++++ .../com/p4square/grow/ccb/CCBUserVerifierTest.java | 139 +++++ .../p4square/grow/ccb/CustomFieldCacheTest.java | 241 ++++++++ .../java/com/p4square/grow/config/ConfigTest.java | 62 ++ .../java/com/p4square/grow/model/AnswerTest.java | 137 ++++ .../p4square/grow/model/CircleQuestionTest.java | 92 +++ .../com/p4square/grow/model/ImageQuestionTest.java | 74 +++ .../java/com/p4square/grow/model/PlaylistTest.java | 154 +++++ .../java/com/p4square/grow/model/PointTest.java | 129 ++++ .../com/p4square/grow/model/QuadQuestionTest.java | 92 +++ .../p4square/grow/model/QuadScoringEngineTest.java | 85 +++ .../java/com/p4square/grow/model/QuestionTest.java | 80 +++ .../java/com/p4square/grow/model/ScoreTest.java | 111 ++++ .../grow/model/SimpleScoringEngineTest.java | 86 +++ .../p4square/grow/model/SliderQuestionTest.java | 64 ++ .../grow/model/SliderScoringEngineTest.java | 161 +++++ .../com/p4square/grow/model/TextQuestionTest.java | 74 +++ .../p4square/grow/model/TrainingRecordTest.java | 85 +++ .../com/p4square/grow/config/ConfigTest.properties | 16 + .../com/p4square/grow/model/trainingrecord.json | 18 + .../grow/backend/resources/ResourceTestBase.java | 101 --- .../resources/TrainingRecordResourceTest.java | 142 ----- .../p4square/grow/ccb/CCBProgressReporterTest.java | 231 ------- tst/com/p4square/grow/ccb/CCBUserVerifierTest.java | 139 ----- .../p4square/grow/ccb/CustomFieldCacheTest.java | 241 -------- tst/com/p4square/grow/config/ConfigTest.java | 62 -- tst/com/p4square/grow/config/ConfigTest.properties | 16 - tst/com/p4square/grow/model/AnswerTest.java | 137 ---- .../p4square/grow/model/CircleQuestionTest.java | 92 --- tst/com/p4square/grow/model/ImageQuestionTest.java | 74 --- tst/com/p4square/grow/model/PlaylistTest.java | 154 ----- tst/com/p4square/grow/model/PointTest.java | 129 ---- tst/com/p4square/grow/model/QuadQuestionTest.java | 92 --- .../p4square/grow/model/QuadScoringEngineTest.java | 85 --- tst/com/p4square/grow/model/QuestionTest.java | 80 --- tst/com/p4square/grow/model/ScoreTest.java | 111 ---- .../grow/model/SimpleScoringEngineTest.java | 86 --- .../p4square/grow/model/SliderQuestionTest.java | 64 -- .../grow/model/SliderScoringEngineTest.java | 161 ----- tst/com/p4square/grow/model/TextQuestionTest.java | 74 --- .../p4square/grow/model/TrainingRecordTest.java | 85 --- tst/com/p4square/grow/model/trainingrecord.json | 18 - web/WEB-INF/web.xml | 44 -- web/error.html | 63 -- web/favicon.ico | Bin 4286 -> 0 bytes web/images/02-a1-hover.jpg | Bin 32906 -> 0 bytes web/images/02-a1.jpg | Bin 25513 -> 0 bytes web/images/02-a2-hover.jpg | Bin 28357 -> 0 bytes web/images/02-a2.jpg | Bin 21029 -> 0 bytes web/images/02-a3-hover.jpg | Bin 14410 -> 0 bytes web/images/02-a3.jpg | Bin 11069 -> 0 bytes web/images/02-a4-hover.jpg | Bin 29145 -> 0 bytes web/images/02-a4.jpg | Bin 21415 -> 0 bytes web/images/02-a5-hover.jpg | Bin 30555 -> 0 bytes web/images/02-a5.jpg | Bin 22376 -> 0 bytes web/images/02-a6-hover.jpg | Bin 30024 -> 0 bytes web/images/02-a6.jpg | Bin 21951 -> 0 bytes web/images/08-a1-hover.jpg | Bin 35555 -> 0 bytes web/images/08-a1.jpg | Bin 28795 -> 0 bytes web/images/08-a2-hover.jpg | Bin 36234 -> 0 bytes web/images/08-a2.jpg | Bin 30094 -> 0 bytes web/images/08-a3-hover.jpg | Bin 30761 -> 0 bytes web/images/08-a3.jpg | Bin 23219 -> 0 bytes web/images/about-grow.png | Bin 59408 -> 0 bytes web/images/acts242.png | Bin 10307 -> 0 bytes web/images/close.png | Bin 1303 -> 0 bytes web/images/complete.png | Bin 3559 -> 0 bytes web/images/faux_right_column.png | Bin 111 -> 0 bytes web/images/foursquarechurchlogin.png | Bin 6299 -> 0 bytes web/images/foursquarelg.png | Bin 5616 -> 0 bytes web/images/foursquaresm.png | Bin 1884 -> 0 bytes web/images/grow-poster.png | Bin 36091 -> 0 bytes web/images/hero.png | Bin 380869 -> 0 bytes web/images/leadershipdev.png | Bin 10233 -> 0 bytes web/images/loginbg.png | Bin 4906 -> 0 bytes web/images/logo.png | Bin 4017 -> 0 bytes web/images/next.png | Bin 1420 -> 0 bytes web/images/noticeicon.png | Bin 1230 -> 0 bytes web/images/play.png | Bin 1374 -> 0 bytes web/images/previous.png | Bin 1407 -> 0 bytes web/images/quad.png | Bin 12954 -> 0 bytes web/images/quadselector.png | Bin 963 -> 0 bytes web/images/reply.png | Bin 223 -> 0 bytes web/images/slider.png | Bin 1260 -> 0 bytes web/images/videoimage.jpg | Bin 16165 -> 0 bytes web/notfound.html | 63 -- web/scripts/growth.js | 315 ---------- web/scripts/jquery-ui.js | 7 - web/scripts/jquery.min.js | 5 - web/style.css | 688 --------------------- 481 files changed, 17308 insertions(+), 17212 deletions(-) delete mode 100644 build.xml delete mode 100644 ivy.xml delete mode 100644 ivysettings.xml create mode 100644 pom.xml delete mode 100644 src/com/p4square/f1oauth/Attribute.java delete mode 100644 src/com/p4square/f1oauth/F1API.java delete mode 100644 src/com/p4square/f1oauth/F1Access.java delete mode 100644 src/com/p4square/f1oauth/F1Exception.java delete mode 100644 src/com/p4square/f1oauth/F1ProgressReporter.java delete mode 100644 src/com/p4square/f1oauth/F1User.java delete mode 100644 src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java delete mode 100644 src/com/p4square/f1oauth/SecondPartyAuthenticator.java delete mode 100644 src/com/p4square/f1oauth/SecondPartyVerifier.java delete mode 100644 src/com/p4square/fmfacade/FMFacade.java delete mode 100644 src/com/p4square/fmfacade/FreeMarkerPageResource.java delete mode 100644 src/com/p4square/fmfacade/ftl/GetMethod.java delete mode 100644 src/com/p4square/fmfacade/json/ClientException.java delete mode 100644 src/com/p4square/fmfacade/json/JsonRequestClient.java delete mode 100644 src/com/p4square/fmfacade/json/JsonResponse.java delete mode 100644 src/com/p4square/grow/GrowProcessComponent.java delete mode 100644 src/com/p4square/grow/backend/BackendVerifier.java delete mode 100644 src/com/p4square/grow/backend/CassandraGrowData.java delete mode 100644 src/com/p4square/grow/backend/DynamoGrowData.java delete mode 100644 src/com/p4square/grow/backend/GrowBackend.java delete mode 100644 src/com/p4square/grow/backend/GrowData.java delete mode 100644 src/com/p4square/grow/backend/apiinfo.html delete mode 100644 src/com/p4square/grow/backend/db/CassandraCollectionProvider.java delete mode 100644 src/com/p4square/grow/backend/db/CassandraDatabase.java delete mode 100644 src/com/p4square/grow/backend/db/CassandraKey.java delete mode 100644 src/com/p4square/grow/backend/db/CassandraProviderImpl.java delete mode 100644 src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java delete mode 100644 src/com/p4square/grow/backend/dynamo/DbTool.java delete mode 100644 src/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java delete mode 100644 src/com/p4square/grow/backend/dynamo/DynamoDatabase.java delete mode 100644 src/com/p4square/grow/backend/dynamo/DynamoKey.java delete mode 100644 src/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java delete mode 100644 src/com/p4square/grow/backend/feed/FeedDataProvider.java delete mode 100644 src/com/p4square/grow/backend/feed/ThreadResource.java delete mode 100644 src/com/p4square/grow/backend/feed/TopicResource.java delete mode 100644 src/com/p4square/grow/backend/resources/AccountResource.java delete mode 100644 src/com/p4square/grow/backend/resources/BannerResource.java delete mode 100644 src/com/p4square/grow/backend/resources/SurveyResource.java delete mode 100644 src/com/p4square/grow/backend/resources/SurveyResultsResource.java delete mode 100644 src/com/p4square/grow/backend/resources/TrainingRecordResource.java delete mode 100644 src/com/p4square/grow/backend/resources/TrainingResource.java delete mode 100644 src/com/p4square/grow/ccb/CCBProgressReporter.java delete mode 100644 src/com/p4square/grow/ccb/CCBUser.java delete mode 100644 src/com/p4square/grow/ccb/CCBUserVerifier.java delete mode 100644 src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java delete mode 100644 src/com/p4square/grow/ccb/CustomFieldCache.java delete mode 100644 src/com/p4square/grow/ccb/MonitoredCCBAPI.java delete mode 100644 src/com/p4square/grow/config/Config.java delete mode 100644 src/com/p4square/grow/frontend/AccountRedirectResource.java delete mode 100644 src/com/p4square/grow/frontend/AssessmentResetPage.java delete mode 100644 src/com/p4square/grow/frontend/AssessmentResultsPage.java delete mode 100644 src/com/p4square/grow/frontend/AuthenticatedResource.java delete mode 100644 src/com/p4square/grow/frontend/ChapterCompletePage.java delete mode 100644 src/com/p4square/grow/frontend/ErrorPage.java delete mode 100644 src/com/p4square/grow/frontend/FeedData.java delete mode 100644 src/com/p4square/grow/frontend/FeedResource.java delete mode 100644 src/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java delete mode 100644 src/com/p4square/grow/frontend/GrowFrontend.java delete mode 100644 src/com/p4square/grow/frontend/IntegrationDriver.java delete mode 100644 src/com/p4square/grow/frontend/JsonRequestProvider.java delete mode 100644 src/com/p4square/grow/frontend/LoginFormAuthenticator.java delete mode 100644 src/com/p4square/grow/frontend/LoginPageResource.java delete mode 100644 src/com/p4square/grow/frontend/LogoutResource.java delete mode 100644 src/com/p4square/grow/frontend/NewAccountResource.java delete mode 100644 src/com/p4square/grow/frontend/NewBelieverResource.java delete mode 100644 src/com/p4square/grow/frontend/NotFoundException.java delete mode 100644 src/com/p4square/grow/frontend/ProgressReporter.java delete mode 100644 src/com/p4square/grow/frontend/SurveyPageResource.java delete mode 100644 src/com/p4square/grow/frontend/TrainingPageResource.java delete mode 100644 src/com/p4square/grow/frontend/VideosResource.java delete mode 100644 src/com/p4square/grow/model/Answer.java delete mode 100644 src/com/p4square/grow/model/Banner.java delete mode 100644 src/com/p4square/grow/model/Chapter.java delete mode 100644 src/com/p4square/grow/model/CircleQuestion.java delete mode 100644 src/com/p4square/grow/model/ImageQuestion.java delete mode 100644 src/com/p4square/grow/model/Message.java delete mode 100644 src/com/p4square/grow/model/MessageThread.java delete mode 100644 src/com/p4square/grow/model/Playlist.java delete mode 100644 src/com/p4square/grow/model/Point.java delete mode 100644 src/com/p4square/grow/model/QuadQuestion.java delete mode 100644 src/com/p4square/grow/model/QuadScoringEngine.java delete mode 100644 src/com/p4square/grow/model/Question.java delete mode 100644 src/com/p4square/grow/model/RecordedAnswer.java delete mode 100644 src/com/p4square/grow/model/Score.java delete mode 100644 src/com/p4square/grow/model/ScoringEngine.java delete mode 100644 src/com/p4square/grow/model/SimpleScoringEngine.java delete mode 100644 src/com/p4square/grow/model/SliderQuestion.java delete mode 100644 src/com/p4square/grow/model/SliderScoringEngine.java delete mode 100644 src/com/p4square/grow/model/TextQuestion.java delete mode 100644 src/com/p4square/grow/model/TrainingRecord.java delete mode 100644 src/com/p4square/grow/model/UserRecord.java delete mode 100644 src/com/p4square/grow/model/VideoRecord.java delete mode 100644 src/com/p4square/grow/provider/CollectionProvider.java delete mode 100644 src/com/p4square/grow/provider/DelegateCollectionProvider.java delete mode 100644 src/com/p4square/grow/provider/DelegateProvider.java delete mode 100644 src/com/p4square/grow/provider/JsonEncodedProvider.java delete mode 100644 src/com/p4square/grow/provider/MapCollectionProvider.java delete mode 100644 src/com/p4square/grow/provider/MapProvider.java delete mode 100644 src/com/p4square/grow/provider/Provider.java delete mode 100644 src/com/p4square/grow/provider/ProvidesAssessments.java delete mode 100644 src/com/p4square/grow/provider/ProvidesQuestions.java delete mode 100644 src/com/p4square/grow/provider/ProvidesStrings.java delete mode 100644 src/com/p4square/grow/provider/ProvidesTrainingRecords.java delete mode 100644 src/com/p4square/grow/provider/ProvidesUserRecords.java delete mode 100644 src/com/p4square/grow/provider/ProvidesVideos.java delete mode 100644 src/com/p4square/grow/provider/TrainingRecordProvider.java delete mode 100644 src/com/p4square/grow/tools/AssessmentStats.java delete mode 100644 src/com/p4square/grow/tools/AttributeBackfillTool.java delete mode 100644 src/com/p4square/grow/tools/AttributeTool.java delete mode 100644 src/com/p4square/restlet/metrics/MetricRouter.java delete mode 100644 src/com/p4square/restlet/metrics/MetricsApplication.java delete mode 100644 src/com/p4square/restlet/metrics/MetricsResource.java delete mode 100644 src/com/p4square/restlet/oauth/OAuthAuthenticator.java delete mode 100644 src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java delete mode 100644 src/com/p4square/restlet/oauth/OAuthException.java delete mode 100644 src/com/p4square/restlet/oauth/OAuthHelper.java delete mode 100644 src/com/p4square/restlet/oauth/OAuthUser.java delete mode 100644 src/com/p4square/restlet/oauth/Token.java delete mode 100644 src/com/p4square/session/Session.java delete mode 100644 src/com/p4square/session/SessionAuthenticator.java delete mode 100644 src/com/p4square/session/SessionCheckingAuthenticator.java delete mode 100644 src/com/p4square/session/SessionCookieAuthenticator.java delete mode 100644 src/com/p4square/session/SessionCreatingAuthenticator.java delete mode 100644 src/com/p4square/session/Sessions.java delete mode 100644 src/grow.properties delete mode 100644 src/jetty-logging.properties delete mode 100644 src/log4j.properties create mode 100644 src/main/java/com/p4square/f1oauth/Attribute.java create mode 100644 src/main/java/com/p4square/f1oauth/F1API.java create mode 100644 src/main/java/com/p4square/f1oauth/F1Access.java create mode 100644 src/main/java/com/p4square/f1oauth/F1Exception.java create mode 100644 src/main/java/com/p4square/f1oauth/F1ProgressReporter.java create mode 100644 src/main/java/com/p4square/f1oauth/F1User.java create mode 100644 src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java create mode 100644 src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java create mode 100644 src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java create mode 100644 src/main/java/com/p4square/fmfacade/FMFacade.java create mode 100644 src/main/java/com/p4square/fmfacade/FreeMarkerPageResource.java create mode 100644 src/main/java/com/p4square/fmfacade/ftl/GetMethod.java create mode 100644 src/main/java/com/p4square/fmfacade/json/ClientException.java create mode 100644 src/main/java/com/p4square/fmfacade/json/JsonRequestClient.java create mode 100644 src/main/java/com/p4square/fmfacade/json/JsonResponse.java create mode 100644 src/main/java/com/p4square/grow/GrowProcessComponent.java create mode 100644 src/main/java/com/p4square/grow/backend/BackendVerifier.java create mode 100644 src/main/java/com/p4square/grow/backend/CassandraGrowData.java create mode 100644 src/main/java/com/p4square/grow/backend/DynamoGrowData.java create mode 100644 src/main/java/com/p4square/grow/backend/GrowBackend.java create mode 100644 src/main/java/com/p4square/grow/backend/GrowData.java create mode 100644 src/main/java/com/p4square/grow/backend/db/CassandraCollectionProvider.java create mode 100644 src/main/java/com/p4square/grow/backend/db/CassandraDatabase.java create mode 100644 src/main/java/com/p4square/grow/backend/db/CassandraKey.java create mode 100644 src/main/java/com/p4square/grow/backend/db/CassandraProviderImpl.java create mode 100644 src/main/java/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java create mode 100644 src/main/java/com/p4square/grow/backend/dynamo/DbTool.java create mode 100644 src/main/java/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java create mode 100644 src/main/java/com/p4square/grow/backend/dynamo/DynamoDatabase.java create mode 100644 src/main/java/com/p4square/grow/backend/dynamo/DynamoKey.java create mode 100644 src/main/java/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java create mode 100644 src/main/java/com/p4square/grow/backend/feed/FeedDataProvider.java create mode 100644 src/main/java/com/p4square/grow/backend/feed/ThreadResource.java create mode 100644 src/main/java/com/p4square/grow/backend/feed/TopicResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/AccountResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/BannerResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/SurveyResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/SurveyResultsResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/TrainingRecordResource.java create mode 100644 src/main/java/com/p4square/grow/backend/resources/TrainingResource.java create mode 100644 src/main/java/com/p4square/grow/ccb/CCBProgressReporter.java create mode 100644 src/main/java/com/p4square/grow/ccb/CCBUser.java create mode 100644 src/main/java/com/p4square/grow/ccb/CCBUserVerifier.java create mode 100644 src/main/java/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java create mode 100644 src/main/java/com/p4square/grow/ccb/CustomFieldCache.java create mode 100644 src/main/java/com/p4square/grow/ccb/MonitoredCCBAPI.java create mode 100644 src/main/java/com/p4square/grow/config/Config.java create mode 100644 src/main/java/com/p4square/grow/frontend/AccountRedirectResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/AssessmentResetPage.java create mode 100644 src/main/java/com/p4square/grow/frontend/AssessmentResultsPage.java create mode 100644 src/main/java/com/p4square/grow/frontend/AuthenticatedResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/ChapterCompletePage.java create mode 100644 src/main/java/com/p4square/grow/frontend/ErrorPage.java create mode 100644 src/main/java/com/p4square/grow/frontend/FeedData.java create mode 100644 src/main/java/com/p4square/grow/frontend/FeedResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/GrowFrontend.java create mode 100644 src/main/java/com/p4square/grow/frontend/IntegrationDriver.java create mode 100644 src/main/java/com/p4square/grow/frontend/JsonRequestProvider.java create mode 100644 src/main/java/com/p4square/grow/frontend/LoginFormAuthenticator.java create mode 100644 src/main/java/com/p4square/grow/frontend/LoginPageResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/LogoutResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/NewAccountResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/NewBelieverResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/NotFoundException.java create mode 100644 src/main/java/com/p4square/grow/frontend/ProgressReporter.java create mode 100644 src/main/java/com/p4square/grow/frontend/SurveyPageResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/TrainingPageResource.java create mode 100644 src/main/java/com/p4square/grow/frontend/VideosResource.java create mode 100644 src/main/java/com/p4square/grow/model/Answer.java create mode 100644 src/main/java/com/p4square/grow/model/Banner.java create mode 100644 src/main/java/com/p4square/grow/model/Chapter.java create mode 100644 src/main/java/com/p4square/grow/model/CircleQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/ImageQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/Message.java create mode 100644 src/main/java/com/p4square/grow/model/MessageThread.java create mode 100644 src/main/java/com/p4square/grow/model/Playlist.java create mode 100644 src/main/java/com/p4square/grow/model/Point.java create mode 100644 src/main/java/com/p4square/grow/model/QuadQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/QuadScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/Question.java create mode 100644 src/main/java/com/p4square/grow/model/RecordedAnswer.java create mode 100644 src/main/java/com/p4square/grow/model/Score.java create mode 100644 src/main/java/com/p4square/grow/model/ScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/SimpleScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/SliderQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/SliderScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/TextQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/TrainingRecord.java create mode 100644 src/main/java/com/p4square/grow/model/UserRecord.java create mode 100644 src/main/java/com/p4square/grow/model/VideoRecord.java create mode 100644 src/main/java/com/p4square/grow/provider/CollectionProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/DelegateCollectionProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/DelegateProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/JsonEncodedProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/MapCollectionProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/MapProvider.java create mode 100644 src/main/java/com/p4square/grow/provider/Provider.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesAssessments.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesQuestions.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesStrings.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesTrainingRecords.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesUserRecords.java create mode 100644 src/main/java/com/p4square/grow/provider/ProvidesVideos.java create mode 100644 src/main/java/com/p4square/grow/provider/TrainingRecordProvider.java create mode 100644 src/main/java/com/p4square/grow/tools/AssessmentStats.java create mode 100644 src/main/java/com/p4square/grow/tools/AttributeBackfillTool.java create mode 100644 src/main/java/com/p4square/grow/tools/AttributeTool.java create mode 100644 src/main/java/com/p4square/restlet/metrics/MetricRouter.java create mode 100644 src/main/java/com/p4square/restlet/metrics/MetricsApplication.java create mode 100644 src/main/java/com/p4square/restlet/metrics/MetricsResource.java create mode 100644 src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java create mode 100644 src/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java create mode 100644 src/main/java/com/p4square/restlet/oauth/OAuthException.java create mode 100644 src/main/java/com/p4square/restlet/oauth/OAuthHelper.java create mode 100644 src/main/java/com/p4square/restlet/oauth/OAuthUser.java create mode 100644 src/main/java/com/p4square/restlet/oauth/Token.java create mode 100644 src/main/java/com/p4square/session/Session.java create mode 100644 src/main/java/com/p4square/session/SessionAuthenticator.java create mode 100644 src/main/java/com/p4square/session/SessionCheckingAuthenticator.java create mode 100644 src/main/java/com/p4square/session/SessionCookieAuthenticator.java create mode 100644 src/main/java/com/p4square/session/SessionCreatingAuthenticator.java create mode 100644 src/main/java/com/p4square/session/Sessions.java create mode 100644 src/main/resources/com/p4square/grow/backend/apiinfo.html create mode 100644 src/main/resources/grow.properties create mode 100644 src/main/resources/jetty-logging.properties create mode 100644 src/main/resources/log4j.properties create mode 100644 src/main/resources/templates/macros/common-page.ftl create mode 100644 src/main/resources/templates/macros/common.ftl create mode 100644 src/main/resources/templates/macros/content.ftl create mode 100644 src/main/resources/templates/macros/hms.ftl create mode 100644 src/main/resources/templates/macros/noticebox.ftl create mode 100644 src/main/resources/templates/pages/assessment.html.ftl create mode 100644 src/main/resources/templates/pages/contact.html.ftl create mode 100644 src/main/resources/templates/pages/deeper/believer.html.ftl create mode 100644 src/main/resources/templates/pages/deeper/disciple.html.ftl create mode 100644 src/main/resources/templates/pages/deeper/leader.html.ftl create mode 100644 src/main/resources/templates/pages/deeper/seeker.html.ftl create mode 100644 src/main/resources/templates/pages/deeper/teacher.html.ftl create mode 100644 src/main/resources/templates/pages/index.html.ftl create mode 100644 src/main/resources/templates/pages/learnmore.html.ftl create mode 100644 src/main/resources/templates/pages/login.html.ftl create mode 100644 src/main/resources/templates/pages/newaccount.html.ftl create mode 100644 src/main/resources/templates/pages/verification.html.ftl create mode 100644 src/main/resources/templates/pages/version.ftl create mode 100644 src/main/resources/templates/templates/assessment-results.ftl create mode 100644 src/main/resources/templates/templates/banner.ftl create mode 100644 src/main/resources/templates/templates/communityfeed.ftl create mode 100644 src/main/resources/templates/templates/deeperheader.ftl create mode 100644 src/main/resources/templates/templates/error.ftl create mode 100644 src/main/resources/templates/templates/footer.ftl create mode 100644 src/main/resources/templates/templates/getstarted-button.ftl create mode 100644 src/main/resources/templates/templates/gitversion.ftl create mode 100644 src/main/resources/templates/templates/header.ftl create mode 100644 src/main/resources/templates/templates/index-hero.ftl create mode 100644 src/main/resources/templates/templates/nav.ftl create mode 100644 src/main/resources/templates/templates/newbeliever.ftl create mode 100644 src/main/resources/templates/templates/question-circle.ftl create mode 100644 src/main/resources/templates/templates/question-image.ftl create mode 100644 src/main/resources/templates/templates/question-quad.ftl create mode 100644 src/main/resources/templates/templates/question-slider.ftl create mode 100644 src/main/resources/templates/templates/question-text.ftl create mode 100644 src/main/resources/templates/templates/stage-complete.ftl create mode 100644 src/main/resources/templates/templates/stage-teacher-forward.ftl create mode 100644 src/main/resources/templates/templates/survey.ftl create mode 100644 src/main/resources/templates/templates/training.ftl create mode 100644 src/main/resources/templates/utils/dump.ftl create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/error.html create mode 100644 src/main/webapp/favicon.ico create mode 100644 src/main/webapp/images/02-a1-hover.jpg create mode 100644 src/main/webapp/images/02-a1.jpg create mode 100644 src/main/webapp/images/02-a2-hover.jpg create mode 100644 src/main/webapp/images/02-a2.jpg create mode 100644 src/main/webapp/images/02-a3-hover.jpg create mode 100644 src/main/webapp/images/02-a3.jpg create mode 100644 src/main/webapp/images/02-a4-hover.jpg create mode 100644 src/main/webapp/images/02-a4.jpg create mode 100644 src/main/webapp/images/02-a5-hover.jpg create mode 100644 src/main/webapp/images/02-a5.jpg create mode 100644 src/main/webapp/images/02-a6-hover.jpg create mode 100644 src/main/webapp/images/02-a6.jpg create mode 100644 src/main/webapp/images/08-a1-hover.jpg create mode 100644 src/main/webapp/images/08-a1.jpg create mode 100644 src/main/webapp/images/08-a2-hover.jpg create mode 100644 src/main/webapp/images/08-a2.jpg create mode 100644 src/main/webapp/images/08-a3-hover.jpg create mode 100644 src/main/webapp/images/08-a3.jpg create mode 100644 src/main/webapp/images/about-grow.png create mode 100644 src/main/webapp/images/acts242.png create mode 100644 src/main/webapp/images/close.png create mode 100644 src/main/webapp/images/complete.png create mode 100644 src/main/webapp/images/faux_right_column.png create mode 100644 src/main/webapp/images/foursquarechurchlogin.png create mode 100644 src/main/webapp/images/foursquarelg.png create mode 100644 src/main/webapp/images/foursquaresm.png create mode 100644 src/main/webapp/images/grow-poster.png create mode 100644 src/main/webapp/images/hero.png create mode 100644 src/main/webapp/images/leadershipdev.png create mode 100644 src/main/webapp/images/loginbg.png create mode 100644 src/main/webapp/images/logo.png create mode 100644 src/main/webapp/images/next.png create mode 100644 src/main/webapp/images/noticeicon.png create mode 100644 src/main/webapp/images/play.png create mode 100644 src/main/webapp/images/previous.png create mode 100644 src/main/webapp/images/quad.png create mode 100644 src/main/webapp/images/quadselector.png create mode 100644 src/main/webapp/images/reply.png create mode 100644 src/main/webapp/images/slider.png create mode 100644 src/main/webapp/images/videoimage.jpg create mode 100644 src/main/webapp/notfound.html create mode 100644 src/main/webapp/scripts/growth.js create mode 100644 src/main/webapp/scripts/jquery-ui.js create mode 100644 src/main/webapp/scripts/jquery.min.js create mode 100644 src/main/webapp/style.css delete mode 100644 src/templates/macros/common-page.ftl delete mode 100644 src/templates/macros/common.ftl delete mode 100644 src/templates/macros/content.ftl delete mode 100644 src/templates/macros/hms.ftl delete mode 100644 src/templates/macros/noticebox.ftl delete mode 100644 src/templates/pages/assessment.html.ftl delete mode 100644 src/templates/pages/contact.html.ftl delete mode 100644 src/templates/pages/deeper/believer.html.ftl delete mode 100644 src/templates/pages/deeper/disciple.html.ftl delete mode 100644 src/templates/pages/deeper/leader.html.ftl delete mode 100644 src/templates/pages/deeper/seeker.html.ftl delete mode 100644 src/templates/pages/deeper/teacher.html.ftl delete mode 100644 src/templates/pages/index.html.ftl delete mode 100644 src/templates/pages/learnmore.html.ftl delete mode 100644 src/templates/pages/login.html.ftl delete mode 100644 src/templates/pages/newaccount.html.ftl delete mode 100644 src/templates/pages/verification.html.ftl delete mode 100644 src/templates/pages/version.ftl delete mode 100644 src/templates/templates/assessment-results.ftl delete mode 100644 src/templates/templates/banner.ftl delete mode 100644 src/templates/templates/communityfeed.ftl delete mode 100644 src/templates/templates/deeperheader.ftl delete mode 100644 src/templates/templates/error.ftl delete mode 100644 src/templates/templates/footer.ftl delete mode 100644 src/templates/templates/getstarted-button.ftl delete mode 100644 src/templates/templates/gitversion.ftl delete mode 100644 src/templates/templates/header.ftl delete mode 100644 src/templates/templates/index-hero.ftl delete mode 100644 src/templates/templates/nav.ftl delete mode 100644 src/templates/templates/newbeliever.ftl delete mode 100644 src/templates/templates/question-circle.ftl delete mode 100644 src/templates/templates/question-image.ftl delete mode 100644 src/templates/templates/question-quad.ftl delete mode 100644 src/templates/templates/question-slider.ftl delete mode 100644 src/templates/templates/question-text.ftl delete mode 100644 src/templates/templates/stage-complete.ftl delete mode 100644 src/templates/templates/stage-teacher-forward.ftl delete mode 100644 src/templates/templates/survey.ftl delete mode 100644 src/templates/templates/training.ftl delete mode 100644 src/templates/utils/dump.ftl create mode 100644 src/test/java/com/p4square/grow/backend/resources/ResourceTestBase.java create mode 100644 src/test/java/com/p4square/grow/backend/resources/TrainingRecordResourceTest.java create mode 100644 src/test/java/com/p4square/grow/ccb/CCBProgressReporterTest.java create mode 100644 src/test/java/com/p4square/grow/ccb/CCBUserVerifierTest.java create mode 100644 src/test/java/com/p4square/grow/ccb/CustomFieldCacheTest.java create mode 100644 src/test/java/com/p4square/grow/config/ConfigTest.java create mode 100644 src/test/java/com/p4square/grow/model/AnswerTest.java create mode 100644 src/test/java/com/p4square/grow/model/CircleQuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/ImageQuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/PlaylistTest.java create mode 100644 src/test/java/com/p4square/grow/model/PointTest.java create mode 100644 src/test/java/com/p4square/grow/model/QuadQuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/QuadScoringEngineTest.java create mode 100644 src/test/java/com/p4square/grow/model/QuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/ScoreTest.java create mode 100644 src/test/java/com/p4square/grow/model/SimpleScoringEngineTest.java create mode 100644 src/test/java/com/p4square/grow/model/SliderQuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/SliderScoringEngineTest.java create mode 100644 src/test/java/com/p4square/grow/model/TextQuestionTest.java create mode 100644 src/test/java/com/p4square/grow/model/TrainingRecordTest.java create mode 100644 src/test/resources/com/p4square/grow/config/ConfigTest.properties create mode 100644 src/test/resources/com/p4square/grow/model/trainingrecord.json delete mode 100644 tst/com/p4square/grow/backend/resources/ResourceTestBase.java delete mode 100644 tst/com/p4square/grow/backend/resources/TrainingRecordResourceTest.java delete mode 100644 tst/com/p4square/grow/ccb/CCBProgressReporterTest.java delete mode 100644 tst/com/p4square/grow/ccb/CCBUserVerifierTest.java delete mode 100644 tst/com/p4square/grow/ccb/CustomFieldCacheTest.java delete mode 100644 tst/com/p4square/grow/config/ConfigTest.java delete mode 100644 tst/com/p4square/grow/config/ConfigTest.properties delete mode 100644 tst/com/p4square/grow/model/AnswerTest.java delete mode 100644 tst/com/p4square/grow/model/CircleQuestionTest.java delete mode 100644 tst/com/p4square/grow/model/ImageQuestionTest.java delete mode 100644 tst/com/p4square/grow/model/PlaylistTest.java delete mode 100644 tst/com/p4square/grow/model/PointTest.java delete mode 100644 tst/com/p4square/grow/model/QuadQuestionTest.java delete mode 100644 tst/com/p4square/grow/model/QuadScoringEngineTest.java delete mode 100644 tst/com/p4square/grow/model/QuestionTest.java delete mode 100644 tst/com/p4square/grow/model/ScoreTest.java delete mode 100644 tst/com/p4square/grow/model/SimpleScoringEngineTest.java delete mode 100644 tst/com/p4square/grow/model/SliderQuestionTest.java delete mode 100644 tst/com/p4square/grow/model/SliderScoringEngineTest.java delete mode 100644 tst/com/p4square/grow/model/TextQuestionTest.java delete mode 100644 tst/com/p4square/grow/model/TrainingRecordTest.java delete mode 100644 tst/com/p4square/grow/model/trainingrecord.json delete mode 100644 web/WEB-INF/web.xml delete mode 100644 web/error.html delete mode 100644 web/favicon.ico delete mode 100644 web/images/02-a1-hover.jpg delete mode 100644 web/images/02-a1.jpg delete mode 100644 web/images/02-a2-hover.jpg delete mode 100644 web/images/02-a2.jpg delete mode 100644 web/images/02-a3-hover.jpg delete mode 100644 web/images/02-a3.jpg delete mode 100644 web/images/02-a4-hover.jpg delete mode 100644 web/images/02-a4.jpg delete mode 100644 web/images/02-a5-hover.jpg delete mode 100644 web/images/02-a5.jpg delete mode 100644 web/images/02-a6-hover.jpg delete mode 100644 web/images/02-a6.jpg delete mode 100644 web/images/08-a1-hover.jpg delete mode 100644 web/images/08-a1.jpg delete mode 100644 web/images/08-a2-hover.jpg delete mode 100644 web/images/08-a2.jpg delete mode 100644 web/images/08-a3-hover.jpg delete mode 100644 web/images/08-a3.jpg delete mode 100644 web/images/about-grow.png delete mode 100644 web/images/acts242.png delete mode 100644 web/images/close.png delete mode 100644 web/images/complete.png delete mode 100644 web/images/faux_right_column.png delete mode 100644 web/images/foursquarechurchlogin.png delete mode 100644 web/images/foursquarelg.png delete mode 100644 web/images/foursquaresm.png delete mode 100644 web/images/grow-poster.png delete mode 100644 web/images/hero.png delete mode 100644 web/images/leadershipdev.png delete mode 100644 web/images/loginbg.png delete mode 100644 web/images/logo.png delete mode 100644 web/images/next.png delete mode 100644 web/images/noticeicon.png delete mode 100644 web/images/play.png delete mode 100644 web/images/previous.png delete mode 100644 web/images/quad.png delete mode 100644 web/images/quadselector.png delete mode 100644 web/images/reply.png delete mode 100644 web/images/slider.png delete mode 100644 web/images/videoimage.jpg delete mode 100644 web/notfound.html delete mode 100644 web/scripts/growth.js delete mode 100644 web/scripts/jquery-ui.js delete mode 100644 web/scripts/jquery.min.js delete mode 100644 web/style.css diff --git a/.gitignore b/.gitignore index 7e32fa1..14412d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -build +target/ out/ lib *.swp .idea *.iml +service.log devfiles/grow-server.properties diff --git a/.travis.yml b/.travis.yml index 35dc598..9bcf999 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ language: java jdk: - oraclejdk8 -before_install: "./devfiles/scripts/get-build-tools.sh" -install: "ant resolve test" diff --git a/README.md b/README.md index 6852640..f67ca04 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,16 @@ This is the source for the [Foursquare GROW](http://foursquaregrow.com) Website. Requirements -------------- -* JDK 1.7 -* Ant -* Ivy -* jesterpm-build-tools - +* JDK 1.8 +* Maven Usage ------- -1. Download and bootstrap jesterpm-build-tools from http://github.com/jesterpm/jesterpm-build-tools -2. Copy devfiles/grow-server.properties.default to devfiles/grow-server.properties and insert your - AWS and F1 credentials. -3. Run `ant resolve` to download the dependencies. -4. Run `ant server` to start the website on http://localhost:8085 - -The website defaults to running in dev mode which will only modify the dev Dynamo tables. You can -also run `ant server-prod` to cause the local website to access the production site's Dyanmo -tables. +1. Copy devfiles/grow-server.properties.default to devfiles/grow-server.properties and insert your + AWS and F1 or CCB credentials. +2. Run `maven compile` to compile. +3. Run `maven exec:exec` to start the website on http://localhost:8085 + The website defaults to running in dev mode which will only modify the dev Dynamo tables. + You *must recompile* for changes to take effect. +4. Run `maven war:war` to produce a war file to deploy. diff --git a/build.xml b/build.xml deleted file mode 100644 index be2a325..0000000 --- a/build.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ivy.xml b/ivy.xml deleted file mode 100644 index 2953b42..0000000 --- a/ivy.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ivysettings.xml b/ivysettings.xml deleted file mode 100644 index 177bd80..0000000 --- a/ivysettings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0d23cd9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,226 @@ + + + 4.0.0 + + com.p4square + foursquare-grow + 1.0-SNAPSHOT + + foursquare-grow + The Foursquare Grow Website + https://github.com/PuyallupFoursquare/foursquare-grow + + + + Jesse Morgan + jesse@jesterpm.net + + + + + + org.restlet + org.restlet + http://maven.restlet.org + + + + + + + com.amazonaws + aws-java-sdk-bom + 1.10.62 + pom + import + + + + + + + log4j + log4j + [1.2,) + + + + org.restlet.jee + org.restlet + [2.2-M5] + + + org.restlet.jee + org.restlet.ext.servlet + [2.2-M5] + + + org.restlet.jee + org.restlet.ext.jackson + [2.2-M5] + + + org.restlet.jee + org.restlet.ext.freemarker + [2.2-M5] + + + org.restlet.jee + org.restlet.ext.httpclient + [2.2-M5] + + + + + com.netflix.astyanax + astyanax-core + 1.56.49 + + + com.netflix.astyanax + astyanax-thrift + 1.56.49 + + + com.netflix.astyanax + astyanax-cassandra + 1.56.49 + + + com.amazonaws + aws-java-sdk-dynamodb + + + + io.dropwizard.metrics + metrics-core + 3.1.0 + + + io.dropwizard.metrics + metrics-json + 3.1.0 + + + + com.p4square + ccbapi + [1.0,) + + + + + org.apache.httpcomponents + httpcore + 4.4.4 + + + org.apache.httpcomponents + httpclient + 4.3.6 + + + joda-time + joda-time + 2.8.1 + + + + + junit + junit + 4.8.2 + test + + + org.easymock + easymock + 3.4 + test + + + + + + + src/main/resources + false + + templates/templates/gitversion.ftl + + + + src/main/resources + true + + templates/templates/gitversion.ftl + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + 1.8 + 1.8 + + + + + com.lukegb.mojo + gitdescribe-maven-plugin + 3.0 + + + + gitdescribe + + git-describe + initialize + + + --always + --dirty + --tags + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + + + exec + + + + + java + + -classpath + + com.p4square.grow.GrowProcessComponent + devfiles/grow-server.properties + + + + + + + + scm:git:git@github.com:PuyallupFoursquare/foursquare-grow.git + scm:git:git@github.com:PuyallupFoursquare/foursquare-grow.git + scm:git:git@github.com:PuyallupFoursquare/foursquare-grow.git + + + diff --git a/src/com/p4square/f1oauth/Attribute.java b/src/com/p4square/f1oauth/Attribute.java deleted file mode 100644 index 64f2507..0000000 --- a/src/com/p4square/f1oauth/Attribute.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import java.util.Date; - -/** - * F1 Attribute Data. - * - * @author Jesse Morgan - */ -public class Attribute { - private final String mAttributeName; - private String mId; - private Date mStartDate; - private Date mEndDate; - private String mComment; - - /** - * @param name The attribute name. - */ - public Attribute(final String name) { - mAttributeName = name; - } - - /** - * @return the Attribute name. - */ - public String getAttributeName() { - return mAttributeName; - } - - /** - * @return the id of this specific attribute instance. - */ - public String getId() { - return mId; - } - - /** - * Set the attribute id to id. - */ - public void setId(final String id) { - mId = id; - } - - /** - * @return the start date for the attribute. - */ - public Date getStartDate() { - return mStartDate; - } - - /** - * Set the start date for the attribute. - */ - public void setStartDate(final Date date) { - mStartDate = date; - } - - /** - * @return the end date for the attribute. - */ - public Date getEndDate() { - return mEndDate; - } - - /** - * Set the end date for the attribute. - */ - public void setEndDate(final Date date) { - mEndDate = date; - } - - /** - * @return The comment on the Attribute. - */ - public String getComment() { - return mComment; - } - - /** - * Set the comment on the attribute. - */ - public void setComment(final String comment) { - mComment = comment; - } -} diff --git a/src/com/p4square/f1oauth/F1API.java b/src/com/p4square/f1oauth/F1API.java deleted file mode 100644 index a525c3f..0000000 --- a/src/com/p4square/f1oauth/F1API.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.p4square.restlet.oauth.OAuthException; -import com.p4square.restlet.oauth.OAuthUser; - -/** - * F1 API methods which require an authenticated user. - * - * @author Jesse Morgan - */ -public interface F1API { - /** - * Fetch information about a user. - * - * @param user The user to fetch information about. - * @return An F1User object. - */ - F1User getF1User(OAuthUser user) throws OAuthException, IOException; - - /** - * Fetch a list of all attributes ids and names. - * - * @return A Map of attribute name to attribute id. - */ - Map getAttributeList() throws F1Exception; - - /** - * Add an attribute to the user. - * - * @param user The user to add the attribute to. - * @param attributeName The attribute to add. - * @param attribute The attribute to add. - */ - boolean addAttribute(String userId, Attribute attribute) throws F1Exception; - - /** - * Return attributes assigned to user. - * - * A user may be assigned multiple attributes with the same name, thus even if - * attributeName is specified, multiple attributes may be returned. - * - * @param userId The user to query. - * @param attributeName A specific attribute to return, null for all. - * @return A list of Attributes - */ - List getAttribute(String userId, String attributeName) throws F1Exception; - -} diff --git a/src/com/p4square/f1oauth/F1Access.java b/src/com/p4square/f1oauth/F1Access.java deleted file mode 100644 index c3307f1..0000000 --- a/src/com/p4square/f1oauth/F1Access.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import java.io.IOException; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; - -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.MediaType; -import org.restlet.data.Method; -import org.restlet.data.Status; -import org.restlet.engine.util.Base64; -import org.restlet.ext.jackson.JacksonRepresentation; -import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; - -import com.p4square.restlet.oauth.OAuthException; -import com.p4square.restlet.oauth.OAuthHelper; -import com.p4square.restlet.oauth.OAuthUser; -import com.p4square.restlet.oauth.Token; - -/** - * F1 API Access. - * - * @author Jesse Morgan - */ -public class F1Access { - public enum UserType { - WEBLINK, PORTAL; - } - - private static final Logger LOG = Logger.getLogger(F1Access.class); - - private static final String VERSION_STRING = "/v1/"; - private static final String REQUESTTOKEN_URL = "Tokens/RequestToken"; - private static final String AUTHORIZATION_URL = "Login"; - private static final String ACCESSTOKEN_URL= "Tokens/AccessToken"; - private static final String TRUSTED_ACCESSTOKEN_URL = "/AccessToken"; - - private static final SimpleDateFormat DATE_FORMAT = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - - private final String mBaseUrl; - private final String mMethod; - - private final OAuthHelper mOAuthHelper; - - private final Map mAttributeIdByName; - - private MetricRegistry mMetricRegistry; - - /** - */ - public F1Access(Context context, String consumerKey, String consumerSecret, - String baseUrl, String churchCode, UserType userType) { - - switch (userType) { - case WEBLINK: - mMethod = "WeblinkUser"; - break; - case PORTAL: - mMethod = "PortalUser"; - break; - default: - throw new IllegalArgumentException("Unknown UserType"); - } - - mBaseUrl = "https://" + churchCode + "." + baseUrl + VERSION_STRING; - - // Create the OAuthHelper. This implicitly registers the helper to - // handle outgoing requests which need OAuth authentication. - mOAuthHelper = new OAuthHelper(context, consumerKey, consumerSecret) { - @Override - protected String getRequestTokenUrl() { - return mBaseUrl + REQUESTTOKEN_URL; - } - - @Override - public String getLoginUrl(Token requestToken, String callback) { - String loginUrl = mBaseUrl + mMethod + AUTHORIZATION_URL - + "?oauth_token=" + URLEncoder.encode(requestToken.getToken()); - - if (callback != null) { - loginUrl += "&oauth_callback=" + URLEncoder.encode(callback); - } - - return loginUrl; - } - - @Override - protected String getAccessTokenUrl() { - return mBaseUrl + ACCESSTOKEN_URL; - } - }; - - mAttributeIdByName = new HashMap<>(); - } - - /** - * Set the MetricRegistry to get metrics recorded. - */ - public void setMetricRegistry(MetricRegistry metrics) { - mMetricRegistry = metrics; - } - - /** - * Request an AccessToken for a particular username and password. - * - * This is an F1 extension to OAuth: - * http://developer.fellowshipone.com/docs/v1/Util/AuthDocs.help#2creds - */ - public OAuthUser getAccessToken(String username, String password) throws OAuthException { - Timer.Context timer = getTimer("F1Access.getAccessToken.time"); - boolean success = true; - - try { - Request request = new Request(Method.POST, - mBaseUrl + mMethod + TRUSTED_ACCESSTOKEN_URL); - request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); - - String base64String = Base64.encode((username + " " + password).getBytes(), false); - request.setEntity(new StringRepresentation(base64String)); - - return mOAuthHelper.processAccessTokenRequest(request); - - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.getAccessToken.success"); - } else { - incrementCounter("F1Access.getAccessToken.failure"); - } - } - } - - /** - * Create a new Account. - * - * @param firstname The user's first name. - * @param lastname The user's last name. - * @param email The user's email address. - * @param redirect The URL to send the user to after confirming his address. - * - * @return true if created, false if the account already exists. - */ - public boolean createAccount(String firstname, String lastname, String email, String redirect) - throws OAuthException { - Timer.Context timer = getTimer("F1Access.createAccount.time"); - boolean success = true; - - try { - String req = String.format("{\n\"account\":{\n\"firstName\":\"%s\",\n" - + "\"lastName\":\"%s\",\n\"email\":\"%s\",\n" - + "\"urlRedirect\":\"%s\"\n}\n}", - firstname, lastname, email, redirect); - - Request request = new Request(Method.POST, mBaseUrl + "Accounts"); - request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); - request.setEntity(new StringRepresentation(req, MediaType.APPLICATION_JSON)); - - Response response = mOAuthHelper.getResponse(request); - - Status status = response.getStatus(); - if (Status.SUCCESS_NO_CONTENT.equals(status)) { - return true; - - } else if (Status.CLIENT_ERROR_CONFLICT.equals(status)) { - return false; - - } else { - throw new OAuthException(status); - } - - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.createAccount.success"); - } else { - incrementCounter("F1Access.createAccount.failure"); - } - } - } - - /** - * @return An F1API authenticated by the given user. - */ - public F1API getAuthenticatedApi(OAuthUser user) { - return new AuthenticatedApi(user); - } - - private class AuthenticatedApi implements F1API { - private final OAuthUser mUser; - - public AuthenticatedApi(OAuthUser user) { - mUser = user; - } - - /** - * Fetch information about a user. - * - * @param user The user to fetch information about. - * @return An F1User object. - */ - @Override - public F1User getF1User(OAuthUser user) throws OAuthException, IOException { - Timer.Context timer = getTimer("F1Access.getF1User.time"); - boolean success = true; - - try { - Request request = new Request(Method.GET, user.getLocation() + ".json"); - request.setChallengeResponse(mUser.getChallengeResponse()); - Response response = mOAuthHelper.getResponse(request); - - try { - Status status = response.getStatus(); - if (status.isSuccess()) { - JacksonRepresentation entity = - new JacksonRepresentation(response.getEntity(), Map.class); - Map data = entity.getObject(); - return new F1User(user, data); - - } else { - throw new OAuthException(status); - } - - } finally { - if (response.getEntity() != null) { - response.release(); - } - } - - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.getF1User.success"); - } else { - incrementCounter("F1Access.getF1User.failure"); - } - } - } - - @Override - public Map getAttributeList() throws F1Exception { - // Note: this list is shared by all F1 users. - synchronized (mAttributeIdByName) { - if (mAttributeIdByName.size() == 0) { - Timer.Context timer = getTimer("F1Access.getAttributeList.time"); - boolean success = true; - - try { - // Reload attributes. Maybe it will be there now... - Request request = new Request(Method.GET, - mBaseUrl + "People/AttributeGroups.json"); - request.setChallengeResponse(mUser.getChallengeResponse()); - Response response = mOAuthHelper.getResponse(request); - - Representation representation = response.getEntity(); - try { - Status status = response.getStatus(); - if (status.isSuccess()) { - JacksonRepresentation entity = - new JacksonRepresentation(response.getEntity(), Map.class); - - Map attributeGroups = (Map) entity.getObject().get("attributeGroups"); - List groups = (List) attributeGroups.get("attributeGroup"); - - for (Map group : groups) { - List attributes = (List) group.get("attribute"); - if (attributes != null) { - for (Map attribute : attributes) { - String id = (String) attribute.get("@id"); - String name = ((String) attribute.get("name")); - mAttributeIdByName.put(name.toLowerCase(), id); - LOG.debug("Caching attribute '" + name - + "' with id '" + id + "'"); - } - } - } - } - - } catch (IOException e) { - throw new F1Exception("Could not parse AttributeGroups.", e); - - } finally { - if (representation != null) { - representation.release(); - } - } - - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.getAttributeList.success"); - } else { - incrementCounter("F1Access.getAttributeList.failure"); - } - } - } - - return mAttributeIdByName; - } - } - - /** - * Add an attribute to the user. - * - * @param user The user to add the attribute to. - * @param attributeName The attribute to add. - * @param attribute The attribute to add. - */ - public boolean addAttribute(String userId, Attribute attribute) - throws F1Exception { - - // Get the attribute id. - String attributeId = getAttributeId(attribute.getAttributeName()); - if (attributeId == null) { - throw new F1Exception("Could not find id for " + attribute.getAttributeName()); - } - - // Get Attribute Template - Map attributeTemplate = null; - - Timer.Context timer = getTimer("F1Access.addAttribute.GET.time"); - boolean success = true; - - try { - Request request = new Request(Method.GET, - mBaseUrl + "People/" + userId + "/Attributes/new.json"); - request.setChallengeResponse(mUser.getChallengeResponse()); - Response response = mOAuthHelper.getResponse(request); - - Representation representation = response.getEntity(); - try { - Status status = response.getStatus(); - if (status.isSuccess()) { - JacksonRepresentation entity = - new JacksonRepresentation(response.getEntity(), Map.class); - attributeTemplate = entity.getObject(); - - } else { - throw new F1Exception("Failed to retrieve attribute template: " - + status); - } - - } catch (IOException e) { - throw new F1Exception("Could not parse attribute template.", e); - - } finally { - if (representation != null) { - representation.release(); - } - } - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.addAttribute.GET.success"); - } else { - incrementCounter("F1Access.addAttribute.GET.failure"); - } - } - - if (attributeTemplate == null) { - throw new F1Exception("Could not retrieve attribute template."); - } - - // Populate Attribute Template - Map attributeMap = (Map) attributeTemplate.get("attribute"); - Map attributeGroup = (Map) attributeMap.get("attributeGroup"); - - Map attributeIdMap = new HashMap<>(); - attributeIdMap.put("@id", attributeId); - attributeGroup.put("attribute", attributeIdMap); - - if (attribute.getStartDate() != null) { - attributeMap.put("startDate", DATE_FORMAT.format(attribute.getStartDate())); - } - - if (attribute.getStartDate() != null) { - attributeMap.put("endDate", DATE_FORMAT.format(attribute.getStartDate())); - } - - attributeMap.put("comment", attribute.getComment()); - - // POST new attribute - Status status; - timer = getTimer("F1Access.addAttribute.POST.time"); - success = true; - - try { - Request request = new Request(Method.POST, - mBaseUrl + "People/" + userId + "/Attributes.json"); - request.setChallengeResponse(mUser.getChallengeResponse()); - request.setEntity(new JacksonRepresentation(attributeTemplate)); - Response response = mOAuthHelper.getResponse(request); - - Representation representation = response.getEntity(); - try { - status = response.getStatus(); - - if (status.isSuccess()) { - return true; - } - - } finally { - if (representation != null) { - representation.release(); - } - } - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.addAttribute.POST.success"); - } else { - incrementCounter("F1Access.getAccessToken.POST.failure"); - } - } - - LOG.debug("addAttribute failed POST: " + status); - return false; - } - - @Override - public List getAttribute(String userId, String attributeNameFilter) - throws F1Exception { - - Map attributesResponse; - - // Get Attributes - Timer.Context timer = getTimer("F1Access.getAttribute.time"); - boolean success = true; - - try { - Request request = new Request(Method.GET, - mBaseUrl + "People/" + userId + "/Attributes.json"); - request.setChallengeResponse(mUser.getChallengeResponse()); - Response response = mOAuthHelper.getResponse(request); - - Representation representation = response.getEntity(); - try { - Status status = response.getStatus(); - if (status.isSuccess()) { - JacksonRepresentation entity = - new JacksonRepresentation(response.getEntity(), Map.class); - attributesResponse = entity.getObject(); - - } else { - throw new F1Exception("Failed to retrieve attributes: " - + status); - } - - } catch (IOException e) { - throw new F1Exception("Could not parse attributes.", e); - - } finally { - if (representation != null) { - representation.release(); - } - } - } catch (Exception e) { - success = false; - throw e; - - } finally { - if (timer != null) { - timer.stop(); - } - if (success) { - incrementCounter("F1Access.getAttribute.success"); - } else { - incrementCounter("F1Access.getAttribute.failure"); - } - } - - // Parse Response - List result = new ArrayList<>(); - - try { - // I feel like I'm writing lisp here... - Map attributesMap = (Map) attributesResponse.get("attributes"); - if (attributesMap == null) { - return result; - } - - List attributes = (List) (attributesMap).get("attribute"); - for (Map attributeMap : attributes) { - String id = (String) attributeMap.get("@id"); - String startDate = (String) attributeMap.get("startDate"); - String endDate = (String) attributeMap.get("endDate"); - String comment = (String) attributeMap.get("comment"); - - Map attributeIdMap = (Map) ((Map) attributeMap.get("attributeGroup")) - .get("attribute"); - String attributeName = (String) attributeIdMap.get("name"); - - if (attributeNameFilter == null - || attributeNameFilter.equalsIgnoreCase(attributeName)) { - - Attribute attribute = new Attribute(attributeName); - attribute.setId(id); - if (startDate != null) { - attribute.setStartDate(DATE_FORMAT.parse(startDate)); - } - if (endDate != null) { - attribute.setEndDate(DATE_FORMAT.parse(endDate)); - } - attribute.setComment(comment); - result.add(attribute); - } - } - } catch (Exception e) { - throw new F1Exception("Failed to parse attributes response.", e); - } - - return result; - } - - /** - * @return an attribute id for the given attribute name. - */ - private String getAttributeId(String attributeName) throws F1Exception { - Map attributeMap = getAttributeList(); - - return attributeMap.get(attributeName.toLowerCase()); - } - - } - - private Timer.Context getTimer(String name) { - if (mMetricRegistry != null) { - return mMetricRegistry.timer(name).time(); - } else { - return null; - } - } - - private void incrementCounter(String name) { - if (mMetricRegistry != null) { - mMetricRegistry.counter(name).inc(); - } - } -} diff --git a/src/com/p4square/f1oauth/F1Exception.java b/src/com/p4square/f1oauth/F1Exception.java deleted file mode 100644 index 54c1a77..0000000 --- a/src/com/p4square/f1oauth/F1Exception.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.f1oauth; - -public class F1Exception extends Exception { - public F1Exception(String message) { - super(message); - } - - public F1Exception(String message, Exception cause) { - super(message, cause); - } -} diff --git a/src/com/p4square/f1oauth/F1ProgressReporter.java b/src/com/p4square/f1oauth/F1ProgressReporter.java deleted file mode 100644 index 8382020..0000000 --- a/src/com/p4square/f1oauth/F1ProgressReporter.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.p4square.f1oauth; - -import com.p4square.grow.frontend.ProgressReporter; -import org.apache.log4j.Logger; -import org.restlet.security.User; - -import java.util.Date; - -/** - * A ProgressReporter implementation to record progress in F1. - */ -public class F1ProgressReporter implements ProgressReporter { - - private static final Logger LOG = Logger.getLogger(F1ProgressReporter.class); - - private F1Access mF1Access; - - public F1ProgressReporter(final F1Access f1access) { - mF1Access = f1access; - } - - @Override - public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) { - String attributeName = "Assessment Complete - " + level; - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(date); - attribute.setComment(results); - addAttribute(user, attribute); - } - - @Override - public void reportChapterComplete(final User user, final String chapter, final Date date) { - final String attributeName = "Training Complete - " + chapter; - final Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(date); - addAttribute(user, attribute); - } - - private void addAttribute(final User user, final Attribute attribute) { - if (!(user instanceof F1User)) { - throw new IllegalArgumentException("User must be an F1User, but got " + user.getClass().getName()); - } - - try { - final F1User f1User = (F1User) user; - final F1API f1 = mF1Access.getAuthenticatedApi(f1User); - - if (!f1.addAttribute(user.getIdentifier(), attribute)) { - LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " - + attribute.getAttributeName()); - } - } catch (Exception e) { - LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " - + attribute.getAttributeName(), e); - } - } -} diff --git a/src/com/p4square/f1oauth/F1User.java b/src/com/p4square/f1oauth/F1User.java deleted file mode 100644 index e5ab487..0000000 --- a/src/com/p4square/f1oauth/F1User.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import java.util.Map; - -import com.p4square.restlet.oauth.OAuthException; -import com.p4square.restlet.oauth.OAuthUser; - -/** - * - * @author Jesse Morgan - */ -public class F1User extends OAuthUser { - public static final String ID = "@id"; - public static final String FIRST_NAME = "firstName"; - public static final String LAST_NAME = "lastName"; - public static final String ICODE = "@iCode"; - - private final Map mData; - - /** - * Copy the user information from user into a new F1User. - * - * @param user Original user. - * @param data F1 Person Record. - * @throws IllegalStateException if data.get("person") is null. - */ - public F1User(OAuthUser user, Map data) { - super(user.getLocation(), user.getToken()); - - mData = (Map) data.get("person"); - if (mData == null) { - throw new IllegalStateException("Bad data"); - } - - setIdentifier(getString(ID)); - setFirstName(getString(FIRST_NAME)); - setLastName(getString(LAST_NAME)); - } - - /** - * Get a String from the map. - * - * @param key The map key. - * @return The value associated with the key, or null. - */ - public String getString(String key) { - Object blob = get(key); - - if (blob instanceof String) { - return (String) blob; - - } else { - return null; - } - } - - /** - * Fetch an object from the F1 record. - * - * @param key The map key - * @return The object in the map or null. - */ - public Object get(String key) { - return mData.get(key); - } -} diff --git a/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java b/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java deleted file mode 100644 index 865f5d6..0000000 --- a/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.p4square.f1oauth; - -import com.codahale.metrics.MetricRegistry; -import com.p4square.grow.config.Config; -import com.p4square.grow.frontend.IntegrationDriver; -import com.p4square.grow.frontend.ProgressReporter; -import org.restlet.Context; -import org.restlet.security.Verifier; - -/** - * The FellowshipOneIntegrationDriver creates implementations of various - * objects to support integration with Fellowship One. - */ -public class FellowshipOneIntegrationDriver implements IntegrationDriver { - - private final Context mContext; - private final MetricRegistry mMetricRegistry; - private final Config mConfig; - private final F1Access mAPI; - - private final ProgressReporter mProgressReporter; - - public FellowshipOneIntegrationDriver(final Context context) { - mContext = context; - mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); - mMetricRegistry = (MetricRegistry) context.getAttributes().get("com.p4square.grow.metrics"); - - mAPI = new F1Access(context, - mConfig.getString("f1ConsumerKey", ""), - mConfig.getString("f1ConsumerSecret", ""), - mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"), - mConfig.getString("f1ChurchCode", "pfseawa"), - F1Access.UserType.WEBLINK); - mAPI.setMetricRegistry(mMetricRegistry); - - mProgressReporter = new F1ProgressReporter(mAPI); - } - - /** - * @return An F1Access instance. - */ - public F1Access getF1Access() { - return mAPI; - } - - @Override - public Verifier newUserAuthenticationVerifier() { - return new SecondPartyVerifier(mContext, mAPI); - } - - @Override - public ProgressReporter getProgressReporter() { - return mProgressReporter; - } -} diff --git a/src/com/p4square/f1oauth/SecondPartyAuthenticator.java b/src/com/p4square/f1oauth/SecondPartyAuthenticator.java deleted file mode 100644 index 8deefec..0000000 --- a/src/com/p4square/f1oauth/SecondPartyAuthenticator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import org.apache.log4j.Logger; - -import com.p4square.restlet.oauth.OAuthException; -import com.p4square.restlet.oauth.OAuthUser; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; - -/** - * Restlet Authenticator for 2nd - * @author Jesse Morgan - */ -public class SecondPartyAuthenticator extends Authenticator { - private static final Logger LOG = Logger.getLogger(SecondPartyAuthenticator.class); - - private final F1Access mHelper; - - public SecondPartyAuthenticator(Context context, boolean optional, F1Access helper) { - super(context, optional); - - mHelper = helper; - } - - protected boolean authenticate(Request request, Response response) { - if (request.getChallengeResponse() == null) { - return false; // no credentials - } - - String username = request.getChallengeResponse().getIdentifier(); - String password = new String(request.getChallengeResponse().getSecret()); - - try { - OAuthUser user = mHelper.getAccessToken(username, password); - request.getClientInfo().setUser(user); - - return true; - - } catch (OAuthException e) { - LOG.info("OAuth Exception: " + e); - } - - return false; // Invalid credentials - } -} diff --git a/src/com/p4square/f1oauth/SecondPartyVerifier.java b/src/com/p4square/f1oauth/SecondPartyVerifier.java deleted file mode 100644 index 882c7e7..0000000 --- a/src/com/p4square/f1oauth/SecondPartyVerifier.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.f1oauth; - -import java.io.IOException; -import java.util.Map; - -import org.apache.log4j.Logger; - -import com.p4square.restlet.oauth.OAuthException; -import com.p4square.restlet.oauth.OAuthUser; - -import org.restlet.Context; -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.ext.jackson.JacksonRepresentation; -import org.restlet.security.Verifier; - -/** - * Restlet Verifier for F1 2nd Party Authentication - * - * @author Jesse Morgan - */ -public class SecondPartyVerifier implements Verifier { - private static final Logger LOG = Logger.getLogger(SecondPartyVerifier.class); - - private final Restlet mDispatcher; - private final F1Access mHelper; - - public SecondPartyVerifier(Context context, F1Access helper) { - if (helper == null) { - throw new IllegalArgumentException("Helper can not be null."); - } - - mDispatcher = context.getClientDispatcher(); - mHelper = helper; - } - - @Override - public int verify(Request request, Response response) { - if (request.getChallengeResponse() == null) { - return RESULT_MISSING; // no credentials - } - - String username = request.getChallengeResponse().getIdentifier(); - String password = new String(request.getChallengeResponse().getSecret()); - - try { - OAuthUser ouser = mHelper.getAccessToken(username, password); - - // Once we have a user, fetch the people record to get the user id. - F1User user = mHelper.getAuthenticatedApi(ouser).getF1User(ouser); - user.setEmail(username); - - // This seems like a hack... but it'll work - request.getClientInfo().setUser(user); - - return RESULT_VALID; - - } catch (Exception e) { - LOG.info("OAuth Exception: " + e, e); - } - - return RESULT_INVALID; // Invalid credentials - } - -} diff --git a/src/com/p4square/fmfacade/FMFacade.java b/src/com/p4square/fmfacade/FMFacade.java deleted file mode 100644 index 0e552b0..0000000 --- a/src/com/p4square/fmfacade/FMFacade.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade; - -import java.io.IOException; - -import org.restlet.Application; -import org.restlet.Component; -import org.restlet.data.Protocol; -import org.restlet.Restlet; -import org.restlet.routing.Router; - -import freemarker.template.Configuration; -import freemarker.template.DefaultObjectWrapper; -import freemarker.template.Template; - -import org.apache.log4j.Logger; - -import com.p4square.grow.config.Config; - -/** - * - * @author Jesse Morgan - */ -public class FMFacade extends Application { - private static final Logger cLog = Logger.getLogger(FMFacade.class); - private final Configuration mFMConfig; - - public FMFacade() { - mFMConfig = new Configuration(); - mFMConfig.setClassForTemplateLoading(getClass(), "/templates"); - mFMConfig.setObjectWrapper(new DefaultObjectWrapper()); - } - - /** - * @return a Config object. - */ - public Config getConfig() { - return null; - } - - @Override - public synchronized Restlet createInboundRoot() { - return createRouter(); - } - - /** - * Retrieve a template. - * - * @param name The template name. - * @return A FreeMarker template or null on error. - */ - public Template getTemplate(String name) { - try { - return mFMConfig.getTemplate(name); - - } catch (IOException e) { - cLog.error("Could not load template \"" + name + "\"", e); - return null; - } - } - - /** - * Create the router to be used by this application. This can be overriden - * by sub-classes to add additional routes. - * - * @return The router. - */ - protected Router createRouter() { - Router router = new Router(getContext()); - router.attachDefault(FreeMarkerPageResource.class); - - return router; - } - - /** - * 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.getDefaultHost().attach(new FMFacade()); - - // Setup shutdown hook - Runtime.getRuntime().addShutdownHook(new Thread() { - public void run() { - try { - component.stop(); - } catch (Exception e) { - cLog.error("Exception during cleanup", e); - } - } - }); - - cLog.info("Starting server..."); - - try { - component.start(); - } catch (Exception e) { - cLog.fatal("Could not start: " + e.getMessage(), e); - } - } -} diff --git a/src/com/p4square/fmfacade/FreeMarkerPageResource.java b/src/com/p4square/fmfacade/FreeMarkerPageResource.java deleted file mode 100644 index 8c8948a..0000000 --- a/src/com/p4square/fmfacade/FreeMarkerPageResource.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade; - -import java.util.Map; -import java.util.HashMap; - -import freemarker.template.Template; - -import org.restlet.Context; -import org.restlet.data.MediaType; -import org.restlet.data.Status; -import org.restlet.ext.freemarker.TemplateRepresentation; -import org.restlet.representation.Representation; -import org.restlet.resource.ServerResource; -import org.restlet.security.User; - -import org.apache.log4j.Logger; - -import com.p4square.fmfacade.ftl.GetMethod; - -import com.p4square.session.Session; -import com.p4square.session.Sessions; - -/** - * - * @author Jesse Morgan - */ -public class FreeMarkerPageResource extends ServerResource { - private static Logger cLog = Logger.getLogger(FreeMarkerPageResource.class); - - public static Map baseRootObject(final Context context, final FMFacade fmf) { - Map root = new HashMap(); - - root.put("get", new GetMethod(context.getClientDispatcher())); - root.put("config", fmf.getConfig()); - - return root; - } - - private FMFacade mFMF; - private String mCurrentPage; - - @Override - public void doInit() { - mFMF = (FMFacade) getApplication(); - mCurrentPage = getReference().getRemainingPart(false, false); - } - - protected Representation get() { - try { - Template t = mFMF.getTemplate("pages" + mCurrentPage + ".ftl"); - - if (t == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - return new TemplateRepresentation(t, getRootObject(), - MediaType.TEXT_HTML); - - } catch (Exception e) { - cLog.fatal("Could not render page: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } - - /** - * Build and return the root object to pass to the FTL Template. - * @return A map of objects and methods for the template to access. - */ - protected Map getRootObject() { - Map root = baseRootObject(getContext(), mFMF); - - root.put("attributes", getRequestAttributes()); - root.put("query", getQuery().getValuesMap()); - - if (getClientInfo().isAuthenticated()) { - final User user = getClientInfo().getUser(); - final Map userMap = new HashMap(); - userMap.put("id", user.getIdentifier()); - userMap.put("firstName", user.getFirstName()); - userMap.put("lastName", user.getLastName()); - userMap.put("email", user.getEmail()); - root.put("user", userMap); - } - - Session s = Sessions.getInstance().get(getRequest()); - if (s != null) { - root.put("session", s.getMap()); - } - - return root; - } -} diff --git a/src/com/p4square/fmfacade/ftl/GetMethod.java b/src/com/p4square/fmfacade/ftl/GetMethod.java deleted file mode 100644 index a47c4b0..0000000 --- a/src/com/p4square/fmfacade/ftl/GetMethod.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade.ftl; - -import java.util.List; -import java.util.Map; -import java.util.HashMap; - -import java.io.IOException; - -import freemarker.core.Environment; -import freemarker.template.SimpleScalar; -import freemarker.template.TemplateMethodModel; -import freemarker.template.TemplateModel; -import freemarker.template.TemplateModelException; - -import org.apache.log4j.Logger; - -import org.restlet.data.Status; -import org.restlet.data.Method; -import org.restlet.representation.Representation; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; - -import org.restlet.ext.jackson.JacksonRepresentation; - -/** - * This method allows templates to make GET requests. - * - * @author Jesse Morgan - */ -public class GetMethod implements TemplateMethodModel { - private static final Logger cLog = Logger.getLogger(GetMethod.class); - - private final Restlet mDispatcher; - - public GetMethod(Restlet dispatcher) { - mDispatcher = dispatcher; - } - - /** - * @param args List with exactly two arguments: - * * The variable in which to put the result. - * * The URI to GET. - */ - public TemplateModel exec(List args) throws TemplateModelException { - final Environment env = Environment.getCurrentEnvironment(); - - if (args.size() != 2) { - throw new TemplateModelException( - "Expecting exactly one argument containing the URI"); - } - - Request request = new Request(Method.GET, (String) args.get(1)); - Response response = mDispatcher.handle(request); - Status status = response.getStatus(); - Representation representation = response.getEntity(); - - try { - if (response.getStatus().isSuccess()) { - JacksonRepresentation mapRepresentation; - if (representation instanceof JacksonRepresentation) { - mapRepresentation = (JacksonRepresentation) representation; - } else { - mapRepresentation = new JacksonRepresentation( - representation, Map.class); - } - try { - TemplateModel mapModel = env.getObjectWrapper().wrap(mapRepresentation.getObject()); - - env.setVariable((String) args.get(0), mapModel); - - } catch (IOException e) { - cLog.warn("Exception occurred when calling getObject(): " - + e.getMessage(), e); - status = Status.SERVER_ERROR_INTERNAL; - } - } - - Map statusMap = new HashMap(); - statusMap.put("code", status.getCode()); - statusMap.put("reason", status.getReasonPhrase()); - statusMap.put("succeeded", status.isSuccess()); - return env.getObjectWrapper().wrap(statusMap); - } finally { - if (representation != null) { - representation.release(); - } - } - } -} diff --git a/src/com/p4square/fmfacade/json/ClientException.java b/src/com/p4square/fmfacade/json/ClientException.java deleted file mode 100644 index c233193..0000000 --- a/src/com/p4square/fmfacade/json/ClientException.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade.json; - -/** - * - * @author Jesse Morgan - */ -public class ClientException extends Exception { - - public ClientException(final String msg) { - super(msg); - } - - public ClientException(final String msg, final Exception cause) { - super(msg, cause); - } -} diff --git a/src/com/p4square/fmfacade/json/JsonRequestClient.java b/src/com/p4square/fmfacade/json/JsonRequestClient.java deleted file mode 100644 index 19a394f..0000000 --- a/src/com/p4square/fmfacade/json/JsonRequestClient.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade.json; - -import java.util.Map; - -import java.io.IOException; - -import org.apache.log4j.Logger; - -import org.restlet.data.Status; -import org.restlet.data.Method; -import org.restlet.representation.Representation; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; - -import org.restlet.ext.jackson.JacksonRepresentation; - -/** - * - * @author Jesse Morgan - */ -public class JsonRequestClient { - private final Restlet mDispatcher; - - public JsonRequestClient(Restlet dispatcher) { - mDispatcher = dispatcher; - } - - /** - * Perform a GET request for the given URI and parse the response as a - * JSON map. - * - * @return A JsonResponse object which can be used to retrieve the - * response as a JSON map. - */ - public JsonResponse get(final String uri) { - final Request request = new Request(Method.GET, uri); - final Response response = mDispatcher.handle(request); - - return new JsonResponse(response); - } - - /** - * Perform a PUT request for the given URI and parse the response as a - * JSON map. - * - * @return A JsonResponse object which can be used to retrieve the - * response as a JSON map. - */ - public JsonResponse put(final String uri, Representation entity) { - final Request request = new Request(Method.PUT, uri); - request.setEntity(entity); - - final Response response = mDispatcher.handle(request); - return new JsonResponse(response); - } - - /** - * Perform a PUT request for the given URI and parse the response as a - * JSON map. - * - * @return A JsonResponse object which can be used to retrieve the - * response as a JSON map. - */ - public JsonResponse put(final String uri, Map map) { - return put(uri, new JacksonRepresentation(map)); - } - - /** - * Perform a POST request for the given URI and parse the response as a - * JSON map. - * - * @return A JsonResponse object which can be used to retrieve the - * response as a JSON map. - */ - public JsonResponse post(final String uri, Representation entity) { - final Request request = new Request(Method.POST, uri); - request.setEntity(entity); - - final Response response = mDispatcher.handle(request); - return new JsonResponse(response); - } - - /** - * Perform a POST request for the given URI and parse the response as a - * JSON map. - * - * @return A JsonResponse object which can be used to retrieve the - * response as a JSON map. - */ - public JsonResponse post(final String uri, Map map) { - return post(uri, new JacksonRepresentation(map)); - } - - /** - * Perform a DELETE request for the given URI. - * - * @return A JsonResponse object with the status of the request. - */ - public JsonResponse delete(final String uri) { - final Request request = new Request(Method.DELETE, uri); - final Response response = mDispatcher.handle(request); - return new JsonResponse(response); - } -} diff --git a/src/com/p4square/fmfacade/json/JsonResponse.java b/src/com/p4square/fmfacade/json/JsonResponse.java deleted file mode 100644 index b9cb587..0000000 --- a/src/com/p4square/fmfacade/json/JsonResponse.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.fmfacade.json; - -import java.util.Map; - -import java.io.IOException; - -import org.restlet.data.Status; -import org.restlet.data.Reference; -import org.restlet.representation.Representation; -import org.restlet.Response; - -import org.restlet.ext.jackson.JacksonRepresentation; - -/** - * JsonResponse wraps a Restlet Response object and parses the entity, if any, - * as a JSON map. - * - * @author Jesse Morgan - */ -public class JsonResponse { - private final Response mResponse; - private final Representation mRepresentation; - - private Map mMap; - - JsonResponse(Response response) { - mResponse = response; - mRepresentation = response.getEntity(); - mMap = null; - - if (!response.getStatus().isSuccess()) { - if (mRepresentation != null) { - mRepresentation.release(); - } - } - } - - /** - * @return the Status info from the response. - */ - public Status getStatus() { - return mResponse.getStatus(); - } - - /** - * @return the Reference for a redirect. - */ - public Reference getRedirectLocation() { - return mResponse.getLocationRef(); - } - - /** - * Return the parsed json map from the response. - */ - public Map getMap() throws ClientException { - if (mMap == null) { - Representation representation = mRepresentation; - - // Parse response - if (representation == null) { - return null; - } - - JacksonRepresentation mapRepresentation; - if (representation instanceof JacksonRepresentation) { - mapRepresentation = (JacksonRepresentation) representation; - } else { - mapRepresentation = new JacksonRepresentation( - representation, Map.class); - } - - try { - mMap = (Map) mapRepresentation.getObject(); - - } catch (IOException e) { - throw new ClientException("Failed to parse response: " + e.getMessage(), e); - } - } - - return mMap; - } - -} diff --git a/src/com/p4square/grow/GrowProcessComponent.java b/src/com/p4square/grow/GrowProcessComponent.java deleted file mode 100644 index f63538c..0000000 --- a/src/com/p4square/grow/GrowProcessComponent.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import com.codahale.metrics.ConsoleReporter; -import com.codahale.metrics.MetricRegistry; - -import org.apache.log4j.Logger; - -import org.restlet.Application; -import org.restlet.Component; -import org.restlet.Restlet; -import org.restlet.data.ChallengeScheme; -import org.restlet.data.Protocol; -import org.restlet.resource.Directory; -import org.restlet.security.ChallengeAuthenticator; - -import com.p4square.grow.backend.BackendVerifier; -import com.p4square.grow.backend.GrowBackend; -import com.p4square.grow.config.Config; -import com.p4square.grow.frontend.GrowFrontend; -import com.p4square.restlet.metrics.MetricsApplication; - -/** - * - * @author Jesse Morgan - */ -public class GrowProcessComponent extends Component { - private static Logger LOG = Logger.getLogger(GrowProcessComponent.class); - - private static final String BACKEND_REALM = "Grow Backend"; - - private final Config mConfig; - private final MetricRegistry mMetricRegistry; - - /** - * Create a new Grow Process website component combining a frontend and backend. - */ - public GrowProcessComponent() throws Exception { - this(new Config()); - } - - public GrowProcessComponent(Config config) throws Exception { - // Clients - getClients().add(Protocol.FILE); - getClients().add(Protocol.HTTP); - getClients().add(Protocol.HTTPS); - - // Prepare mConfig - mConfig = config; - mConfig.updateConfig(this.getClass().getResourceAsStream("/grow.properties")); - - // Prepare Metrics - mMetricRegistry = new MetricRegistry(); - - // Frontend - GrowFrontend frontend = new GrowFrontend(mConfig, mMetricRegistry); - getDefaultHost().attach(frontend); - - // Backend - GrowBackend backend = new GrowBackend(mConfig, mMetricRegistry); - getInternalRouter().attach("/backend", backend); - - // Authenticated access to the backend - BackendVerifier verifier = new BackendVerifier(backend.getUserRecordProvider()); - ChallengeAuthenticator auth = new ChallengeAuthenticator(getContext().createChildContext(), - false, ChallengeScheme.HTTP_BASIC, BACKEND_REALM, verifier); - auth.setNext(backend); - getDefaultHost().attach("/backend", auth); - - // Authenticated access to metrics - ChallengeAuthenticator metricAuth = new ChallengeAuthenticator( - getContext().createChildContext(), false, - ChallengeScheme.HTTP_BASIC, BACKEND_REALM, verifier); - metricAuth.setNext(new MetricsApplication(mMetricRegistry)); - getDefaultHost().attach("/metrics", metricAuth); - } - - - @Override - public void start() throws Exception { - String configDomain = getContext().getParameters().getFirstValue("com.p4square.grow.configDomain"); - if (configDomain != null) { - mConfig.setDomain(configDomain); - } - - String configFilename = getContext().getParameters().getFirstValue("com.p4square.grow.configFile"); - if (configFilename != null) { - mConfig.updateConfig(configFilename); - } - - super.start(); - } - - /** - * Stand-alone main for testing. - */ - public static void main(String[] args) throws Exception { - // Load an optional config file from the first argument. - Config config = new Config(); - config.setDomain("dev"); - if (args.length >= 1) { - config.updateConfig(args[0]); - } - - // Override domain - if (args.length == 2) { - config.setDomain(args[1]); - } - - // Start the HTTP Server - final GrowProcessComponent component = new GrowProcessComponent(config); - component.getServers().add(Protocol.HTTP, 8085); - - // 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 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/com/p4square/grow/backend/BackendVerifier.java b/src/com/p4square/grow/backend/BackendVerifier.java deleted file mode 100644 index 83160a9..0000000 --- a/src/com/p4square/grow/backend/BackendVerifier.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend; - -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.apache.commons.codec.binary.Hex; - -import org.restlet.security.SecretVerifier; - -import com.p4square.grow.model.UserRecord; -import com.p4square.grow.provider.Provider; - -/** - * Verify the given credentials against the users with backend access. - */ -public class BackendVerifier extends SecretVerifier { - - private final Provider mUserProvider; - - public BackendVerifier(Provider userProvider) { - mUserProvider = userProvider; - } - - @Override - public int verify(String identifier, char[] secret) { - if (identifier == null) { - throw new IllegalArgumentException("Null identifier"); - } - - if (secret == null) { - throw new IllegalArgumentException("Null secret"); - } - - // Does the user exist? - UserRecord user; - try { - user = mUserProvider.get(identifier); - if (user == null) { - return RESULT_UNKNOWN; - } - - } catch (IOException e) { - return RESULT_UNKNOWN; - } - - // Does the user have a backend password? - String storedHash = user.getBackendPasswordHash(); - if (storedHash == null) { - // This user doesn't have access - return RESULT_INVALID; - } - - // Validate the password. - try { - String hashedInput = hashPassword(secret); - if (hashedInput.equals(storedHash)) { - return RESULT_VALID; - } - - } catch (NoSuchAlgorithmException e) { - return RESULT_UNSUPPORTED; - } - - // If all else fails, fail. - return RESULT_INVALID; - } - - /** - * Hash the given secret. - */ - public static String hashPassword(char[] secret) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - - // Convert the char[] to byte[] - // FIXME This approach is incorrectly truncating multibyte - // characters. - byte[] b = new byte[secret.length]; - for (int i = 0; i < secret.length; i++) { - b[i] = (byte) secret[i]; - } - - md.update(b); - - byte[] hash = md.digest(); - return new String(Hex.encodeHex(hash)); - } -} diff --git a/src/com/p4square/grow/backend/CassandraGrowData.java b/src/com/p4square/grow/backend/CassandraGrowData.java deleted file mode 100644 index 22a7716..0000000 --- a/src/com/p4square/grow/backend/CassandraGrowData.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend; - -import java.io.IOException; - -import com.p4square.grow.config.Config; - -import com.p4square.grow.backend.db.CassandraDatabase; -import com.p4square.grow.backend.db.CassandraKey; -import com.p4square.grow.backend.db.CassandraProviderImpl; -import com.p4square.grow.backend.db.CassandraCollectionProvider; -import com.p4square.grow.backend.db.CassandraTrainingRecordProvider; - -import com.p4square.grow.model.Message; -import com.p4square.grow.model.MessageThread; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.Question; -import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.UserRecord; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.DelegateCollectionProvider; -import com.p4square.grow.provider.DelegateProvider; -import com.p4square.grow.provider.Provider; - -/** - * - * @author Jesse Morgan - */ -class CassandraGrowData implements GrowData { - private static final String DEFAULT_COLUMN = "value"; - - private final Config mConfig; - private final CassandraDatabase mDatabase; - - private final Provider mUserRecordProvider; - - private final Provider mQuestionProvider; - private final CassandraTrainingRecordProvider mTrainingRecordProvider; - private final CollectionProvider mVideoProvider; - - private final CollectionProvider mFeedThreadProvider; - private final CollectionProvider mFeedMessageProvider; - - private final Provider mStringProvider; - - private final CollectionProvider mAnswerProvider; - - public CassandraGrowData(final Config config) { - mConfig = config; - mDatabase = new CassandraDatabase(); - - mUserRecordProvider = new DelegateProvider( - new CassandraProviderImpl(mDatabase, UserRecord.class)) { - @Override - public CassandraKey makeKey(String userid) { - return new CassandraKey("accounts", userid, DEFAULT_COLUMN); - } - }; - - mQuestionProvider = new DelegateProvider( - new CassandraProviderImpl(mDatabase, Question.class)) { - @Override - public CassandraKey makeKey(String questionId) { - return new CassandraKey("strings", "/questions/" + questionId, DEFAULT_COLUMN); - } - }; - - mFeedThreadProvider = new CassandraCollectionProvider(mDatabase, - "feedthreads", MessageThread.class); - mFeedMessageProvider = new CassandraCollectionProvider(mDatabase, - "feedmessages", Message.class); - - mTrainingRecordProvider = new CassandraTrainingRecordProvider(mDatabase); - - mVideoProvider = new DelegateCollectionProvider( - new CassandraCollectionProvider(mDatabase, "strings", String.class)) { - @Override - public String makeCollectionKey(String key) { - return "/training/" + key; - } - - @Override - public String makeKey(String key) { - return key; - } - - @Override - public String unmakeKey(String key) { - return key; - } - }; - - mStringProvider = new DelegateProvider( - new CassandraProviderImpl(mDatabase, String.class)) { - @Override - public CassandraKey makeKey(String id) { - return new CassandraKey("strings", id, DEFAULT_COLUMN); - } - }; - - mAnswerProvider = new CassandraCollectionProvider( - mDatabase, "assessments", String.class); - } - - @Override - public void start() throws Exception { - mDatabase.setClusterName(mConfig.getString("clusterName", "Dev Cluster")); - mDatabase.setKeyspaceName(mConfig.getString("keyspace", "GROW")); - mDatabase.init(); - } - - @Override - public void stop() throws Exception { - mDatabase.close(); - } - - /** - * @return the current database. - */ - public CassandraDatabase getDatabase() { - return mDatabase; - } - - @Override - public Provider getUserRecordProvider() { - return mUserRecordProvider; - } - - @Override - public Provider getQuestionProvider() { - return mQuestionProvider; - } - - @Override - public Provider getTrainingRecordProvider() { - return mTrainingRecordProvider; - } - - @Override - public CollectionProvider getVideoProvider() { - return mVideoProvider; - } - - @Override - public Playlist getDefaultPlaylist() throws IOException { - return mTrainingRecordProvider.getDefaultPlaylist(); - } - - @Override - public CollectionProvider getThreadProvider() { - return mFeedThreadProvider; - } - - @Override - public CollectionProvider getMessageProvider() { - return mFeedMessageProvider; - } - - @Override - public Provider getStringProvider() { - return mStringProvider; - } - - @Override - public CollectionProvider getAnswerProvider() { - return mAnswerProvider; - } -} diff --git a/src/com/p4square/grow/backend/DynamoGrowData.java b/src/com/p4square/grow/backend/DynamoGrowData.java deleted file mode 100644 index 3b38eac..0000000 --- a/src/com/p4square/grow/backend/DynamoGrowData.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend; - -import java.io.IOException; - -import com.amazonaws.auth.AWSCredentials; - -import com.p4square.grow.backend.dynamo.DynamoDatabase; -import com.p4square.grow.backend.dynamo.DynamoKey; -import com.p4square.grow.backend.dynamo.DynamoProviderImpl; -import com.p4square.grow.backend.dynamo.DynamoCollectionProviderImpl; - -import com.p4square.grow.config.Config; - -import com.p4square.grow.model.Message; -import com.p4square.grow.model.MessageThread; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.Question; -import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.UserRecord; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.DelegateCollectionProvider; -import com.p4square.grow.provider.DelegateProvider; -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * - * @author Jesse Morgan - */ -class DynamoGrowData implements GrowData { - private static final String DEFAULT_COLUMN = "value"; - private static final String DEFAULT_PLAYLIST_KEY = "/training/defaultplaylist"; - - private final Config mConfig; - private final DynamoDatabase mDatabase; - - private final Provider mUserRecordProvider; - - private final Provider mQuestionProvider; - private final Provider mTrainingRecordProvider; - private final CollectionProvider mVideoProvider; - - private final CollectionProvider mFeedThreadProvider; - private final CollectionProvider mFeedMessageProvider; - - private final Provider mStringProvider; - - private final CollectionProvider mAnswerProvider; - - public DynamoGrowData(final Config config) { - mConfig = config; - - mDatabase = new DynamoDatabase(config); - - mUserRecordProvider = new DelegateProvider( - new DynamoProviderImpl(mDatabase, UserRecord.class)) { - @Override - public DynamoKey makeKey(String userid) { - return DynamoKey.newAttributeKey("accounts", userid, DEFAULT_COLUMN); - } - }; - - mQuestionProvider = new DelegateProvider( - new DynamoProviderImpl(mDatabase, Question.class)) { - @Override - public DynamoKey makeKey(String questionId) { - return DynamoKey.newAttributeKey("strings", - "/questions/" + questionId, - DEFAULT_COLUMN); - } - }; - - mFeedThreadProvider = new DynamoCollectionProviderImpl( - mDatabase, "feedthreads", MessageThread.class); - mFeedMessageProvider = new DynamoCollectionProviderImpl( - mDatabase, "feedmessages", Message.class); - - mTrainingRecordProvider = new DelegateProvider( - new DynamoProviderImpl(mDatabase, TrainingRecord.class)) { - @Override - public DynamoKey makeKey(String userId) { - return DynamoKey.newAttributeKey("training", - userId, - DEFAULT_COLUMN); - } - }; - - mVideoProvider = new DelegateCollectionProvider( - new DynamoCollectionProviderImpl(mDatabase, "strings", String.class)) { - @Override - public String makeCollectionKey(String key) { - return "/training/" + key; - } - - @Override - public String makeKey(String key) { - return key; - } - - @Override - public String unmakeKey(String key) { - return key; - } - }; - - mStringProvider = new DelegateProvider( - new DynamoProviderImpl(mDatabase, String.class)) { - @Override - public DynamoKey makeKey(String id) { - return DynamoKey.newAttributeKey("strings", id, DEFAULT_COLUMN); - } - }; - - mAnswerProvider = new DynamoCollectionProviderImpl( - mDatabase, "assessments", String.class); - } - - @Override - public void start() throws Exception { - } - - @Override - public void stop() throws Exception { - } - - @Override - public Provider getUserRecordProvider() { - return mUserRecordProvider; - } - - @Override - public Provider getQuestionProvider() { - return mQuestionProvider; - } - - @Override - public Provider getTrainingRecordProvider() { - return mTrainingRecordProvider; - } - - @Override - public CollectionProvider getVideoProvider() { - return mVideoProvider; - } - - @Override - public Playlist getDefaultPlaylist() throws IOException { - String blob = mStringProvider.get(DEFAULT_PLAYLIST_KEY); - if (blob == null) { - return null; - } - - return JsonEncodedProvider.MAPPER.readValue(blob, Playlist.class); - } - - @Override - public CollectionProvider getThreadProvider() { - return mFeedThreadProvider; - } - - @Override - public CollectionProvider getMessageProvider() { - return mFeedMessageProvider; - } - - @Override - public Provider getStringProvider() { - return mStringProvider; - } - - @Override - public CollectionProvider getAnswerProvider() { - return mAnswerProvider; - } -} diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java deleted file mode 100644 index 4091138..0000000 --- a/src/com/p4square/grow/backend/GrowBackend.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2012 Jesse Morgan - */ - -package com.p4square.grow.backend; - -import java.io.IOException; - -import com.codahale.metrics.MetricRegistry; - -import org.apache.log4j.Logger; - -import org.restlet.Application; -import org.restlet.Component; -import org.restlet.Restlet; -import org.restlet.data.Protocol; -import org.restlet.data.Reference; -import org.restlet.resource.Directory; -import org.restlet.routing.Router; - -import com.p4square.grow.config.Config; - -import com.p4square.grow.model.Message; -import com.p4square.grow.model.MessageThread; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.Question; -import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.UserRecord; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.ProvidesQuestions; -import com.p4square.grow.provider.ProvidesTrainingRecords; -import com.p4square.grow.provider.ProvidesUserRecords; - -import com.p4square.grow.backend.resources.AccountResource; -import com.p4square.grow.backend.resources.BannerResource; -import com.p4square.grow.backend.resources.SurveyResource; -import com.p4square.grow.backend.resources.SurveyResultsResource; -import com.p4square.grow.backend.resources.TrainingRecordResource; -import com.p4square.grow.backend.resources.TrainingResource; - -import com.p4square.grow.backend.feed.FeedDataProvider; -import com.p4square.grow.backend.feed.ThreadResource; -import com.p4square.grow.backend.feed.TopicResource; - -import com.p4square.restlet.metrics.MetricRouter; - -/** - * Main class for the backend application. - * - * @author Jesse Morgan - */ -public class GrowBackend extends Application implements GrowData { - - private final static Logger LOG = Logger.getLogger(GrowBackend.class); - - private final MetricRegistry mMetricRegistry; - - private final Config mConfig; - private final GrowData mGrowData; - - public GrowBackend() { - this(new Config(), new MetricRegistry()); - } - - public GrowBackend(Config config, MetricRegistry metricRegistry) { - mConfig = config; - - mMetricRegistry = metricRegistry; - - mGrowData = new DynamoGrowData(config); - } - - public MetricRegistry getMetrics() { - return mMetricRegistry; - } - - @Override - public Restlet createInboundRoot() { - Router router = new MetricRouter(getContext(), mMetricRegistry); - - // Account API - router.attach("/accounts/{userId}", AccountResource.class); - - // Survey API - router.attach("/assessment/question/{questionId}", SurveyResource.class); - - router.attach("/accounts/{userId}/assessment", SurveyResultsResource.class); - router.attach("/accounts/{userId}/assessment/answers/{questionId}", - SurveyResultsResource.class); - - // Training API - router.attach("/training/{level}", TrainingResource.class); - router.attach("/training/{level}/videos/{videoId}", TrainingResource.class); - - router.attach("/accounts/{userId}/training", TrainingRecordResource.class); - router.attach("/accounts/{userId}/training/videos/{videoId}", - TrainingRecordResource.class); - - // Misc. - router.attach("/banner", BannerResource.class); - - // Feed - router.attach("/feed/{topic}", TopicResource.class); - router.attach("/feed/{topic}/{thread}", ThreadResource.class); - //router.attach("/feed/{topic/{thread}/{message}", MessageResource.class); - - router.attachDefault(new Directory(getContext(), new Reference(getClass().getResource("apiinfo.html")))); - - return router; - } - - /** - * Open the database. - */ - @Override - public void start() throws Exception { - super.start(); - - mGrowData.start(); - } - - /** - * Close the database. - */ - @Override - public void stop() throws Exception { - LOG.info("Shutting down..."); - mGrowData.stop(); - - super.stop(); - } - - @Override - public Provider getUserRecordProvider() { - return mGrowData.getUserRecordProvider(); - } - - @Override - public Provider getQuestionProvider() { - return mGrowData.getQuestionProvider(); - } - - @Override - public CollectionProvider getVideoProvider() { - return mGrowData.getVideoProvider(); - } - - @Override - public Provider getTrainingRecordProvider() { - return mGrowData.getTrainingRecordProvider(); - } - - /** - * @return the Default Playlist. - */ - public Playlist getDefaultPlaylist() throws IOException { - return mGrowData.getDefaultPlaylist(); - } - - @Override - public CollectionProvider getThreadProvider() { - return mGrowData.getThreadProvider(); - } - - @Override - public CollectionProvider getMessageProvider() { - return mGrowData.getMessageProvider(); - } - - @Override - public Provider getStringProvider() { - return mGrowData.getStringProvider(); - } - - @Override - public CollectionProvider getAnswerProvider() { - return mGrowData.getAnswerProvider(); - } - - /** - * Stand-alone main for testing. - */ - public static void main(String[] args) throws Exception { - // Start the HTTP Server - final Component component = new Component(); - component.getServers().add(Protocol.HTTP, 9095); - component.getClients().add(Protocol.HTTP); - component.getDefaultHost().attach(new GrowBackend()); - - // 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); - } - } -} diff --git a/src/com/p4square/grow/backend/GrowData.java b/src/com/p4square/grow/backend/GrowData.java deleted file mode 100644 index 293bb88..0000000 --- a/src/com/p4square/grow/backend/GrowData.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend; - -import com.p4square.grow.backend.feed.FeedDataProvider; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.provider.ProvidesAssessments; -import com.p4square.grow.provider.ProvidesQuestions; -import com.p4square.grow.provider.ProvidesStrings; -import com.p4square.grow.provider.ProvidesTrainingRecords; -import com.p4square.grow.provider.ProvidesUserRecords; -import com.p4square.grow.provider.ProvidesVideos; - -/** - * Aggregate of the data provider interfaces. - * - * Used by GrowBackend to swap out implementations of the providers. - * - * @author Jesse Morgan - */ -interface GrowData extends ProvidesQuestions, ProvidesTrainingRecords, ProvidesVideos, - FeedDataProvider, ProvidesUserRecords, ProvidesStrings, - ProvidesAssessments { - - /** - * Start the data provider. - */ - void start() throws Exception; - - /** - * Stop the data provider. - */ - void stop() throws Exception; -} diff --git a/src/com/p4square/grow/backend/apiinfo.html b/src/com/p4square/grow/backend/apiinfo.html deleted file mode 100644 index a3637c9..0000000 --- a/src/com/p4square/grow/backend/apiinfo.html +++ /dev/null @@ -1,41 +0,0 @@ - - -API Info - - -
-
/backend/accounts/{userId}
-
GET information about userId or PUT new information.
- -
/backend/assessment/question/{questionId}
-
GET information about questionId. Special questionIds: first identifies first question. count returns total number of questions.
- -
/backend/accounts/{userId}/assessment
-
GET the assessment summary for userId or DELETE userId's assessment.
- -
/backend/accounts/{userId}/assessment/answers/{questionId}
-
GET userId's answer to questionId, PUT a new answer, or DELETE an answer.
- -
/backend/training/{level}
-
GET all video information for level.
- -
/backend/training/{level}/videos/{videoId}
-
GET video information for videoId in level.
- -
/backend/accounts/{userId}/training
-
GET training record summary for userId.
- -
/backend/accounts/{userId}/training/videos/{videoId}
-
GET training record for userId and videoId or PUT a new record.
- -
/backend/banner
-
GET the info banner or PUT new banner info.
- -
/backend/feed/{topic}
-
Get all threads for forum topic.
- -
/backend/feed/{topic}/{thread}
-
Get all responses to question thread on forum topic.
-
- - diff --git a/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java b/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java deleted file mode 100644 index bfcb48d..0000000 --- a/src/com/p4square/grow/backend/db/CassandraCollectionProvider.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.db; - -import java.io.IOException; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.netflix.astyanax.model.Column; -import com.netflix.astyanax.model.ColumnList; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * CollectionProvider implementation backed by a Cassandra ColumnFamily. - * - * @author Jesse Morgan - */ -public class CassandraCollectionProvider implements CollectionProvider { - private final CassandraDatabase mDb; - private final String mCF; - private final Class mClazz; - - public CassandraCollectionProvider(CassandraDatabase db, String columnFamily, Class clazz) { - mDb = db; - mCF = columnFamily; - mClazz = clazz; - } - - @Override - public V get(String collection, String key) throws IOException { - String blob = mDb.getKey(mCF, collection, key); - return decode(blob); - } - - @Override - public Map query(String collection) throws IOException { - return query(collection, -1); - } - - @Override - public Map query(String collection, int limit) throws IOException { - Map result = new LinkedHashMap<>(); - - ColumnList row = mDb.getRow(mCF, collection); - if (!row.isEmpty()) { - int count = 0; - for (Column c : row) { - if (limit >= 0 && ++count > limit) { - break; // Limit reached. - } - - String key = c.getName(); - String blob = c.getStringValue(); - V obj = decode(blob); - - result.put(key, obj); - } - } - - return Collections.unmodifiableMap(result); - } - - @Override - public void put(String collection, String key, V obj) throws IOException { - String blob = encode(obj); - mDb.putKey(mCF, collection, key, blob); - } - - /** - * Encode the object as JSON. - * - * @param obj The object to encode. - * @return The JSON encoding of obj. - * @throws IOException if the object cannot be encoded. - */ - protected String encode(V obj) throws IOException { - if (mClazz == String.class) { - return (String) obj; - } else { - return JsonEncodedProvider.MAPPER.writeValueAsString(obj); - } - } - - /** - * Decode the JSON string as an object. - * - * @param blob The JSON data to decode. - * @return The decoded object or null if blob is null. - * @throws IOException If an object cannot be decoded. - */ - protected V decode(String blob) throws IOException { - if (blob == null) { - return null; - } - - if (mClazz == String.class) { - return (V) blob; - } - - V obj = JsonEncodedProvider.MAPPER.readValue(blob, mClazz); - return obj; - } -} diff --git a/src/com/p4square/grow/backend/db/CassandraDatabase.java b/src/com/p4square/grow/backend/db/CassandraDatabase.java deleted file mode 100644 index b8cb6df..0000000 --- a/src/com/p4square/grow/backend/db/CassandraDatabase.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.db; - -import com.netflix.astyanax.AstyanaxContext; -import com.netflix.astyanax.connectionpool.exceptions.ConnectionException; -import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl; -import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor; -import com.netflix.astyanax.connectionpool.NodeDiscoveryType; -import com.netflix.astyanax.connectionpool.OperationResult; -import com.netflix.astyanax.impl.AstyanaxConfigurationImpl; -import com.netflix.astyanax.Keyspace; -import com.netflix.astyanax.ColumnMutation; -import com.netflix.astyanax.model.Column; -import com.netflix.astyanax.model.ColumnFamily; -import com.netflix.astyanax.model.ColumnList; -import com.netflix.astyanax.ColumnListMutation; -import com.netflix.astyanax.MutationBatch; -import com.netflix.astyanax.serializers.StringSerializer; -import com.netflix.astyanax.thrift.ThriftFamilyFactory; - -import org.apache.log4j.Logger; - -/** - * Cassandra Database Abstraction for the Backend. - * - * @author Jesse Morgan - */ -public class CassandraDatabase { - private static Logger cLog = Logger.getLogger(CassandraDatabase.class); - - // Configuration fields. - private String mClusterName; - private String mKeyspaceName; - private String mSeedEndpoint = "127.0.0.1:9160"; - private int mPort = 9160; - - private AstyanaxContext mContext; - private Keyspace mKeyspace; - - /** - * Connect to Cassandra. - * - * Cluster and Keyspace must be set before calling init(). - */ - public void init() { - mContext = new AstyanaxContext.Builder() - .forCluster(mClusterName) - .forKeyspace(mKeyspaceName) - .withAstyanaxConfiguration(new AstyanaxConfigurationImpl() - .setDiscoveryType(NodeDiscoveryType.RING_DESCRIBE) - ) - .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("MyConnectionPool") - .setPort(mPort) - .setMaxConnsPerHost(1) - .setSeeds(mSeedEndpoint) - ) - .withConnectionPoolMonitor(new CountingConnectionPoolMonitor()) - .buildKeyspace(ThriftFamilyFactory.getInstance()); - - mContext.start(); - mKeyspace = mContext.getClient(); - } - - /** - * Close the database connection. - */ - public void close() { - mContext.shutdown(); - } - - /** - * Set the cluster name to connect to. - */ - public void setClusterName(final String cluster) { - mClusterName = cluster; - } - - /** - * Set the name of the keyspace to open. - */ - public void setKeyspaceName(final String keyspace) { - mKeyspaceName = keyspace; - } - - /** - * Change the seed endpoint. - * The default is 127.0.0.1:9160. - */ - public void setSeedEndpoint(final String endpoint) { - mSeedEndpoint = endpoint; - } - - /** - * Change the port to connect to. - * The default is 9160. - */ - public void setPort(final int port) { - mPort = port; - } - - /** - * @return The entire row associated with this key. - */ - public ColumnList getRow(final String cfName, final String key) { - try { - ColumnFamily cf = new ColumnFamily(cfName, - StringSerializer.get(), - StringSerializer.get()); - - OperationResult> result = - mKeyspace.prepareQuery(cf) - .getKey(key) - .execute(); - - return result.getResult(); - - } catch (ConnectionException e) { - cLog.error("getRow failed due to Connection Exception", e); - throw new RuntimeException(e); - } - } - - /** - * @return The value associated with the given key. - */ - public String getKey(final String cfName, final String key) { - return getKey(cfName, key, "value"); - } - - /** - * @return The value associated with the given key, column pair. - */ - public String getKey(final String cfName, final String key, final String column) { - final ColumnList row = getRow(cfName, key); - - if (row != null) { - final Column rowColumn = row.getColumnByName(column); - if (rowColumn != null) { - return rowColumn.getStringValue(); - } - } - - return null; - } - - /** - * Assign value to key. - */ - public void putKey(final String cfName, final String key, final String value) { - putKey(cfName, key, "value", value); - } - - /** - * Assign value to the key, column pair. - */ - public void putKey(final String cfName, final String key, - final String column, final String value) { - - ColumnFamily cf = new ColumnFamily(cfName, - StringSerializer.get(), - StringSerializer.get()); - - MutationBatch m = mKeyspace.prepareMutationBatch(); - m.withRow(cf, key).putColumn(column, value); - - try { - m.execute(); - } catch (ConnectionException e) { - cLog.error("putKey failed due to Connection Exception", e); - throw new RuntimeException(e); - } - } - - /** - * Remove a key, column pair. - */ - public void deleteKey(final String cfName, final String key, final String column) { - ColumnFamily cf = new ColumnFamily(cfName, - StringSerializer.get(), - StringSerializer.get()); - - try { - ColumnMutation m = mKeyspace.prepareColumnMutation(cf, key, column); - m.deleteColumn().execute(); - } catch (ConnectionException e) { - cLog.error("deleteKey failed due to Connection Exception", e); - throw new RuntimeException(e); - } - } - - /** - * Remove a row - */ - public void deleteRow(final String cfName, final String key) { - ColumnFamily cf = new ColumnFamily(cfName, - StringSerializer.get(), - StringSerializer.get()); - - try { - MutationBatch batch = mKeyspace.prepareMutationBatch(); - ColumnListMutation cfm = batch.withRow(cf, key).delete(); - batch.execute(); - - } catch (ConnectionException e) { - cLog.error("deleteRow failed due to Connection Exception", e); - throw new RuntimeException(e); - } - } -} diff --git a/src/com/p4square/grow/backend/db/CassandraKey.java b/src/com/p4square/grow/backend/db/CassandraKey.java deleted file mode 100644 index 853fe96..0000000 --- a/src/com/p4square/grow/backend/db/CassandraKey.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.db; - -/** - * CassandraKey represents a Cassandra key / column pair. - * - * @author Jesse Morgan - */ -public class CassandraKey { - private final String mColumnFamily; - private final String mId; - private final String mColumn; - - public CassandraKey(String columnFamily, String id, String column) { - mColumnFamily = columnFamily; - mId = id; - mColumn = column; - } - - public String getColumnFamily() { - return mColumnFamily; - } - - public String getId() { - return mId; - } - - public String getColumn() { - return mColumn; - } -} diff --git a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java deleted file mode 100644 index da5a9f2..0000000 --- a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.db; - -import java.io.IOException; - -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * Provider implementation backed by a Cassandra ColumnFamily. - * - * @author Jesse Morgan - */ -public class CassandraProviderImpl extends JsonEncodedProvider implements Provider { - private final CassandraDatabase mDb; - - public CassandraProviderImpl(CassandraDatabase db, Class clazz) { - super(clazz); - - mDb = db; - } - - @Override - public V get(CassandraKey key) throws IOException { - String blob = mDb.getKey(key.getColumnFamily(), key.getId(), key.getColumn()); - return decode(blob); - } - - @Override - public void put(CassandraKey key, V obj) throws IOException { - String blob = encode(obj); - mDb.putKey(key.getColumnFamily(), key.getId(), key.getColumn(), blob); - } -} diff --git a/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java b/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java deleted file mode 100644 index 4face52..0000000 --- a/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.db; - -import java.io.IOException; - -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.TrainingRecord; - -import com.p4square.grow.provider.JsonEncodedProvider; -import com.p4square.grow.provider.Provider; - -/** - * - * @author Jesse Morgan - */ -public class CassandraTrainingRecordProvider implements Provider { - private static final CassandraKey DEFAULT_PLAYLIST_KEY = new CassandraKey("strings", "defaultPlaylist", "value"); - - private static final String COLUMN_FAMILY = "training"; - private static final String PLAYLIST_KEY = "playlist"; - private static final String LAST_VIDEO_KEY = "lastVideo"; - - private final CassandraDatabase mDb; - private final Provider mPlaylistProvider; - - public CassandraTrainingRecordProvider(CassandraDatabase db) { - mDb = db; - mPlaylistProvider = new CassandraProviderImpl<>(db, Playlist.class); - } - - @Override - public TrainingRecord get(String userid) throws IOException { - Playlist playlist = mPlaylistProvider.get(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY)); - - if (playlist == null) { - // We consider no playlist to mean no record whatsoever. - return null; - } - - TrainingRecord r = new TrainingRecord(); - r.setPlaylist(playlist); - r.setLastVideo(mDb.getKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY)); - - return r; - } - - @Override - public void put(String userid, TrainingRecord record) throws IOException { - String lastVideo = record.getLastVideo(); - Playlist playlist = record.getPlaylist(); - - mDb.putKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY, lastVideo); - mPlaylistProvider.put(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY), playlist); - } - - /** - * @return the default playlist stored in the database. - */ - public Playlist getDefaultPlaylist() throws IOException { - Playlist playlist = mPlaylistProvider.get(DEFAULT_PLAYLIST_KEY); - - if (playlist == null) { - playlist = new Playlist(); - } - - return playlist; - } -} diff --git a/src/com/p4square/grow/backend/dynamo/DbTool.java b/src/com/p4square/grow/backend/dynamo/DbTool.java deleted file mode 100644 index 374fa83..0000000 --- a/src/com/p4square/grow/backend/dynamo/DbTool.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend.dynamo; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; - -import com.p4square.grow.backend.dynamo.DynamoDatabase; -import com.p4square.grow.backend.dynamo.DynamoKey; -import com.p4square.grow.config.Config; -import com.p4square.grow.model.UserRecord; -import com.p4square.grow.provider.Provider; - -/** - * - * @author Jesse Morgan - */ -public class DbTool { - private static final FilenameFilter JSON_FILTER = new JsonFilter(); - - private static Config mConfig; - private static DynamoDatabase mDatabase; - - public static void usage() { - System.out.println("java com.p4square.grow.backend.dynamo.DbTool ...\n"); - System.out.println("Commands:"); - System.out.println("\t--domain Set config domain"); - System.out.println("\t--dev Set config domain to dev"); - System.out.println("\t--config Merge in config file"); - System.out.println("\t--list List all tables"); - System.out.println("\t--create Create a table"); - System.out.println("\t--update
Update table throughput"); - System.out.println("\t--drop
Delete a table"); - System.out.println("\t--get
Get a value"); - System.out.println("\t--put
Put a value"); - System.out.println("\t--delete
Delete a value"); - System.out.println("\t--scan
List all rows"); - System.out.println("\t--scanf
List all rows"); - System.out.println(); - System.out.println("Bootstrap Commands:"); - System.out.println("\t--bootstrap Create all tables and import all data"); - System.out.println("\t--loadStrings Load all videos and questions"); - System.out.println("\t--destroy Drop all tables"); - System.out.println("\t--addadmin Add a backend account"); - System.out.println("\t--import
Backfill a table"); - } - - public static void main(String... args) { - if (args.length == 0) { - usage(); - System.exit(1); - } - - mConfig = new Config(); - - try { - mConfig.updateConfig(DbTool.class.getResourceAsStream("/grow.properties")); - - int offset = 0; - while (offset < args.length) { - if ("--domain".equals(args[offset])) { - mConfig.setDomain(args[offset + 1]); - mDatabase = null; - offset += 2; - - } else if ("--dev".equals(args[offset])) { - mConfig.setDomain("dev"); - mDatabase = null; - offset += 1; - - } else if ("--config".equals(args[offset])) { - mConfig.updateConfig(args[offset + 1]); - mDatabase = null; - offset += 2; - - } else if ("--list".equals(args[offset])) { - //offset = list(args, ++offset); - - } else if ("--create".equals(args[offset])) { - offset = create(args, ++offset); - - } else if ("--update".equals(args[offset])) { - offset = update(args, ++offset); - - } else if ("--drop".equals(args[offset])) { - offset = drop(args, ++offset); - - } else if ("--get".equals(args[offset])) { - offset = get(args, ++offset); - - } else if ("--put".equals(args[offset])) { - offset = put(args, ++offset); - - } else if ("--delete".equals(args[offset])) { - offset = delete(args, ++offset); - - } else if ("--scan".equals(args[offset])) { - offset = scan(args, ++offset); - - } else if ("--scanf".equals(args[offset])) { - offset = scanf(args, ++offset); - - /* Bootstrap Commands */ - } else if ("--bootstrap".equals(args[offset])) { - if ("dev".equals(mConfig.getDomain())) { - offset = bootstrapDevTables(args, ++offset); - } else { - offset = bootstrapTables(args, ++offset); - } - offset = loadStrings(args, offset); - - } else if ("--loadStrings".equals(args[offset])) { - offset = loadStrings(args, ++offset); - - } else if ("--destroy".equals(args[offset])) { - offset = destroy(args, ++offset); - - } else if ("--addadmin".equals(args[offset])) { - offset = addAdmin(args, ++offset); - - } else if ("--import".equals(args[offset])) { - offset = importTable(args, ++offset); - - } else { - throw new IllegalArgumentException("Unknown command " + args[offset]); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(2); - } - } - - private static DynamoDatabase getDatabase() { - if (mDatabase == null) { - mDatabase = new DynamoDatabase(mConfig); - } - - return mDatabase; - } - - private static int create(String[] args, int offset) { - String name = args[offset++]; - long reads = Long.parseLong(args[offset++]); - long writes = Long.parseLong(args[offset++]); - - DynamoDatabase db = getDatabase(); - - db.createTable(name, reads, writes); - - return offset; - } - - private static int update(String[] args, int offset) { - String name = args[offset++]; - long reads = Long.parseLong(args[offset++]); - long writes = Long.parseLong(args[offset++]); - - DynamoDatabase db = getDatabase(); - - db.updateTable(name, reads, writes); - - return offset; - } - - private static int drop(String[] args, int offset) { - String name = args[offset++]; - - DynamoDatabase db = getDatabase(); - - db.deleteTable(name); - - return offset; - } - - private static int get(String[] args, int offset) { - String table = args[offset++]; - String key = args[offset++]; - String attribute = args[offset++]; - - DynamoDatabase db = getDatabase(); - - String value = db.getAttribute(DynamoKey.newAttributeKey(table, key, attribute)); - - if (value == null) { - value = ""; - } - - System.out.printf("%s %s:%s\n%s\n\n", table, key, attribute, value); - - return offset; - } - - private static int put(String[] args, int offset) { - String table = args[offset++]; - String key = args[offset++]; - String attribute = args[offset++]; - String value = args[offset++]; - - DynamoDatabase db = getDatabase(); - - db.putAttribute(DynamoKey.newAttributeKey(table, key, attribute), value); - - return offset; - } - - private static int delete(String[] args, int offset) { - String table = args[offset++]; - String key = args[offset++]; - String attribute = args[offset++]; - - DynamoDatabase db = getDatabase(); - - db.deleteAttribute(DynamoKey.newAttributeKey(table, key, attribute)); - - System.out.printf("Deleted %s %s:%s\n\n", table, key, attribute); - - return offset; - } - - private static int scan(String[] args, int offset) { - String table = args[offset++]; - - DynamoKey key = DynamoKey.newKey(table, null); - - doScan(key); - - return offset; - } - - private static int scanf(String[] args, int offset) { - String table = args[offset++]; - String attribute = args[offset++]; - - DynamoKey key = DynamoKey.newAttributeKey(table, null, attribute); - - doScan(key); - - return offset; - } - - private static void doScan(DynamoKey key) { - DynamoDatabase db = getDatabase(); - - String attributeFilter = key.getAttribute(); - - while (key != null) { - Map> result = db.getAll(key); - - key = null; // If there are no results, exit - - for (Map.Entry> entry : result.entrySet()) { - key = entry.getKey(); // Save the last key - - for (Map.Entry attribute : entry.getValue().entrySet()) { - if (attributeFilter == null || attributeFilter.equals(attribute.getKey())) { - String keyString = key.getHashKey(); - if (key.getRangeKey() != null) { - keyString += "(" + key.getRangeKey() + ")"; - } - System.out.printf("%s %s:%s\n%s\n\n", - key.getTable(), keyString, attribute.getKey(), - attribute.getValue()); - } - } - } - } - } - - - private static int bootstrapTables(String[] args, int offset) { - DynamoDatabase db = getDatabase(); - - db.createTable("strings", 5, 1); - db.createTable("accounts", 5, 1); - db.createTable("assessments", 5, 5); - db.createTable("training", 5, 5); - db.createTable("feedthreads", 5, 1); - db.createTable("feedmessages", 5, 1); - - return offset; - } - - private static int bootstrapDevTables(String[] args, int offset) { - DynamoDatabase db = getDatabase(); - - db.createTable("strings", 1, 1); - db.createTable("accounts", 1, 1); - db.createTable("assessments", 1, 1); - db.createTable("training", 1, 1); - db.createTable("feedthreads", 1, 1); - db.createTable("feedmessages", 1, 1); - - return offset; - } - - private static int loadStrings(String[] args, int offset) throws IOException { - String data = args[offset++]; - File baseDir = new File(data); - - DynamoDatabase db = getDatabase(); - - insertQuestions(baseDir); - insertVideos(baseDir); - insertDefaultPlaylist(baseDir); - - return offset; - } - - private static int destroy(String[] args, int offset) { - DynamoDatabase db = getDatabase(); - - final String[] tables = { "strings", - "accounts", - "assessments", - "training", - "feedthreads", - "feedmessages" - }; - - for (String table : tables) { - try { - db.deleteTable(table); - } catch (Exception e) { - System.err.println("Deleting " + table + ": " + e.getMessage()); - } - } - - return offset; - } - - private static int addAdmin(String[] args, int offset) throws IOException { - String user = args[offset++]; - String pass = args[offset++]; - - DynamoDatabase db = getDatabase(); - - UserRecord record = new UserRecord(); - record.setId(user); - record.setBackendPassword(pass); - - Provider provider = new DynamoProviderImpl(db, UserRecord.class); - provider.put(DynamoKey.newAttributeKey("accounts", user, "value"), record); - - return offset; - } - - private static int importTable(String[] args, int offset) throws IOException { - String table = args[offset++]; - String filename = args[offset++]; - - DynamoDatabase db = getDatabase(); - - List lines = Files.readAllLines(new File(filename).toPath(), - StandardCharsets.UTF_8); - - int count = 0; - - String key = null; - Map attributes = new HashMap<>(); - for (String line : lines) { - if (line.length() == 0) { - if (attributes.size() > 0) { - db.putKey(DynamoKey.newKey(table, key), attributes); - count++; - - if (count % 50 == 0) { - System.out.printf("Imported %d records into %s...\n", count, table); - } - } - key = null; - attributes = new HashMap<>(); - continue; - } - - if (key == null) { - key = line; - continue; - } - - int space = line.indexOf(' '); - String attribute = line.substring(0, space); - String value = line.substring(space + 1); - - attributes.put(attribute, value); - } - - // Finish up the remaining attributes. - if (key != null && attributes.size() > 0) { - db.putKey(DynamoKey.newKey(table, key), attributes); - count++; - } - - System.out.printf("Imported %d records into %s.\n", count, table); - - return offset; - } - - private static void insertQuestions(File baseDir) throws IOException { - DynamoDatabase db = getDatabase(); - File questions = new File(baseDir, "questions"); - - File[] files = questions.listFiles(JSON_FILTER); - Arrays.sort(files); - - for (File file : files) { - String filename = file.getName(); - String questionId = filename.substring(0, filename.lastIndexOf('.')); - - byte[] encoded = Files.readAllBytes(file.toPath()); - String value = new String(encoded, StandardCharsets.UTF_8); - db.putAttribute(DynamoKey.newAttributeKey("strings", - "/questions/" + questionId, "value"), value); - System.out.println("Inserted /questions/" + questionId); - } - - String filename = files[0].getName(); - String first = filename.substring(0, filename.lastIndexOf('.')); - int count = files.length; - String summary = "{\"first\": \"" + first + "\", \"count\": " + count + "}"; - db.putAttribute(DynamoKey.newAttributeKey("strings", "/questions", "value"), summary); - System.out.println("Inserted /questions"); - } - - private static void insertVideos(File baseDir) throws IOException { - DynamoDatabase db = getDatabase(); - File videos = new File(baseDir, "videos"); - - for (File topic : videos.listFiles()) { - if (!topic.isDirectory()) { - continue; - } - - String topicName = topic.getName(); - - Map attributes = new HashMap<>(); - File[] files = topic.listFiles(JSON_FILTER); - for (File file : files) { - String filename = file.getName(); - String videoId = filename.substring(0, filename.lastIndexOf('.')); - - byte[] encoded = Files.readAllBytes(file.toPath()); - String value = new String(encoded, StandardCharsets.UTF_8); - - attributes.put(videoId, value); - System.out.println("Found /training/" + topicName + ":" + videoId); - } - - db.putKey(DynamoKey.newKey("strings", - "/training/" + topicName), attributes); - System.out.println("Inserted /training/" + topicName); - } - } - - private static void insertDefaultPlaylist(File baseDir) throws IOException { - DynamoDatabase db = getDatabase(); - File file = new File(baseDir, "videos/playlist.json"); - - byte[] encoded = Files.readAllBytes(file.toPath()); - String value = new String(encoded, StandardCharsets.UTF_8); - db.putAttribute(DynamoKey.newAttributeKey("strings", - "/training/defaultplaylist", "value"), value); - System.out.println("Inserted /training/defaultplaylist"); - } - - private static class JsonFilter implements FilenameFilter { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".json"); - } - } -} diff --git a/src/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java b/src/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java deleted file mode 100644 index b53e9f7..0000000 --- a/src/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend.dynamo; - -import java.io.IOException; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * - * @author Jesse Morgan - */ -public class DynamoCollectionProviderImpl implements CollectionProvider { - private final DynamoDatabase mDb; - private final String mTable; - private final Class mClazz; - - public DynamoCollectionProviderImpl(DynamoDatabase db, String table, Class clazz) { - mDb = db; - mTable = table; - mClazz = clazz; - } - - @Override - public V get(String collection, String key) throws IOException { - String blob = mDb.getAttribute(DynamoKey.newAttributeKey(mTable, collection, key)); - return decode(blob); - } - - @Override - public Map query(String collection) throws IOException { - return query(collection, -1); - } - - @Override - public Map query(String collection, int limit) throws IOException { - Map result = new LinkedHashMap<>(); - - Map row = mDb.getKey(DynamoKey.newKey(mTable, collection)); - if (row.size() > 0) { - int count = 0; - for (Map.Entry c : row.entrySet()) { - if (limit >= 0 && ++count > limit) { - break; // Limit reached. - } - - String key = c.getKey(); - String blob = c.getValue(); - V obj = decode(blob); - - result.put(key, obj); - } - } - - return Collections.unmodifiableMap(result); - } - - @Override - public void put(String collection, String key, V obj) throws IOException { - if (obj == null) { - mDb.deleteAttribute(DynamoKey.newAttributeKey(mTable, collection, key)); - } else { - String blob = encode(obj); - mDb.putAttribute(DynamoKey.newAttributeKey(mTable, collection, key), blob); - } - } - - /** - * Encode the object as JSON. - * - * @param obj The object to encode. - * @return The JSON encoding of obj. - * @throws IOException if the object cannot be encoded. - */ - protected String encode(V obj) throws IOException { - if (mClazz == String.class) { - return (String) obj; - } else { - return JsonEncodedProvider.MAPPER.writeValueAsString(obj); - } - } - - /** - * Decode the JSON string as an object. - * - * @param blob The JSON data to decode. - * @return The decoded object or null if blob is null. - * @throws IOException If an object cannot be decoded. - */ - protected V decode(String blob) throws IOException { - if (blob == null) { - return null; - } - - if (mClazz == String.class) { - return (V) blob; - } - - V obj = JsonEncodedProvider.MAPPER.readValue(blob, mClazz); - return obj; - } -} diff --git a/src/com/p4square/grow/backend/dynamo/DynamoDatabase.java b/src/com/p4square/grow/backend/dynamo/DynamoDatabase.java deleted file mode 100644 index 68a165d..0000000 --- a/src/com/p4square/grow/backend/dynamo/DynamoDatabase.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend.dynamo; - -import java.util.Arrays; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; -import com.amazonaws.services.dynamodbv2.model.AttributeAction; -import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; -import com.amazonaws.services.dynamodbv2.model.AttributeValue; -import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; -import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.CreateTableResult; -import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; -import com.amazonaws.services.dynamodbv2.model.DeleteItemResult; -import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; -import com.amazonaws.services.dynamodbv2.model.DeleteTableResult; -import com.amazonaws.services.dynamodbv2.model.GetItemRequest; -import com.amazonaws.services.dynamodbv2.model.GetItemResult; -import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; -import com.amazonaws.services.dynamodbv2.model.KeyType; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; -import com.amazonaws.services.dynamodbv2.model.PutItemRequest; -import com.amazonaws.services.dynamodbv2.model.PutItemResult; -import com.amazonaws.services.dynamodbv2.model.ScanRequest; -import com.amazonaws.services.dynamodbv2.model.ScanResult; -import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; -import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; -import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; -import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; - -import com.p4square.grow.config.Config; - -/** - * A wrapper around the Dynamo API. - */ -public class DynamoDatabase { - private final AmazonDynamoDBClient mClient; - private final String mTablePrefix; - - public DynamoDatabase(final Config config) { - AWSCredentials creds; - - String awsAccessKey = config.getString("awsAccessKey"); - if (awsAccessKey != null) { - creds = new AWSCredentials() { - @Override - public String getAWSAccessKeyId() { - return config.getString("awsAccessKey"); - } - @Override - public String getAWSSecretKey() { - return config.getString("awsSecretKey"); - } - }; - } else { - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - } - - mClient = new AmazonDynamoDBClient(creds); - - String endpoint = config.getString("dynamoEndpoint"); - if (endpoint != null) { - mClient.setEndpoint(endpoint); - } - - String region = config.getString("awsRegion"); - if (region != null) { - mClient.setRegion(Region.getRegion(Regions.fromName(region))); - } - - mTablePrefix = config.getString("dynamoTablePrefix", ""); - } - - public void createTable(String name, long reads, long writes) { - ArrayList attributeDefinitions = new ArrayList<>(); - attributeDefinitions.add(new AttributeDefinition() - .withAttributeName("id") - .withAttributeType("S")); - - ArrayList ks = new ArrayList<>(); - ks.add(new KeySchemaElement().withAttributeName("id").withKeyType(KeyType.HASH)); - - ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput() - .withReadCapacityUnits(reads) - .withWriteCapacityUnits(writes); - - CreateTableRequest request = new CreateTableRequest() - .withTableName(mTablePrefix + name) - .withAttributeDefinitions(attributeDefinitions) - .withKeySchema(ks) - .withProvisionedThroughput(provisionedThroughput); - - CreateTableResult result = mClient.createTable(request); - } - - public void updateTable(String name, long reads, long writes) { - ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput() - .withReadCapacityUnits(reads) - .withWriteCapacityUnits(writes); - - UpdateTableRequest request = new UpdateTableRequest() - .withTableName(mTablePrefix + name) - .withProvisionedThroughput(provisionedThroughput); - - UpdateTableResult result = mClient.updateTable(request); - } - - public void deleteTable(String name) { - DeleteTableRequest deleteTableRequest = new DeleteTableRequest() - .withTableName(mTablePrefix + name); - - DeleteTableResult result = mClient.deleteTable(deleteTableRequest); - } - - /** - * Get all rows from a table. - * - * The key parameter must specify a table. If hash/range key is specified, - * the scan will begin after that key. - * - * @param key Previous key to start with. - * @return An ordered map of all results. - */ - public Map> getAll(final DynamoKey key) { - ScanRequest scanRequest = new ScanRequest().withTableName(mTablePrefix + key.getTable()); - - if (key.getHashKey() != null) { - scanRequest.setExclusiveStartKey(generateKey(key)); - } - - ScanResult scanResult = mClient.scan(scanRequest); - - Map> result = new LinkedHashMap<>(); - for (Map map : scanResult.getItems()) { - String id = null; - String range = null; - Map row = new LinkedHashMap<>(); - for (Map.Entry entry : map.entrySet()) { - if ("id".equals(entry.getKey())) { - id = entry.getValue().getS(); - } else if ("range".equals(entry.getKey())) { - range = entry.getValue().getS(); - } else { - row.put(entry.getKey(), entry.getValue().getS()); - } - } - result.put(DynamoKey.newRangeKey(key.getTable(), id, range), row); - } - - return result; - } - - public Map getKey(final DynamoKey key) { - GetItemRequest getItemRequest = new GetItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withKey(generateKey(key)); - - GetItemResult getItemResult = mClient.getItem(getItemRequest); - Map map = getItemResult.getItem(); - - Map result = new LinkedHashMap<>(); - if (map != null) { - for (Map.Entry entry : map.entrySet()) { - if (!"id".equals(entry.getKey())) { - result.put(entry.getKey(), entry.getValue().getS()); - } - } - } - - return result; - } - - public String getAttribute(final DynamoKey key) { - checkAttributeKey(key); - - GetItemRequest getItemRequest = new GetItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withKey(generateKey(key)) - .withAttributesToGet(key.getAttribute()); - - GetItemResult result = mClient.getItem(getItemRequest); - Map map = result.getItem(); - - if (map == null) { - return null; - } - - AttributeValue value = map.get(key.getAttribute()); - if (value != null) { - return value.getS(); - - } else { - return null; - } - } - - /** - * Set all attributes for the given key. - * - * @param key The key. - * @param values Map of attributes to values. - */ - public void putKey(final DynamoKey key, final Map values) { - Map item = new HashMap<>(); - for (Map.Entry entry : values.entrySet()) { - item.put(entry.getKey(), new AttributeValue().withS(entry.getValue())); - } - - // Set the Key - item.putAll(generateKey(key)); - - PutItemRequest putItemRequest = new PutItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withItem(item); - - PutItemResult result = mClient.putItem(putItemRequest); - } - - /** - * Set the particular attributes of the given key. - * - * @param key The key. - * @param value The new value. - */ - public void putAttribute(final DynamoKey key, final String value) { - checkAttributeKey(key); - - Map updateItem = new HashMap<>(); - updateItem.put(key.getAttribute(), - new AttributeValueUpdate() - .withAction(AttributeAction.PUT) - .withValue(new AttributeValue().withS(value))); - - UpdateItemRequest updateItemRequest = new UpdateItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withKey(generateKey(key)) - .withAttributeUpdates(updateItem); - // TODO: Check conditions. - - UpdateItemResult result = mClient.updateItem(updateItemRequest); - } - - /** - * Delete the given key. - * - * @param key The key. - */ - public void deleteKey(final DynamoKey key) { - DeleteItemRequest deleteItemRequest = new DeleteItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withKey(generateKey(key)); - - DeleteItemResult result = mClient.deleteItem(deleteItemRequest); - } - - /** - * Delete an attribute from the given key. - * - * @param key The key. - */ - public void deleteAttribute(final DynamoKey key) { - checkAttributeKey(key); - - Map updateItem = new HashMap<>(); - updateItem.put(key.getAttribute(), - new AttributeValueUpdate().withAction(AttributeAction.DELETE)); - - UpdateItemRequest updateItemRequest = new UpdateItemRequest() - .withTableName(mTablePrefix + key.getTable()) - .withKey(generateKey(key)) - .withAttributeUpdates(updateItem); - - UpdateItemResult result = mClient.updateItem(updateItemRequest); - } - - /** - * Generate a DynamoDB Key Map from the DynamoKey. - */ - private Map generateKey(final DynamoKey key) { - HashMap keyMap = new HashMap<>(); - keyMap.put("id", new AttributeValue().withS(key.getHashKey())); - - String range = key.getRangeKey(); - if (range != null) { - keyMap.put("range", new AttributeValue().withS(range)); - } - - return keyMap; - } - - private void checkAttributeKey(DynamoKey key) { - if (null == key.getAttribute()) { - throw new IllegalArgumentException("Attribute must be non-null"); - } - } -} diff --git a/src/com/p4square/grow/backend/dynamo/DynamoKey.java b/src/com/p4square/grow/backend/dynamo/DynamoKey.java deleted file mode 100644 index 5cdbacd..0000000 --- a/src/com/p4square/grow/backend/dynamo/DynamoKey.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.backend.dynamo; - -/** - * DynamoKey represents a table, hash key, and range key tupl. - */ -public class DynamoKey { - private final String mTable; - private final String mHashKey; - private final String mRangeKey; - private final String mAttribute; - - public static DynamoKey newKey(final String table, final String hashKey) { - return new DynamoKey(table, hashKey, null, null); - } - - public static DynamoKey newRangeKey(final String table, final String hashKey, - final String rangeKey) { - - return new DynamoKey(table, hashKey, rangeKey, null); - } - - public static DynamoKey newAttributeKey(final String table, final String hashKey, - final String attribute) { - - return new DynamoKey(table, hashKey, null, attribute); - } - - public DynamoKey(final String table, final String hashKey, final String rangeKey, - final String attribute) { - - mTable = table; - mHashKey = hashKey; - mRangeKey = rangeKey; - mAttribute = attribute; - } - - public String getTable() { - return mTable; - } - - public String getHashKey() { - return mHashKey; - } - - public String getRangeKey() { - return mRangeKey; - } - - public String getAttribute() { - return mAttribute; - } -} diff --git a/src/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java b/src/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java deleted file mode 100644 index 93a535f..0000000 --- a/src/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.dynamo; - -import java.io.IOException; - -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * Provider implementation backed by a DynamoDB Table. - * - * @author Jesse Morgan - */ -public class DynamoProviderImpl extends JsonEncodedProvider implements Provider { - private final DynamoDatabase mDb; - - public DynamoProviderImpl(DynamoDatabase db, Class clazz) { - super(clazz); - - mDb = db; - } - - @Override - public V get(DynamoKey key) throws IOException { - String blob = mDb.getAttribute(key); - return decode(blob); - } - - @Override - public void put(DynamoKey key, V obj) throws IOException { - String blob = encode(obj); - mDb.putAttribute(key, blob); - } -} diff --git a/src/com/p4square/grow/backend/feed/FeedDataProvider.java b/src/com/p4square/grow/backend/feed/FeedDataProvider.java deleted file mode 100644 index 6f090c0..0000000 --- a/src/com/p4square/grow/backend/feed/FeedDataProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.feed; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -import com.p4square.grow.model.MessageThread; -import com.p4square.grow.model.Message; -import com.p4square.grow.provider.CollectionProvider; - -/** - * Implementing this interface indicates you can provide a data source for the Feed. - * - * @author Jesse Morgan - */ -public interface FeedDataProvider { - public static final Collection TOPICS = Collections.unmodifiableCollection( - Arrays.asList(new String[] { "seeker", "believer", "disciple", "teacher", "leader" })); - - /** - * @return a CollectionProvider of Threads. - */ - CollectionProvider getThreadProvider(); - - /** - * @return a CollectionProvider of Messages. - */ - CollectionProvider getMessageProvider(); -} diff --git a/src/com/p4square/grow/backend/feed/ThreadResource.java b/src/com/p4square/grow/backend/feed/ThreadResource.java deleted file mode 100644 index e8f46c2..0000000 --- a/src/com/p4square/grow/backend/feed/ThreadResource.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.feed; - -import java.io.IOException; - -import java.util.Date; -import java.util.Map; - -import org.restlet.data.Status; -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; - -import org.restlet.ext.jackson.JacksonRepresentation; - -import org.apache.log4j.Logger; - -import com.p4square.grow.model.Message; - -/** - * ThreadResource manages the messages that make up a thread. - * - * @author Jesse Morgan - */ -public class ThreadResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(ThreadResource.class); - - private FeedDataProvider mBackend; - private String mTopic; - private String mThreadId; - - @Override - public void doInit() { - super.doInit(); - - mBackend = (FeedDataProvider) getApplication(); - mTopic = getAttribute("topic"); - mThreadId = getAttribute("thread"); - } - - /** - * GET a list of messages in a thread. - */ - @Override - protected Representation get() { - // If the topic or threadId are missing, return a 404. - if (mTopic == null || mTopic.length() == 0 || - mThreadId == null || mThreadId.length() == 0) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - // TODO: Support limit query parameter. - - try { - String collectionKey = mTopic + "/" + mThreadId; - Map messages = mBackend.getMessageProvider().query(collectionKey); - return new JacksonRepresentation(messages.values()); - - } catch (IOException e) { - LOG.error("Unexpected exception: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } - - /** - * POST a new message to the thread. - */ - @Override - protected Representation post(Representation entity) { - // If the topic and thread are not provided, respond with not allowed. - // TODO: Check if the thread exists. - if (mTopic == null || !mBackend.TOPICS.contains(mTopic) || - mThreadId == null || mThreadId.length() == 0) { - setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - try { - JacksonRepresentation jsonRep = new JacksonRepresentation(entity, Message.class); - Message message = jsonRep.getObject(); - - // Force the thread id and message to be what we expect. - message.setThreadId(mThreadId); - message.setId(Message.generateId()); - - if (message.getCreated() == null) { - message.setCreated(new Date()); - } - - String collectionKey = mTopic + "/" + mThreadId; - mBackend.getMessageProvider().put(collectionKey, message.getId(), message); - - setLocationRef(mThreadId + "/" + message.getId()); - return new JacksonRepresentation(message); - - } catch (IOException e) { - LOG.error("Unexpected exception: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } -} diff --git a/src/com/p4square/grow/backend/feed/TopicResource.java b/src/com/p4square/grow/backend/feed/TopicResource.java deleted file mode 100644 index 24b6a92..0000000 --- a/src/com/p4square/grow/backend/feed/TopicResource.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.feed; - -import java.io.IOException; - -import java.util.Date; -import java.util.Map; - -import org.restlet.data.Status; -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; - -import org.restlet.ext.jackson.JacksonRepresentation; - -import org.apache.log4j.Logger; - -import com.p4square.grow.model.Message; -import com.p4square.grow.model.MessageThread; - -/** - * TopicResource manages the threads contained in a topic. - * - * @author Jesse Morgan - */ -public class TopicResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(TopicResource.class); - - private FeedDataProvider mBackend; - private String mTopic; - - @Override - public void doInit() { - super.doInit(); - - mBackend = (FeedDataProvider) getApplication(); - mTopic = getAttribute("topic"); - } - - /** - * GET a list of threads in the topic. - */ - @Override - protected Representation get() { - // If no topic is provided, return a list of topics. - if (mTopic == null || mTopic.length() == 0) { - return new JacksonRepresentation(FeedDataProvider.TOPICS); - } - - // Parse limit query parameter. - int limit = -1; - String limitString = getQueryValue("limit"); - if (limitString != null) { - try { - limit = Integer.parseInt(limitString); - } catch (NumberFormatException e) { - setStatus(Status.CLIENT_ERROR_BAD_REQUEST); - return null; - } - } - - try { - Map threads = mBackend.getThreadProvider().query(mTopic, limit); - return new JacksonRepresentation(threads.values()); - - } catch (IOException e) { - LOG.error("Unexpected exception: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } - - /** - * POST a new thread to the topic. - */ - @Override - protected Representation post(Representation entity) { - // If no topic is provided, respond with not allowed. - if (mTopic == null || !mBackend.TOPICS.contains(mTopic)) { - setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - try { - // Deserialize the incoming message. - JacksonRepresentation jsonRep = - new JacksonRepresentation(entity, MessageThread.class); - - // Get the message from the request. - // Throw away the wrapping MessageThread because we'll create our own later. - Message message = jsonRep.getObject().getMessage(); - if (message.getCreated() == null) { - message.setCreated(new Date()); - } - - // Create the new thread. - MessageThread newThread = MessageThread.createNew(); - - // Force the thread id and message to be what we expect. - message.setId(Message.generateId()); - message.setThreadId(newThread.getId()); - newThread.setMessage(message); - - mBackend.getThreadProvider().put(mTopic, newThread.getId(), newThread); - - setLocationRef(mTopic + "/" + newThread.getId()); - return new JacksonRepresentation(newThread); - - } catch (IOException e) { - LOG.error("Unexpected exception: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } -} diff --git a/src/com/p4square/grow/backend/resources/AccountResource.java b/src/com/p4square/grow/backend/resources/AccountResource.java deleted file mode 100644 index 2ac7061..0000000 --- a/src/com/p4square/grow/backend/resources/AccountResource.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; - -import org.restlet.data.Status; -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; - -import org.restlet.ext.jackson.JacksonRepresentation; - -import org.apache.log4j.Logger; - -import com.p4square.grow.model.UserRecord; -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.ProvidesUserRecords; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * Stores a document about a user. - * - * @author Jesse Morgan - */ -public class AccountResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(AccountResource.class); - - private Provider mUserRecordProvider; - - private String mUserId; - - @Override - public void doInit() { - super.doInit(); - - final ProvidesUserRecords backend = (ProvidesUserRecords) getApplication(); - mUserRecordProvider = backend.getUserRecordProvider(); - - mUserId = getAttribute("userId"); - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - try { - UserRecord result = mUserRecordProvider.get(mUserId); - - if (result == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - JacksonRepresentation rep = new JacksonRepresentation(result); - rep.setObjectMapper(JsonEncodedProvider.MAPPER); - return rep; - - } catch (IOException e) { - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } - - /** - * Handle PUT requests - */ - @Override - protected Representation put(Representation entity) { - try { - JacksonRepresentation representation = - new JacksonRepresentation<>(entity, UserRecord.class); - representation.setObjectMapper(JsonEncodedProvider.MAPPER); - UserRecord record = representation.getObject(); - - mUserRecordProvider.put(mUserId, record); - setStatus(Status.SUCCESS_NO_CONTENT); - - } catch (IOException e) { - setStatus(Status.SERVER_ERROR_INTERNAL); - } - - return null; - } -} diff --git a/src/com/p4square/grow/backend/resources/BannerResource.java b/src/com/p4square/grow/backend/resources/BannerResource.java deleted file mode 100644 index 2b9c8e6..0000000 --- a/src/com/p4square/grow/backend/resources/BannerResource.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; - -import org.restlet.data.Status; -import org.restlet.ext.jackson.JacksonRepresentation; -import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; -import org.restlet.resource.ServerResource; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.apache.log4j.Logger; - -import com.p4square.grow.backend.GrowBackend; -import com.p4square.grow.model.Banner; -import com.p4square.grow.provider.JsonEncodedProvider; -import com.p4square.grow.provider.Provider; - -/** - * Fetches or sets the banner string. - * - * @author Jesse Morgan - */ -public class BannerResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(BannerResource.class); - - public static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; - - private Provider mStringProvider; - - @Override - public void doInit() { - super.doInit(); - - final GrowBackend backend = (GrowBackend) getApplication(); - mStringProvider = backend.getStringProvider(); - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - String result = null; - try { - result = mStringProvider.get("banner"); - - } catch (IOException e) { - LOG.warn("Exception loading banner: " + e); - } - - if (result == null || result.length() == 0) { - result = "{\"html\":null}"; - } - - return new StringRepresentation(result); - } - - /** - * Handle PUT requests - */ - @Override - protected Representation put(Representation entity) { - try { - JacksonRepresentation representation = - new JacksonRepresentation<>(entity, Banner.class); - representation.setObjectMapper(MAPPER); - - Banner banner = representation.getObject(); - - mStringProvider.put("banner", MAPPER.writeValueAsString(banner)); - setStatus(Status.SUCCESS_NO_CONTENT); - - } catch (IOException e) { - setStatus(Status.SERVER_ERROR_INTERNAL); - } - - return null; - } -} diff --git a/src/com/p4square/grow/backend/resources/SurveyResource.java b/src/com/p4square/grow/backend/resources/SurveyResource.java deleted file mode 100644 index 8723ee2..0000000 --- a/src/com/p4square/grow/backend/resources/SurveyResource.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; - -import java.util.Map; -import java.util.HashMap; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.restlet.data.MediaType; -import org.restlet.data.Status; -import org.restlet.ext.jackson.JacksonRepresentation; -import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; -import org.restlet.resource.ServerResource; - -import org.apache.log4j.Logger; - -import com.p4square.grow.backend.GrowBackend; -import com.p4square.grow.model.Question; -import com.p4square.grow.provider.JsonEncodedProvider; -import com.p4square.grow.provider.Provider; - -/** - * This resource manages assessment questions. - * - * @author Jesse Morgan - */ -public class SurveyResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(SurveyResource.class); - - private static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; - - private Provider mQuestionProvider; - private Provider mStringProvider; - - private String mQuestionId; - - @Override - public void doInit() { - super.doInit(); - - final GrowBackend backend = (GrowBackend) getApplication(); - mQuestionProvider = backend.getQuestionProvider(); - mStringProvider = backend.getStringProvider(); - - mQuestionId = getAttribute("questionId"); - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - String result = "{}"; - - if (mQuestionId == null) { - // TODO: List all question ids - - } else if (mQuestionId.equals("first")) { - // Get the first question id from db? - Map questionSummary = getQuestionsSummary(); - mQuestionId = (String) questionSummary.get("first"); - - } else if (mQuestionId.equals("count")) { - // Get the first question id from db? - Map questionSummary = getQuestionsSummary(); - - return new StringRepresentation("{\"count\":" + - String.valueOf((Integer) questionSummary.get("count")) + "}"); - } - - if (mQuestionId != null) { - // Get a question by id - Question question = null; - try { - question = mQuestionProvider.get(mQuestionId); - } catch (IOException e) { - LOG.error("IOException loading question: " + e); - } - - if (question == null) { - // 404 - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - JacksonRepresentation rep = new JacksonRepresentation<>(question); - rep.setObjectMapper(MAPPER); - return rep; - } - - return new StringRepresentation(result); - } - - private Map getQuestionsSummary() { - try { - // TODO: This could be better. Quick fix for provider support. - String json = mStringProvider.get("/questions"); - - if (json != null) { - return MAPPER.readValue(json, Map.class); - } - - } catch (IOException e) { - LOG.info("Exception reading questions summary.", e); - } - - return null; - } -} diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java deleted file mode 100644 index 7c15cfd..0000000 --- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; -import java.util.Map; -import java.util.HashMap; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.restlet.data.MediaType; -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.grow.backend.GrowBackend; -import com.p4square.grow.model.Answer; -import com.p4square.grow.model.Question; -import com.p4square.grow.model.RecordedAnswer; -import com.p4square.grow.model.Score; -import com.p4square.grow.model.UserRecord; -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.Provider; - - -/** - * Store the user's answers to the assessment and generate their score. - * - * @author Jesse Morgan - */ -public class SurveyResultsResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(SurveyResultsResource.class); - - private static final ObjectMapper MAPPER = new ObjectMapper(); - - static enum RequestType { - ASSESSMENT, ANSWER - } - - private CollectionProvider mAnswerProvider; - private Provider mQuestionProvider; - private Provider mUserRecordProvider; - - private RequestType mRequestType; - private String mUserId; - private String mQuestionId; - - @Override - public void doInit() { - super.doInit(); - - final GrowBackend backend = (GrowBackend) getApplication(); - mAnswerProvider = backend.getAnswerProvider(); - mQuestionProvider = backend.getQuestionProvider(); - mUserRecordProvider = backend.getUserRecordProvider(); - - mUserId = getAttribute("userId"); - mQuestionId = getAttribute("questionId"); - - mRequestType = RequestType.ASSESSMENT; - if (mQuestionId != null) { - mRequestType = RequestType.ANSWER; - } - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - try { - String result = null; - - switch (mRequestType) { - case ANSWER: - result = mAnswerProvider.get(mUserId, mQuestionId); - break; - - case ASSESSMENT: - result = mAnswerProvider.get(mUserId, "summary"); - if (result == null || result.length() == 0) { - result = buildAssessment(); - } - break; - } - - if (result == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - return new StringRepresentation(result); - } catch (IOException e) { - LOG.error("IOException getting answer: ", e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } - - /** - * Handle PUT requests - */ - @Override - protected Representation put(Representation entity) { - boolean success = false; - - switch (mRequestType) { - case ANSWER: - try { - mAnswerProvider.put(mUserId, mQuestionId, entity.getText()); - mAnswerProvider.put(mUserId, "lastAnswered", mQuestionId); - mAnswerProvider.put(mUserId, "summary", null); - success = true; - - } catch (Exception e) { - LOG.warn("Caught exception putting answer: " + e.getMessage(), e); - } - break; - - default: - setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - if (success) { - setStatus(Status.SUCCESS_NO_CONTENT); - - } else { - setStatus(Status.SERVER_ERROR_INTERNAL); - } - - return null; - } - - /** - * Clear assessment results. - */ - @Override - protected Representation delete() { - boolean success = false; - - switch (mRequestType) { - case ANSWER: - try { - mAnswerProvider.put(mUserId, mQuestionId, null); - mAnswerProvider.put(mUserId, "summary", null); - success = true; - - } catch (Exception e) { - LOG.warn("Caught exception putting answer: " + e.getMessage(), e); - } - break; - - case ASSESSMENT: - try { - mAnswerProvider.put(mUserId, "summary", null); - mAnswerProvider.put(mUserId, "lastAnswered", null); - // TODO Delete answers - - UserRecord record = mUserRecordProvider.get(mUserId); - if (record != null) { - record.setLanding("assessment"); - mUserRecordProvider.put(mUserId, record); - } - - success = true; - - } catch (Exception e) { - LOG.warn("Caught exception putting answer: " + e.getMessage(), e); - } - break; - - default: - setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - if (success) { - setStatus(Status.SUCCESS_NO_CONTENT); - - } else { - setStatus(Status.SERVER_ERROR_INTERNAL); - } - - return null; - - } - - /** - * This method compiles assessment results. - */ - private String buildAssessment() throws IOException { - StringBuilder sb = new StringBuilder("{ "); - - // Last question answered - final String lastAnswered = mAnswerProvider.get(mUserId, "lastAnswered"); - if (lastAnswered != null && lastAnswered.length() > 0) { - sb.append("\"lastAnswered\": \"" + lastAnswered + "\", "); - } - - // Compute score - Map row = mAnswerProvider.query(mUserId); - if (row.size() > 0) { - Score score = new Score(); - boolean scoringDone = false; - int totalAnswers = 0; - for (Map.Entry c : row.entrySet()) { - if (c.getKey().equals("lastAnswered") || c.getKey().equals("summary")) { - continue; - } - - try { - Question question = mQuestionProvider.get(c.getKey()); - RecordedAnswer userAnswer = MAPPER.readValue(c.getValue(), RecordedAnswer.class); - - if (question == null) { - LOG.warn("Answer for unknown question: " + c.getKey()); - continue; - } - - LOG.debug("Scoring questionId: " + c.getKey()); - scoringDone = !question.scoreAnswer(score, userAnswer); - - } catch (Exception e) { - LOG.error("Failed to score question: {userid: \"" + mUserId + - "\", questionid:\"" + c.getKey() + - "\", userAnswer:\"" + c.getValue() + "\"}", e); - } - - totalAnswers++; - } - - sb.append("\"score\":" + score.getScore()); - sb.append(", \"sum\":" + score.getSum()); - sb.append(", \"count\":" + score.getCount()); - sb.append(", \"totalAnswers\":" + totalAnswers); - sb.append(", \"result\":\"" + score.toString() + "\""); - } - - sb.append(" }"); - String summary = sb.toString(); - - // Persist summary - mAnswerProvider.put(mUserId, "summary", summary); - - return summary; - } -} diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java deleted file mode 100644 index 51ba56a..0000000 --- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; - -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.HashMap; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.restlet.data.MediaType; -import org.restlet.data.Status; -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; - -import org.restlet.ext.jackson.JacksonRepresentation; - -import org.apache.log4j.Logger; - -import com.p4square.grow.backend.GrowBackend; - -import com.p4square.grow.model.Chapter; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.VideoRecord; -import com.p4square.grow.model.TrainingRecord; - -import com.p4square.grow.provider.CollectionProvider; -import com.p4square.grow.provider.JsonEncodedProvider; -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.ProvidesAssessments; -import com.p4square.grow.provider.ProvidesTrainingRecords; - -import com.p4square.grow.model.Score; - -/** - * - * @author Jesse Morgan - */ -public class TrainingRecordResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(TrainingRecordResource.class); - private static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; - - static enum RequestType { - SUMMARY, VIDEO - } - - private Provider mTrainingRecordProvider; - private CollectionProvider mAnswerProvider; - - private RequestType mRequestType; - private String mUserId; - private String mVideoId; - private TrainingRecord mRecord; - - @Override - public void doInit() { - super.doInit(); - - mTrainingRecordProvider = ((ProvidesTrainingRecords) getApplication()).getTrainingRecordProvider(); - mAnswerProvider = ((ProvidesAssessments) getApplication()).getAnswerProvider(); - - mUserId = getAttribute("userId"); - mVideoId = getAttribute("videoId"); - - try { - Playlist defaultPlaylist = ((ProvidesTrainingRecords) getApplication()).getDefaultPlaylist(); - - mRecord = mTrainingRecordProvider.get(mUserId); - if (mRecord == null) { - mRecord = new TrainingRecord(); - mRecord.setPlaylist(defaultPlaylist); - skipAssessedChapters(mUserId, mRecord); - } else { - // Merge the playlist with the most recent version. - mRecord.getPlaylist().merge(defaultPlaylist); - } - - } catch (IOException e) { - LOG.error("IOException loading TrainingRecord: " + e.getMessage(), e); - mRecord = null; - } - - mRequestType = RequestType.SUMMARY; - if (mVideoId != null) { - mRequestType = RequestType.VIDEO; - } - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - JacksonRepresentation rep = null; - - if (mRecord == null) { - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - - switch (mRequestType) { - case VIDEO: - VideoRecord video = mRecord.getPlaylist().find(mVideoId); - if (video == null) { - break; // Fall through and return 404 - } - rep = new JacksonRepresentation(video); - break; - - case SUMMARY: - rep = new JacksonRepresentation(mRecord); - break; - } - - if (rep == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - - } else { - rep.setObjectMapper(JsonEncodedProvider.MAPPER); - return rep; - } - } - - /** - * Handle PUT requests - */ - @Override - protected Representation put(Representation entity) { - if (mRecord == null) { - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - - switch (mRequestType) { - case VIDEO: - try { - JacksonRepresentation representation = - new JacksonRepresentation<>(entity, VideoRecord.class); - representation.setObjectMapper(JsonEncodedProvider.MAPPER); - VideoRecord update = representation.getObject(); - VideoRecord video = mRecord.getPlaylist().find(mVideoId); - - if (video == null) { - // TODO: Video isn't on their playlist... - LOG.warn("Skipping video completion for video missing from playlist."); - - } else if (update.getComplete() && !video.getComplete()) { - // Video was newly completed - video.complete(); - mRecord.setLastVideo(mVideoId); - - mTrainingRecordProvider.put(mUserId, mRecord); - } - - setStatus(Status.SUCCESS_NO_CONTENT); - - } catch (Exception e) { - LOG.warn("Caught exception updating training record: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - } - break; - - default: - setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - } - - return null; - } - - private Score getAssessedScore(String userId) throws IOException { - // Get the user's score. - Score assessedScore = new Score(0, 0); - - String summaryString = mAnswerProvider.get(userId, "summary"); - if (summaryString == null) { - throw new IOException("Asked to create training record for unassessed user " + userId); - } - - Map summary = MAPPER.readValue(summaryString, Map.class); - - if (summary.containsKey("sum") && summary.containsKey("count")) { - double sum = (Double) summary.get("sum"); - int count = (Integer) summary.get("count"); - assessedScore = new Score(sum, count); - } - - return assessedScore; - } - - /** - * Mark the chapters which the user assessed through as not required. - */ - private void skipAssessedChapters(String userId, TrainingRecord record) { - // Get the user's score. - Score assessedScore = new Score(0, 0); - - try { - assessedScore = getAssessedScore(userId); - } catch (IOException e) { - LOG.error("IOException fetching assessment record for " + userId, e); - return; - } - - // Mark the correct videos as not required. - Playlist playlist = record.getPlaylist(); - - for (Map.Entry entry : playlist.getChaptersMap().entrySet()) { - String chapterId = entry.getKey(); - Chapter chapter = entry.getValue(); - boolean required; - - if ("introduction".equals(chapter)) { - // Introduction chapter is always required - required = true; - - } else { - // Chapter required if the floor of the score is <= the chapter's numeric value. - required = assessedScore.floor() <= Score.numericScore(chapterId); - } - - if (!required) { - for (VideoRecord video : chapter.getVideos().values()) { - video.setRequired(required); - } - } - } - } -} diff --git a/src/com/p4square/grow/backend/resources/TrainingResource.java b/src/com/p4square/grow/backend/resources/TrainingResource.java deleted file mode 100644 index 6efdfab..0000000 --- a/src/com/p4square/grow/backend/resources/TrainingResource.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.io.IOException; -import java.util.Map; - -import org.restlet.data.Status; -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; - -import org.apache.log4j.Logger; - -import com.p4square.grow.backend.GrowBackend; -import com.p4square.grow.backend.db.CassandraDatabase; - -import com.p4square.grow.provider.CollectionProvider; -/** - * This resource returns a listing of training items for a particular level. - * - * @author Jesse Morgan - */ -public class TrainingResource extends ServerResource { - private final static Logger LOG = Logger.getLogger(TrainingResource.class); - - private CollectionProvider mVideoProvider; - - private String mLevel; - private String mVideoId; - - @Override - public void doInit() { - super.doInit(); - - GrowBackend backend = (GrowBackend) getApplication(); - mVideoProvider = backend.getVideoProvider(); - - mLevel = getAttribute("level"); - mVideoId = getAttribute("videoId"); - } - - /** - * Handle GET Requests. - */ - @Override - protected Representation get() { - String result = null; - - if (mLevel == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - try { - if (mVideoId == null) { - // Get all videos - // TODO: This could be improved, but this is the quickest way to get - // providers working. - Map videos = mVideoProvider.query(mLevel); - if (videos.size() > 0) { - StringBuilder sb = new StringBuilder("{ \"level\": \"" + mLevel + "\""); - sb.append(", \"videos\": ["); - boolean first = true; - for (String value : videos.values()) { - if (!first) { - sb.append(", "); - } - sb.append(value); - first = false; - } - sb.append("] }"); - result = sb.toString(); - } - - } else { - // Get single video - result = mVideoProvider.get(mLevel, mVideoId); - } - - if (result == null) { - // 404 - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - - return new StringRepresentation(result); - - } catch (IOException e) { - LOG.error("IOException fetch video: " + e.getMessage(), e); - setStatus(Status.SERVER_ERROR_INTERNAL); - return null; - } - } -} diff --git a/src/com/p4square/grow/ccb/CCBProgressReporter.java b/src/com/p4square/grow/ccb/CCBProgressReporter.java deleted file mode 100644 index d2826eb..0000000 --- a/src/com/p4square/grow/ccb/CCBProgressReporter.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.p4square.grow.ccb; - -import com.p4square.ccbapi.CCBAPI; -import com.p4square.ccbapi.model.*; -import com.p4square.grow.frontend.ProgressReporter; -import com.p4square.grow.model.Score; -import org.apache.log4j.Logger; -import org.restlet.security.User; - -import java.io.IOException; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; - -/** - * A ProgressReporter which records progress in CCB. - * - * Except not really, because it's not implemented yet. - * This is just a placeholder until ccb-api-client-java has support for updating an individual. - */ -public class CCBProgressReporter implements ProgressReporter { - - private static final Logger LOG = Logger.getLogger(CCBProgressReporter.class); - - private static final String GROW_LEVEL = "GrowLevelTrain"; - private static final String GROW_ASSESSMENT = "GrowLevelAsmnt"; - - private final CCBAPI mAPI; - private final CustomFieldCache mCache; - - public CCBProgressReporter(final CCBAPI api, final CustomFieldCache cache) { - mAPI = api; - mCache = cache; - } - - @Override - public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) { - if (!(user instanceof CCBUser)) { - throw new IllegalArgumentException("Expected CCBUser but got " + user.getClass().getCanonicalName()); - } - final CCBUser ccbuser = (CCBUser) user; - - updateLevelAndDate(ccbuser, GROW_ASSESSMENT, level, date); - } - - @Override - public void reportChapterComplete(final User user, final String chapter, final Date date) { - if (!(user instanceof CCBUser)) { - throw new IllegalArgumentException("Expected CCBUser but got " + user.getClass().getCanonicalName()); - } - final CCBUser ccbuser = (CCBUser) user; - - // Only update the level if it is increasing. - final CustomPulldownFieldValue currentLevel = ccbuser.getProfile() - .getCustomPulldownFields().getByLabel(GROW_LEVEL); - - if (currentLevel != null) { - if (Score.numericScore(chapter) <= Score.numericScore(currentLevel.getSelection().getLabel())) { - LOG.info("Not updating level for " + user.getIdentifier() - + " because current level (" + currentLevel.getSelection().getLabel() - + ") is greater than new level (" + chapter + ")"); - return; - } - } - - updateLevelAndDate(ccbuser, GROW_LEVEL, chapter, date); - } - - private void updateLevelAndDate(final CCBUser user, final String field, final String level, final Date date) { - boolean modified = false; - - final UpdateIndividualProfileRequest req = new UpdateIndividualProfileRequest() - .withIndividualId(user.getProfile().getId()); - - final CustomField pulldownField = mCache.getIndividualPulldownByLabel(field); - if (pulldownField != null) { - final LookupTableType type = LookupTableType.valueOf(pulldownField.getName().toUpperCase()); - final LookupTableItem item = mCache.getPulldownItemByName(type, level); - if (item != null) { - req.withCustomPulldownField(pulldownField.getName(), item.getId()); - modified = true; - } - } - - final CustomField dateField = mCache.getDateFieldByLabel(field); - if (dateField != null) { - req.withCustomDateField(dateField.getName(), date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); - modified = true; - } - - try { - // Only update if a field exists. - if (modified) { - mAPI.updateIndividualProfile(req); - } - - } catch (IOException e) { - LOG.error("updateIndividual failed for " + user.getIdentifier() - + ", field " + field - + ", level " + level - + ", date " + date.toString()); - } - } -} diff --git a/src/com/p4square/grow/ccb/CCBUser.java b/src/com/p4square/grow/ccb/CCBUser.java deleted file mode 100644 index 7313172..0000000 --- a/src/com/p4square/grow/ccb/CCBUser.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.p4square.grow.ccb; - -import com.p4square.ccbapi.model.IndividualProfile; -import org.restlet.security.User; - -/** - * CCBUser is an adapter between a CCB IndividualProfile and a Restlet User. - * - * Note: CCBUser prefixes the user's identifier with "CCB-". This is done to - * ensure the identifier does not collide with identifiers from other - * systems. - */ -public class CCBUser extends User { - - private final IndividualProfile mProfile; - - /** - * Wrap an IndividualProfile inside a User object. - * - * @param profile The CCB IndividualProfile for the user. - */ - public CCBUser(final IndividualProfile profile) { - mProfile = profile; - - setIdentifier("CCB-" + mProfile.getId()); - setFirstName(mProfile.getFirstName()); - setLastName(mProfile.getLastName()); - setEmail(mProfile.getEmail()); - } - - /** - * @return The IndividualProfile of the user. - */ - public IndividualProfile getProfile() { - return mProfile; - } -} diff --git a/src/com/p4square/grow/ccb/CCBUserVerifier.java b/src/com/p4square/grow/ccb/CCBUserVerifier.java deleted file mode 100644 index db10b75..0000000 --- a/src/com/p4square/grow/ccb/CCBUserVerifier.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.p4square.grow.ccb; - -import com.p4square.ccbapi.CCBAPI; -import com.p4square.ccbapi.model.GetIndividualProfilesRequest; -import com.p4square.ccbapi.model.GetIndividualProfilesResponse; -import org.apache.log4j.Logger; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Verifier; - -/** - * CCBUserVerifier authenticates a user through the CCB individual_profile_from_login_password API. - */ -public class CCBUserVerifier implements Verifier { - private static final Logger LOG = Logger.getLogger(CCBUserVerifier.class); - - private final CCBAPI mAPI; - - public CCBUserVerifier(final CCBAPI api) { - mAPI = api; - } - - @Override - public int verify(Request request, Response response) { - if (request.getChallengeResponse() == null) { - return RESULT_MISSING; // no credentials - } - - final String username = request.getChallengeResponse().getIdentifier(); - final char[] password = request.getChallengeResponse().getSecret(); - - try { - GetIndividualProfilesResponse resp = mAPI.getIndividualProfiles( - new GetIndividualProfilesRequest().withLoginPassword(username, password)); - - if (resp.getIndividuals().size() == 1) { - // Wrap the IndividualProfile up in an User and update the user on the request. - final CCBUser user = new CCBUser(resp.getIndividuals().get(0)); - LOG.info("Successfully authenticated " + user.getIdentifier()); - request.getClientInfo().setUser(user); - return RESULT_VALID; - } - - } catch (Exception e) { - LOG.error("CCB API Exception: " + e, e); - } - - return RESULT_INVALID; // Invalid credentials - } -} diff --git a/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java b/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java deleted file mode 100644 index fc6148f..0000000 --- a/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.p4square.grow.ccb; - -import com.codahale.metrics.MetricRegistry; -import com.p4square.ccbapi.CCBAPI; -import com.p4square.ccbapi.CCBAPIClient; -import com.p4square.grow.config.Config; -import com.p4square.grow.frontend.IntegrationDriver; -import com.p4square.grow.frontend.ProgressReporter; -import org.restlet.Context; -import org.restlet.security.Verifier; - -import java.net.URI; -import java.net.URISyntaxException; - -/** - * The ChurchCommunityBuilderIntegrationDriver is used to integrate Grow with Church Community Builder. - */ -public class ChurchCommunityBuilderIntegrationDriver implements IntegrationDriver { - - private final Context mContext; - private final MetricRegistry mMetricRegistry; - private final Config mConfig; - - private final CCBAPI mAPI; - - private final CCBProgressReporter mProgressReporter; - - public ChurchCommunityBuilderIntegrationDriver(final Context context) { - mContext = context; - mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); - mMetricRegistry = (MetricRegistry) context.getAttributes().get("com.p4square.grow.metrics"); - - try { - CCBAPI api = new CCBAPIClient(new URI(mConfig.getString("CCBAPIURL", "")), - mConfig.getString("CCBAPIUser", ""), - mConfig.getString("CCBAPIPassword", "")); - - if (mMetricRegistry != null) { - api = new MonitoredCCBAPI(api, mMetricRegistry); - } - - mAPI = api; - - final CustomFieldCache cache = new CustomFieldCache(mAPI); - mProgressReporter = new CCBProgressReporter(mAPI, cache); - - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - @Override - public Verifier newUserAuthenticationVerifier() { - return new CCBUserVerifier(mAPI); - } - - @Override - public ProgressReporter getProgressReporter() { - return mProgressReporter; - } -} diff --git a/src/com/p4square/grow/ccb/CustomFieldCache.java b/src/com/p4square/grow/ccb/CustomFieldCache.java deleted file mode 100644 index d93e6d9..0000000 --- a/src/com/p4square/grow/ccb/CustomFieldCache.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.p4square.grow.ccb; - -import com.p4square.ccbapi.CCBAPI; -import com.p4square.ccbapi.model.*; -import org.apache.log4j.Logger; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * CustomFieldCache maintains an index from custom field labels to names. - */ -public class CustomFieldCache { - - private static final Logger LOG = Logger.getLogger(CustomFieldCache.class); - - private final CCBAPI mAPI; - - private CustomFieldCollection mTextFields; - private CustomFieldCollection mDateFields; - private CustomFieldCollection mIndividualPulldownFields; - private CustomFieldCollection mGroupPulldownFields; - - private final Map> mItemByNameTable; - - public CustomFieldCache(final CCBAPI api) { - mAPI = api; - mTextFields = new CustomFieldCollection<>(); - mDateFields = new CustomFieldCollection<>(); - mIndividualPulldownFields = new CustomFieldCollection<>(); - mGroupPulldownFields = new CustomFieldCollection<>(); - mItemByNameTable = new HashMap<>(); - } - - public CustomField getTextFieldByLabel(final String label) { - if (mTextFields.size() == 0) { - refresh(); - } - return mTextFields.getByLabel(label); - } - - public CustomField getDateFieldByLabel(final String label) { - if (mDateFields.size() == 0) { - refresh(); - } - return mDateFields.getByLabel(label); - } - - public CustomField getIndividualPulldownByLabel(final String label) { - if (mIndividualPulldownFields.size() == 0) { - refresh(); - } - return mIndividualPulldownFields.getByLabel(label); - } - - public CustomField getGroupPulldownByLabel(final String label) { - if (mGroupPulldownFields.size() == 0) { - refresh(); - } - return mGroupPulldownFields.getByLabel(label); - } - - public LookupTableItem getPulldownItemByName(final LookupTableType type, final String name) { - Map items = mItemByNameTable.get(type); - if (items == null) { - if (!cacheLookupTable(type)) { - return null; - } - items = mItemByNameTable.get(type); - } - - return items.get(name.toLowerCase()); - } - - private synchronized void refresh() { - try { - // Get all of the custom fields. - final GetCustomFieldLabelsResponse resp = mAPI.getCustomFieldLabels(); - - final CustomFieldCollection newTextFields = new CustomFieldCollection<>(); - final CustomFieldCollection newDateFields = new CustomFieldCollection<>(); - final CustomFieldCollection newIndPulldownFields = new CustomFieldCollection<>(); - final CustomFieldCollection newGrpPulldownFields = new CustomFieldCollection<>(); - - for (final CustomField field : resp.getCustomFields()) { - if (field.getName().startsWith("udf_ind_text_")) { - newTextFields.add(field); - } else if (field.getName().startsWith("udf_ind_date_")) { - newDateFields.add(field); - } else if (field.getName().startsWith("udf_ind_pulldown_")) { - newIndPulldownFields.add(field); - } else if (field.getName().startsWith("udf_grp_pulldown_")) { - newGrpPulldownFields.add(field); - } else { - LOG.warn("Unknown custom field type " + field.getName()); - } - } - - this.mTextFields = newTextFields; - this.mDateFields = newDateFields; - this.mIndividualPulldownFields = newIndPulldownFields; - this.mGroupPulldownFields = newGrpPulldownFields; - - } catch (IOException e) { - // Error fetching labels. - LOG.error("Error fetching custom fields: " + e.getMessage(), e); - } - } - - private synchronized boolean cacheLookupTable(final LookupTableType type) { - try { - final GetLookupTableResponse resp = mAPI.getLookupTable(new GetLookupTableRequest().withType(type)); - mItemByNameTable.put(type, resp.getItems().stream().collect( - Collectors.toMap(item -> item.getName().toLowerCase(), Function.identity()))); - return true; - - } catch (IOException e) { - LOG.error("Exception caching lookup table of type " + type, e); - } - - return false; - } -} diff --git a/src/com/p4square/grow/ccb/MonitoredCCBAPI.java b/src/com/p4square/grow/ccb/MonitoredCCBAPI.java deleted file mode 100644 index 43b6433..0000000 --- a/src/com/p4square/grow/ccb/MonitoredCCBAPI.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.p4square.grow.ccb; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; -import com.p4square.ccbapi.CCBAPI; -import com.p4square.ccbapi.model.*; - -import java.io.IOException; - -/** - * MonitoredCCBAPI is a CCBAPI decorator which records metrics for each API call. - */ -public class MonitoredCCBAPI implements CCBAPI { - - private final CCBAPI mAPI; - private final MetricRegistry mMetricRegistry; - - public MonitoredCCBAPI(final CCBAPI api, final MetricRegistry metricRegistry) { - if (api == null) { - throw new IllegalArgumentException("api must not be null."); - } - mAPI = api; - - if (metricRegistry == null) { - throw new IllegalArgumentException("metricRegistry must not be null."); - } - mMetricRegistry = metricRegistry; - } - - @Override - public GetCustomFieldLabelsResponse getCustomFieldLabels() throws IOException { - final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getCustomFieldLabels.time").time(); - boolean success = false; - try { - final GetCustomFieldLabelsResponse resp = mAPI.getCustomFieldLabels(); - success = true; - return resp; - } finally { - timer.stop(); - mMetricRegistry.counter("CCBAPI.getCustomFieldLabels.success").inc(success ? 1 : 0); - mMetricRegistry.counter("CCBAPI.getCustomFieldLabels.failure").inc(!success ? 1 : 0); - } - } - - @Override - public GetLookupTableResponse getLookupTable(final GetLookupTableRequest request) throws IOException { - final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getLookupTable.time").time(); - boolean success = false; - try { - final GetLookupTableResponse resp = mAPI.getLookupTable(request); - success = true; - return resp; - } finally { - timer.stop(); - mMetricRegistry.counter("CCBAPI.getLookupTable.success").inc(success ? 1 : 0); - mMetricRegistry.counter("CCBAPI.getLookupTable.failure").inc(!success ? 1 : 0); - } - } - - @Override - public GetIndividualProfilesResponse getIndividualProfiles(GetIndividualProfilesRequest request) - throws IOException { - final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getIndividualProfiles").time(); - boolean success = false; - try { - final GetIndividualProfilesResponse resp = mAPI.getIndividualProfiles(request); - mMetricRegistry.counter("CCBAPI.getIndividualProfiles.count").inc(resp.getIndividuals().size()); - success = true; - return resp; - } finally { - timer.stop(); - mMetricRegistry.counter("CCBAPI.getIndividualProfiles.success").inc(success ? 1 : 0); - mMetricRegistry.counter("CCBAPI.getIndividualProfiles.failure").inc(!success ? 1 : 0); - } - } - - @Override - public UpdateIndividualProfileResponse updateIndividualProfile(UpdateIndividualProfileRequest request) throws IOException { - final Timer.Context timer = mMetricRegistry.timer("CCBAPI.updateIndividualProfile").time(); - boolean success = false; - try { - final UpdateIndividualProfileResponse resp = mAPI.updateIndividualProfile(request); - success = true; - return resp; - } finally { - timer.stop(); - mMetricRegistry.counter("CCBAPI.updateIndividualProfile.success").inc(success ? 1 : 0); - mMetricRegistry.counter("CCBAPI.updateIndividualProfile.failure").inc(!success ? 1 : 0); - } - } - - @Override - public void close() throws IOException { - mAPI.close(); - } -} diff --git a/src/com/p4square/grow/config/Config.java b/src/com/p4square/grow/config/Config.java deleted file mode 100644 index 2fc2ea3..0000000 --- a/src/com/p4square/grow/config/Config.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.config; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.IOException; -import java.util.Properties; - -import org.apache.log4j.Logger; - -/** - * Manage configuration for an application. - * - * Config reads one or more property files as the application config. Duplicate - * properties loaded later override properties loaded earlier. Config has the - * concept of a domain to distinguish settings for development and production. - * The default domain is prod for production. Domain can be any String such as - * dev for development or test for testing. - * - * The property files are processed like java.util.Properties except that the - * keys are specified as DOMAIN.KEY. An asterisk (*) can be used in place of a - * domain to indicate it should apply to all domains. If a domain specific entry - * exists for the current domain, it will override any global config. - * - * @author Jesse Morgan - */ -public class Config { - private static final Logger LOG = Logger.getLogger(Config.class); - - private String mDomain; - private Properties mProperties; - - /** - * Construct a new Config object. - * - * Sets the domain to the value of the system property CONFIG_DOMAIN, if present. - * If the system property is not set then the environment variable CONFIG_DOMAIN is checked. - * If neither are set the domain defaults to prod. - */ - public Config() { - // Check the command line for a domain property. - mDomain = System.getProperty("CONFIG_DOMAIN"); - - // If the domain was not set with a property, check for an environment variable. - if (mDomain == null) { - mDomain = System.getenv("CONFIG_DOMAIN"); - } - - // If neither were set, default to prod - if (mDomain == null) { - mDomain = "prod"; - } - - mProperties = new Properties(); - } - - /** - * Change the domain from the default string "prod". - * - * @param domain The new domain. - */ - public void setDomain(String domain) { - LOG.info("Setting Config domain to " + domain); - mDomain = domain; - } - - /** - * @return the current domain. - */ - public String getDomain() { - return mDomain; - } - - /** - * Load properties from a file. - * Any exception are logged and suppressed. - */ - public void updateConfig(String propertyFilename) { - final File propFile = new File(propertyFilename); - - LOG.info("Loading properties from " + propFile); - - try { - InputStream in = new FileInputStream(propFile); - updateConfig(in); - - } catch (IOException e) { - LOG.error("Could not load properties file: " + e.getMessage(), e); - } - } - - /** - * Load properties from an InputStream. - * This method closes the InputStream when it completes. - * - * @param in The InputStream - */ - public void updateConfig(InputStream in) throws IOException { - LOG.info("Loading properties from InputStream"); - mProperties.load(in); - in.close(); - } - - /** - * Get a String from the config. - * - * @return The config value or null if it is not found. - */ - public String getString(String key) { - return getString(key, null); - } - - /** - * Get a String from the config. - * - * @return The config value or defaultValue if it can not be found. - */ - public String getString(final String key, final String defaultValue) { - String result; - - // Command line properties trump all. - result = System.getProperty(key); - if (result != null) { - LOG.debug("Reading System.getProperty(" + key + "). Got result = { " + result + " }"); - return result; - } - - // Environment variables can also override configs - result = System.getenv(key); - if (result != null) { - LOG.debug("Reading System.getenv(" + key + "). Got result = { " + result + " }"); - return result; - } - - final String domainKey = mDomain + "." + key; - result = mProperties.getProperty(domainKey); - if (result != null) { - LOG.debug("Reading config for key = { " + key + " }. Got result = { " + result + " }"); - return result; - } - - final String globalKey = "*." + key; - result = mProperties.getProperty(globalKey); - if (result != null) { - LOG.debug("Reading config for key = { " + key + " }. Got result = { " + result + " }"); - return result; - } - - LOG.debug("Reading config for key = { " + key + " }. Got default value = { " + defaultValue + " }"); - return defaultValue; - } - - /** - * Get an integer from the config. - * - * @return The config value or Integer.MIN_VALUE if the key is not present or the - * config can not be parsed. - */ - public int getInt(String key) { - return getInt(key, Integer.MIN_VALUE); - } - - /** - * Get an integer from the config. - * - * @return The config value or defaultValue if the key is not present or the - * config can not be parsed. - */ - public int getInt(String key, int defaultValue) { - final String propertyValue = getString(key); - - if (propertyValue != null) { - try { - final int result = Integer.valueOf(propertyValue); - return result; - - } catch (NumberFormatException e) { - LOG.warn("Expected property to be an integer: " - + key + " = { " + propertyValue + " }"); - } - } - - return defaultValue; - } - - public boolean getBoolean(String key) { - return getBoolean(key, false); - } - - public boolean getBoolean(String key, boolean defaultValue) { - final String propertyValue = getString(key); - - if (propertyValue != null) { - return (propertyValue.charAt(0) & 0xDF) == 'T'; - } - - return defaultValue; - } -} diff --git a/src/com/p4square/grow/frontend/AccountRedirectResource.java b/src/com/p4square/grow/frontend/AccountRedirectResource.java deleted file mode 100644 index be2ae65..0000000 --- a/src/com/p4square/grow/frontend/AccountRedirectResource.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 - */ -public class AccountRedirectResource extends ServerResource { - private static final Logger LOG = Logger.getLogger(AccountRedirectResource.class); - - private Config mConfig; - private Provider 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( - new JsonRequestProvider(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/com/p4square/grow/frontend/AssessmentResetPage.java b/src/com/p4square/grow/frontend/AssessmentResetPage.java deleted file mode 100644 index 519b135..0000000 --- a/src/com/p4square/grow/frontend/AssessmentResetPage.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 - */ -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/com/p4square/grow/frontend/AssessmentResultsPage.java b/src/com/p4square/grow/frontend/AssessmentResultsPage.java deleted file mode 100644 index f1c924b..0000000 --- a/src/com/p4square/grow/frontend/AssessmentResultsPage.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 - */ -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 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/com/p4square/grow/frontend/AuthenticatedResource.java b/src/com/p4square/grow/frontend/AuthenticatedResource.java deleted file mode 100644 index 800eb83..0000000 --- a/src/com/p4square/grow/frontend/AuthenticatedResource.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.frontend; - -import org.restlet.resource.ServerResource; -import org.restlet.representation.Representation; - -/** - * - * @author Jesse Morgan - */ -public class AuthenticatedResource extends ServerResource { - protected Representation post() { - return null; - } -} diff --git a/src/com/p4square/grow/frontend/ChapterCompletePage.java b/src/com/p4square/grow/frontend/ChapterCompletePage.java deleted file mode 100644 index 35abc43..0000000 --- a/src/com/p4square/grow/frontend/ChapterCompletePage.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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 - */ -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 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( - new JsonRequestProvider( - 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 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 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 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/com/p4square/grow/frontend/ErrorPage.java b/src/com/p4square/grow/frontend/ErrorPage.java deleted file mode 100644 index 81abe74..0000000 --- a/src/com/p4square/grow/frontend/ErrorPage.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 - */ -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 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 root) { - cTemplate = template; - cRoot = root; - } - - protected Representation getRepresentation() { - if (cTemplate == null) { - return new StringRepresentation(mMessage); - - } else { - Map root = new HashMap(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/com/p4square/grow/frontend/FeedData.java b/src/com/p4square/grow/frontend/FeedData.java deleted file mode 100644 index feb03a1..0000000 --- a/src/com/p4square/grow/frontend/FeedData.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 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> mThreadsProvider; - private final JsonRequestProvider mThreadProvider; - - private final JsonRequestProvider> mMessagesProvider; - private final JsonRequestProvider 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>(clientDispatcher, threadType); - mThreadProvider = new JsonRequestProvider(clientDispatcher, MessageThread.class); - - JavaType messageType = factory.constructCollectionType(List.class, Message.class); - mMessagesProvider = new JsonRequestProvider>(clientDispatcher, messageType); - mMessageProvider = new JsonRequestProvider(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 getThreads(final String topic, final int limit) throws IOException { - return mThreadsProvider.get(makeUrl(limit, topic)); - } - - public List 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/com/p4square/grow/frontend/FeedResource.java b/src/com/p4square/grow/frontend/FeedResource.java deleted file mode 100644 index 13d0fa0..0000000 --- a/src/com/p4square/grow/frontend/FeedResource.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java b/src/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java deleted file mode 100644 index 3ab140e..0000000 --- a/src/com/p4square/grow/frontend/GroupLeaderTrainingPageResource.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.frontend; - -/** - * Display the Group Leader training videos. - * - * @author Jesse Morgan - */ -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/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java deleted file mode 100644 index b5f62fb..0000000 --- a/src/com/p4square/grow/frontend/GrowFrontend.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -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 - */ -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/com/p4square/grow/frontend/IntegrationDriver.java b/src/com/p4square/grow/frontend/IntegrationDriver.java deleted file mode 100644 index b9c3508..0000000 --- a/src/com/p4square/grow/frontend/IntegrationDriver.java +++ /dev/null @@ -1,26 +0,0 @@ -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/com/p4square/grow/frontend/JsonRequestProvider.java b/src/com/p4square/grow/frontend/JsonRequestProvider.java deleted file mode 100644 index bf3b2b3..0000000 --- a/src/com/p4square/grow/frontend/JsonRequestProvider.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 - */ -public class JsonRequestProvider extends JsonEncodedProvider implements Provider { - - private final Restlet mDispatcher; - - public JsonRequestProvider(Restlet dispatcher, Class 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/com/p4square/grow/frontend/LoginFormAuthenticator.java b/src/com/p4square/grow/frontend/LoginFormAuthenticator.java deleted file mode 100644 index 21c9097..0000000 --- a/src/com/p4square/grow/frontend/LoginFormAuthenticator.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 - */ -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/com/p4square/grow/frontend/LoginPageResource.java b/src/com/p4square/grow/frontend/LoginPageResource.java deleted file mode 100644 index 38eba07..0000000 --- a/src/com/p4square/grow/frontend/LoginPageResource.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 - */ -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 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/com/p4square/grow/frontend/LogoutResource.java b/src/com/p4square/grow/frontend/LogoutResource.java deleted file mode 100644 index e26dcb7..0000000 --- a/src/com/p4square/grow/frontend/LogoutResource.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 - */ -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/com/p4square/grow/frontend/NewAccountResource.java b/src/com/p4square/grow/frontend/NewAccountResource.java deleted file mode 100644 index 5c13017..0000000 --- a/src/com/p4square/grow/frontend/NewAccountResource.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 - */ -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 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/com/p4square/grow/frontend/NewBelieverResource.java b/src/com/p4square/grow/frontend/NewBelieverResource.java deleted file mode 100644 index 8fe078a..0000000 --- a/src/com/p4square/grow/frontend/NewBelieverResource.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 - */ -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/com/p4square/grow/frontend/NotFoundException.java b/src/com/p4square/grow/frontend/NotFoundException.java deleted file mode 100644 index dfa2a4c..0000000 --- a/src/com/p4square/grow/frontend/NotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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/com/p4square/grow/frontend/ProgressReporter.java b/src/com/p4square/grow/frontend/ProgressReporter.java deleted file mode 100644 index 2f36832..0000000 --- a/src/com/p4square/grow/frontend/ProgressReporter.java +++ /dev/null @@ -1,30 +0,0 @@ -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/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java deleted file mode 100644 index 3575fe3..0000000 --- a/src/com/p4square/grow/frontend/SurveyPageResource.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * 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 - */ -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 mQuestionProvider; - private Provider 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( - new JsonRequestProvider(getContext().getClientDispatcher(), - Question.class)) { - @Override - public String makeKey(String questionId) { - return getBackendEndpoint() + "/assessment/question/" + questionId; - } - }; - - mUserRecordProvider = new DelegateProvider( - new JsonRequestProvider(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 answer = new HashMap(); - 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/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java deleted file mode 100644 index a1e7789..0000000 --- a/src/com/p4square/grow/frontend/TrainingPageResource.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * 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 - */ -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> VIDEO_COMPARATOR = new Comparator>() { - @Override - public int compare(Map left, Map 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 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(new JsonRequestProvider(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 chapters = playlist.getChapterStatuses(); - Map allowedChapters = new LinkedHashMap(); - - // 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> videos = null; - { - JsonResponse response = backendGet("/training/" + mChapter); - if (!response.getStatus().isSuccess()) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); - return null; - } - videos = (List>) response.getMap().get("videos"); - Collections.sort(videos, VIDEO_COMPARATOR); - } - - // Mark the completed videos as completed - int chapterProgress = 0; - for (Map 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/com/p4square/grow/frontend/VideosResource.java b/src/com/p4square/grow/frontend/VideosResource.java deleted file mode 100644 index 2099a77..0000000 --- a/src/com/p4square/grow/frontend/VideosResource.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 - */ -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(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 data = new HashMap(); - 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; - } -} diff --git a/src/com/p4square/grow/model/Answer.java b/src/com/p4square/grow/model/Answer.java deleted file mode 100644 index a818365..0000000 --- a/src/com/p4square/grow/model/Answer.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import org.apache.log4j.Logger; - -/** - * This is the model of an assessment question's answer. - * - * @author Jesse Morgan - */ -public class Answer { - private static final Logger LOG = Logger.getLogger(Answer.class); - - /** - * ScoreType determines how the answer will be scored. - * - */ - public static enum ScoreType { - /** - * This question has no effect on the score. - */ - NONE, - - /** - * The score of this question is part of the average. - */ - AVERAGE, - - /** - * The score of this question is the total score, no other questions - * matter after this point. - */ - TRUMP; - - @Override - public String toString() { - return name().toLowerCase(); - } - } - - private String mAnswerText; - private ScoreType mType; - private float mScoreFactor; - private String mNextQuestionId; - - public Answer() { - mType = ScoreType.AVERAGE; - } - - /** - * @return The text associated with the answer. - */ - public String getText() { - return mAnswerText; - } - - /** - * Set the text associated with the answer. - * @param text The new text. - */ - public void setText(String text) { - mAnswerText = text; - } - - /** - * @return the ScoreType for the Answer. - */ - public ScoreType getType() { - return mType; - } - - /** - * Set the ScoreType for the answer. - * @param type The new ScoreType. - */ - public void setType(ScoreType type) { - mType = type; - } - - /** - * @return the delta of the score if this answer is selected. - */ - public float getScore() { - if (mType == ScoreType.NONE) { - return 0; - } - - return mScoreFactor; - } - - /** - * Set the score delta for this answer. - * @param score The new delta. - */ - public void setScore(float score) { - mScoreFactor = score; - } - - /** - * @return the id of the next question if this answer is selected, or null - * if selecting this answer has no effect. - */ - public String getNextQuestion() { - return mNextQuestionId; - } - - /** - * Set the id of the next question when this answer is selected. - * @param id The next question id or null to proceed as usual. - */ - public void setNextQuestion(String id) { - mNextQuestionId = id; - } - - /** - * Adjust the running score for the selection of this answer. - * @param score The running score to adjust. - * @return true if scoring should continue, false if this answer trumps all. - */ - public boolean score(final Score score) { - switch (getType()) { - case TRUMP: - score.sum = getScore(); - score.count = 1; - return false; // Quit scoring. - - case AVERAGE: - LOG.debug("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }"); - score.sum += getScore(); - score.count++; - break; - - case NONE: - break; - } - - return true; // Continue scoring - } -} diff --git a/src/com/p4square/grow/model/Banner.java b/src/com/p4square/grow/model/Banner.java deleted file mode 100644 index b786b36..0000000 --- a/src/com/p4square/grow/model/Banner.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Page Banner Data. - */ -public class Banner { - private String mHtml; - - public String getHtml() { - return mHtml; - } - - public void setHtml(final String html) { - mHtml = html; - } -} diff --git a/src/com/p4square/grow/model/Chapter.java b/src/com/p4square/grow/model/Chapter.java deleted file mode 100644 index 3a08e4c..0000000 --- a/src/com/p4square/grow/model/Chapter.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * Chapter is a list of VideoRecords in a Playlist. - * - * @author Jesse Morgan - */ -public class Chapter implements Cloneable { - private String mName; - private Map mVideos; - - public Chapter(String name) { - mName = name; - mVideos = new HashMap(); - } - - /** - * Private constructor for JSON decoding. - */ - private Chapter() { - this(null); - } - - /** - * @return The Chapter name. - */ - public String getName() { - return mName; - } - - /** - * Set the chapter name. - * - * @param name The name of the chapter. - */ - public void setName(final String name) { - mName = name; - } - - /** - * @return The VideoRecord for videoid or null if videoid is not in the chapter. - */ - public VideoRecord getVideoRecord(String videoid) { - return mVideos.get(videoid); - } - - /** - * @return A map of video ids to VideoRecords. - */ - @JsonAnyGetter - public Map getVideos() { - return mVideos; - } - - /** - * Set the VideoRecord for a video id. - * @param videoId the video id. - * @param video the VideoRecord. - */ - @JsonAnySetter - public void setVideoRecord(String videoId, VideoRecord video) { - mVideos.put(videoId, video); - } - - /** - * Remove the VideoRecord for a video id. - * @param videoId The id to remove. - */ - public void removeVideoRecord(String videoId) { - mVideos.remove(videoId); - } - - /** - * @return true if every required video has been completed. - */ - @JsonIgnore - public boolean isComplete() { - boolean complete = true; - - for (VideoRecord r : mVideos.values()) { - if (r.getRequired() && !r.getComplete()) { - return false; - } - } - - return complete; - } - - /** - * Deeply clone a chapter. - * - * @return a new Chapter object identical but independent of this one. - */ - public Chapter clone() throws CloneNotSupportedException { - Chapter c = new Chapter(mName); - for (Map.Entry videoEntry : mVideos.entrySet()) { - c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone()); - } - return c; - } -} diff --git a/src/com/p4square/grow/model/CircleQuestion.java b/src/com/p4square/grow/model/CircleQuestion.java deleted file mode 100644 index 71acc14..0000000 --- a/src/com/p4square/grow/model/CircleQuestion.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Circle Question. - * - * @author Jesse Morgan - */ -public class CircleQuestion extends Question { - private static final ScoringEngine ENGINE = new QuadScoringEngine(); - - private String mTopLeft; - private String mTopRight; - private String mBottomLeft; - private String mBottomRight; - - /** - * @return the Top Left label. - */ - public String getTopLeft() { - return mTopLeft; - } - - /** - * Set the Top Left label. - * @param s The new top left label. - */ - public void setTopLeft(String s) { - mTopLeft = s; - } - - /** - * @return the Top Right label. - */ - public String getTopRight() { - return mTopRight; - } - - /** - * Set the Top Right label. - * @param s The new top left label. - */ - public void setTopRight(String s) { - mTopRight = s; - } - - /** - * @return the Bottom Left label. - */ - public String getBottomLeft() { - return mBottomLeft; - } - - /** - * Set the Bottom Left label. - * @param s The new top left label. - */ - public void setBottomLeft(String s) { - mBottomLeft = s; - } - - /** - * @return the Bottom Right label. - */ - public String getBottomRight() { - return mBottomRight; - } - - /** - * Set the Bottom Right label. - * @param s The new top left label. - */ - public void setBottomRight(String s) { - mBottomRight = s; - } - - @Override - public boolean scoreAnswer(Score score, RecordedAnswer answer) { - return ENGINE.scoreAnswer(score, this, answer); - } - - @Override - public QuestionType getType() { - return QuestionType.CIRCLE; - } -} diff --git a/src/com/p4square/grow/model/ImageQuestion.java b/src/com/p4square/grow/model/ImageQuestion.java deleted file mode 100644 index d94c32c..0000000 --- a/src/com/p4square/grow/model/ImageQuestion.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Image Question. - * - * @author Jesse Morgan - */ -public class ImageQuestion extends Question { - private static final ScoringEngine ENGINE = new SimpleScoringEngine(); - - @Override - public boolean scoreAnswer(Score score, RecordedAnswer answer) { - return ENGINE.scoreAnswer(score, this, answer); - } - - @Override - public QuestionType getType() { - return QuestionType.IMAGE; - } -} diff --git a/src/com/p4square/grow/model/Message.java b/src/com/p4square/grow/model/Message.java deleted file mode 100644 index 9d33320..0000000 --- a/src/com/p4square/grow/model/Message.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.Date; -import java.util.UUID; - -/** - * A feed message. - * - * @author Jesse Morgan - */ -public class Message { - private String mThreadId; - private String mId; - private UserRecord mAuthor; - private Date mCreated; - private String mMessage; - - /** - * @return a new message id. - */ - public static String generateId() { - return String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString()); - } - - /** - * @return The id of the thread that the message belongs to. - */ - public String getThreadId() { - return mThreadId; - } - - /** - * Set the id of the thread that the message belongs to. - * @param id The new thread id. - */ - public void setThreadId(String id) { - mThreadId = id; - } - - /** - * @return The id the message. - */ - public String getId() { - return mId; - } - - /** - * Set the id of the message. - * @param id The new message id. - */ - public void setId(String id) { - mId = id; - } - - /** - * @return The author of the message. - */ - public UserRecord getAuthor() { - return mAuthor; - } - - /** - * Set the author of the message. - * @param author The new author. - */ - public void setAuthor(UserRecord author) { - mAuthor = author; - } - - /** - * @return The Date the message was created. - */ - public Date getCreated() { - return mCreated; - } - - /** - * Set the Date the message was created. - * @param date The new creation date. - */ - public void setCreated(Date date) { - mCreated = date; - } - - /** - * @return The message text. - */ - public String getMessage() { - return mMessage; - } - - /** - * Set the message text. - * @param text The message text. - */ - public void setMessage(String text) { - mMessage = text; - } -} diff --git a/src/com/p4square/grow/model/MessageThread.java b/src/com/p4square/grow/model/MessageThread.java deleted file mode 100644 index 9542a18..0000000 --- a/src/com/p4square/grow/model/MessageThread.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.UUID; - -/** - * - * @author Jesse Morgan - */ -public class MessageThread { - private String mId; - private Message mMessage; - - /** - * Create a new thread with a probably unique id. - * - * @return the new thread. - */ - public static MessageThread createNew() { - MessageThread t = new MessageThread(); - // IDs are keyed to sort lexicographically from latest to oldest. - t.setId(String.format("%016x-%s", Long.MAX_VALUE - System.currentTimeMillis(), - UUID.randomUUID().toString())); - - return t; - } - - /** - * @return The id the message. - */ - public String getId() { - return mId; - } - - /** - * Set the id of the message. - * @param id The new message id. - */ - public void setId(String id) { - mId = id; - } - - /** - * @return The original message. - */ - public Message getMessage() { - return mMessage; - } - - /** - * Set the original message. - * @param id The new message. - */ - public void setMessage(Message message) { - mMessage = message; - } -} diff --git a/src/com/p4square/grow/model/Playlist.java b/src/com/p4square/grow/model/Playlist.java deleted file mode 100644 index 3e77ada..0000000 --- a/src/com/p4square/grow/model/Playlist.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * Representation of a user's playlist. - * - * @author Jesse Morgan - */ -public class Playlist { - /** - * Map of Chapter ID to map of Video ID to VideoRecord. - */ - private Map mPlaylist; - - private Date mLastUpdated; - - /** - * Construct an empty playlist. - */ - public Playlist() { - mPlaylist = new HashMap(); - mLastUpdated = new Date(0); // Default to a prehistoric date if we don't have one. - } - - /** - * Find the VideoRecord for a video id. - */ - public VideoRecord find(String videoId) { - for (Chapter chapter : mPlaylist.values()) { - VideoRecord r = chapter.getVideoRecord(videoId); - - if (r != null) { - return r; - } - } - - return null; - } - - /** - * @param videoId The video to search for. - * @return the Chapter containing videoId. - */ - private Chapter findChapter(String videoId) { - for (Chapter chapter : mPlaylist.values()) { - VideoRecord r = chapter.getVideoRecord(videoId); - - if (r != null) { - return chapter; - } - } - - return null; - } - - /** - * @return The last modified date of the source playlist. - */ - public Date getLastUpdated() { - return mLastUpdated; - } - - /** - * Set the last updated date. - * @param date the new last updated date. - */ - public void setLastUpdated(Date date) { - mLastUpdated = date; - } - - /** - * Add a video to the playlist. - */ - public VideoRecord add(String chapterId, String videoId) { - Chapter chapter = mPlaylist.get(chapterId); - - if (chapter == null) { - chapter = new Chapter(chapterId); - mPlaylist.put(chapterId, chapter); - } - - VideoRecord r = new VideoRecord(); - chapter.setVideoRecord(videoId, r); - return r; - } - - /** - * Add a Chapter to the Playlist. - * @param chapterId The name of the chapter. - * @param chapter The Chapter object to add. - */ - @JsonAnySetter - public void addChapter(String chapterId, Chapter chapter) { - chapter.setName(chapterId); - mPlaylist.put(chapterId, chapter); - } - - /** - * @return a map of chapter id to chapter. - */ - @JsonAnyGetter - public Map getChaptersMap() { - return mPlaylist; - } - - /** - * @return The last chapter to be completed. - */ - @JsonIgnore - public Map getChapterStatuses() { - Map completed = new HashMap(); - - for (Map.Entry entry : mPlaylist.entrySet()) { - completed.put(entry.getKey(), entry.getValue().isComplete()); - } - - return completed; - } - - /** - * @return true if all required videos in the chapter have been watched. - */ - public boolean isChapterComplete(String chapterId) { - Chapter chapter = mPlaylist.get(chapterId); - if (chapter != null) { - return chapter.isComplete(); - } - - return false; - } - - /** - * Merge a playlist into this playlist. - * - * Merge is accomplished by adding all missing Chapters and VideoRecords to - * this playlist. - */ - public void merge(Playlist source) { - if (source.getLastUpdated().before(mLastUpdated)) { - // Already up to date. - return; - } - - for (Map.Entry entry : source.getChaptersMap().entrySet()) { - String chapterName = entry.getKey(); - Chapter theirChapter = entry.getValue(); - Chapter myChapter = mPlaylist.get(entry.getKey()); - - if (myChapter == null) { - // Add new chapter - myChapter = new Chapter(chapterName); - addChapter(chapterName, myChapter); - } - - // Check chapter for missing videos - for (Map.Entry videoEntry : theirChapter.getVideos().entrySet()) { - String videoId = videoEntry.getKey(); - VideoRecord myVideo = myChapter.getVideoRecord(videoId); - - if (myVideo == null) { - myVideo = find(videoId); - if (myVideo == null) { - // New Video - try { - myVideo = videoEntry.getValue().clone(); - myChapter.setVideoRecord(videoId, myVideo); - } catch (CloneNotSupportedException e) { - throw new RuntimeException(e); // Unexpected... - } - } else { - // Video moved - findChapter(videoId).removeVideoRecord(videoId); - myChapter.setVideoRecord(videoId, myVideo); - } - } - } - } - - mLastUpdated = source.getLastUpdated(); - } -} diff --git a/src/com/p4square/grow/model/Point.java b/src/com/p4square/grow/model/Point.java deleted file mode 100644 index e9fc0ca..0000000 --- a/src/com/p4square/grow/model/Point.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Simple double based point class. - * - * @author Jesse Morgan - */ -public class Point { - /** - * Parse a comma separated x,y pair into a point. - * - * @return The point represented by the string. - * @throws IllegalArgumentException if the input is malformed. - */ - public static Point valueOf(String str) { - final int comma = str.indexOf(','); - if (comma == -1 || comma == 0 || comma == str.length() - 1) { - throw new IllegalArgumentException("Malformed point string"); - } - - final String sX = str.substring(0, comma); - final String sY = str.substring(comma + 1); - - return new Point(Double.valueOf(sX), Double.valueOf(sY)); - } - - private final double mX; - private final double mY; - - /** - * Create a new point with the given coordinates. - * - * @param x The x coordinate. - * @param y The y coordinate. - */ - public Point(double x, double y) { - mX = x; - mY = y; - } - - /** - * Compute the distance between this point and another. - * - * @param other The other point. - * @return The distance between this point and other. - */ - public double distance(Point other) { - final double dx = mX - other.mX; - final double dy = mY - other.mY; - - return Math.sqrt(dx*dx + dy*dy); - } - - /** - * @return The x coordinate. - */ - public double getX() { - return mX; - } - - /** - * @return The y coordinate. - */ - public double getY() { - return mY; - } - - /** - * @return The point represented as a comma separated pair. - */ - @Override - public String toString() { - return String.format("%.2f,%.2f", mX, mY); - } -} diff --git a/src/com/p4square/grow/model/QuadQuestion.java b/src/com/p4square/grow/model/QuadQuestion.java deleted file mode 100644 index a7b4179..0000000 --- a/src/com/p4square/grow/model/QuadQuestion.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Two-dimensional Question. - * - * @author Jesse Morgan - */ -public class QuadQuestion extends Question { - private static final ScoringEngine ENGINE = new QuadScoringEngine(); - - private String mTop; - private String mRight; - private String mBottom; - private String mLeft; - - /** - * @return the top label. - */ - public String getTop() { - return mTop; - } - - /** - * Set the top label. - * @param s The new top label. - */ - public void setTop(String s) { - mTop = s; - } - - /** - * @return the right label. - */ - public String getRight() { - return mRight; - } - - /** - * Set the right label. - * @param s The new right label. - */ - public void setRight(String s) { - mRight = s; - } - - /** - * @return the bottom label. - */ - public String getBottom() { - return mBottom; - } - - /** - * Set the bottom label. - * @param s The new bottom label. - */ - public void setBottom(String s) { - mBottom = s; - } - - /** - * @return the left label. - */ - public String getLeft() { - return mLeft; - } - - /** - * Set the left label. - * @param s The new left label. - */ - public void setLeft(String s) { - mLeft = s; - } - - @Override - public boolean scoreAnswer(Score score, RecordedAnswer answer) { - return ENGINE.scoreAnswer(score, this, answer); - } - - @Override - public QuestionType getType() { - return QuestionType.QUAD; - } -} diff --git a/src/com/p4square/grow/model/QuadScoringEngine.java b/src/com/p4square/grow/model/QuadScoringEngine.java deleted file mode 100644 index 33403b5..0000000 --- a/src/com/p4square/grow/model/QuadScoringEngine.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import com.p4square.grow.model.Point; - -/** - * QuadScoringEngine expects the user's answer to be a Point string. We find - * the closest answer Point to the user's answer and treat that as the answer. - * - * @author Jesse Morgan - */ -public class QuadScoringEngine extends ScoringEngine { - - @Override - public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { - // Find all of the answer points. - Point[] answers = new Point[question.getAnswers().size()]; - { - int i = 0; - for (String answerStr : question.getAnswers().keySet()) { - answers[i++] = Point.valueOf(answerStr); - } - } - - // Parse the user's answer. - Point userPoint = Point.valueOf(userAnswer.getAnswerId()); - - // Find the closest answer point to the user's answer. - double minDistance = Double.MAX_VALUE; - int answerIndex = 0; - for (int i = 0; i < answers.length; i++) { - final double distance = userPoint.distance(answers[i]); - if (distance < minDistance) { - minDistance = distance; - answerIndex = i; - } - } - - LOG.debug("Quad " + question.getId() + ": Got answer " - + answers[answerIndex].toString() + " for user point " + userAnswer); - - // Get the answer and update the score. - final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); - return answer.score(score); - } -} diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java deleted file mode 100644 index f4b9458..0000000 --- a/src/com/p4square/grow/model/Question.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonSubTypes.Type; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -/** - * Model of an assessment question. - * - * @author Jesse Morgan - */ -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") -@JsonSubTypes({ - @Type(value = TextQuestion.class, name = "text"), - @Type(value = ImageQuestion.class, name = "image"), - @Type(value = SliderQuestion.class, name = "slider"), - @Type(value = QuadQuestion.class, name = "quad"), - @Type(value = CircleQuestion.class, name = "circle"), -}) -public abstract class Question { - /** - * QuestionType indicates the type of Question. - * - * @author Jesse Morgan - */ - public enum QuestionType { - TEXT, - IMAGE, - SLIDER, - QUAD, - CIRCLE; - - @Override - public String toString() { - return name().toLowerCase(); - } - } - - private String mQuestionId; - private QuestionType mType; - private String mQuestionText; - private Map mAnswers; - - private String mPreviousQuestionId; - private String mNextQuestionId; - - public Question() { - mAnswers = new HashMap(); - } - - /** - * @return the id String for this question. - */ - public String getId() { - return mQuestionId; - } - - /** - * Set the id String for this question. - * @param id New id - */ - public void setId(String id) { - mQuestionId = id; - } - - /** - * @return The Question text. - */ - public String getQuestion() { - return mQuestionText; - } - - /** - * Set the question text. - * @param value The new question text. - */ - public void setQuestion(String value) { - mQuestionText = value; - } - - /** - * @return The id String of the previous question or null if no previous question exists. - */ - public String getPreviousQuestion() { - return mPreviousQuestionId; - } - - /** - * Set the id string of the previous question. - * @param id Previous question id or null if there is no previous question. - */ - public void setPreviousQuestion(String id) { - mPreviousQuestionId = id; - } - - /** - * @return The id String of the next question or null if no next question exists. - */ - public String getNextQuestion() { - return mNextQuestionId; - } - - /** - * Set the id string of the next question. - * @param id next question id or null if there is no next question. - */ - public void setNextQuestion(String id) { - mNextQuestionId = id; - } - - /** - * @return a map of Answer id Strings to Answer objects. - */ - public Map getAnswers() { - return mAnswers; - } - - /** - * Determine the id of the next question based on the answer to this - * question. - * - * @param answerid - * The id of the selected answer. - * @return a question id or null if this is the last question. - */ - public String getNextQuestion(String answerid) { - String nextQuestion = null; - - Answer a = mAnswers.get(answerid); - if (a != null) { - nextQuestion = a.getNextQuestion(); - } - - if (nextQuestion == null) { - nextQuestion = mNextQuestionId; - } - - return nextQuestion; - } - - /** - * Update the score based on the answer to this question. - * - * @param score The running score to update. - * @param answer The answer give to this question. - * @return true if scoring should continue, false if this answer trumps everything else. - */ - public abstract boolean scoreAnswer(Score score, RecordedAnswer answer); - - /** - * @return the QuestionType of this question. - */ - public abstract QuestionType getType(); - -} diff --git a/src/com/p4square/grow/model/RecordedAnswer.java b/src/com/p4square/grow/model/RecordedAnswer.java deleted file mode 100644 index 7d9905d..0000000 --- a/src/com/p4square/grow/model/RecordedAnswer.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Simple model for a user's assessment answer. - * - * @author Jesse Morgan - */ -public class RecordedAnswer { - private String mAnswerId; - - /** - * @return The user's answer. - */ - public String getAnswerId() { - return mAnswerId; - } - - /** - * Set the answer id field. - * @param id The new id. - */ - public void setAnswerId(String id) { - mAnswerId = id; - } - - @Override - public String toString() { - return mAnswerId; - } -} diff --git a/src/com/p4square/grow/model/Score.java b/src/com/p4square/grow/model/Score.java deleted file mode 100644 index 031c309..0000000 --- a/src/com/p4square/grow/model/Score.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Simple structure containing a score's sum and count. - * - * @author Jesse Morgan - */ -public class Score { - /** - * Return the decimal value for the given Score String. - * - * This method satisfies the invariant for Score x: - * numericScore(x.toString()) <= x.getScore() - */ - public static double numericScore(String score) { - score = score.toLowerCase(); - - if ("teacher".equals(score)) { - return 3.5; - } else if ("disciple".equals(score)) { - return 2.5; - } else if ("believer".equals(score)) { - return 1.5; - } else if ("seeker".equals(score)) { - return 0; - } else { - return Integer.MAX_VALUE; - } - } - - double sum; - int count; - - public Score() { - sum = 0; - count = 0; - } - - public Score(double sum, int count) { - this.sum = sum; - this.count = count; - } - - /** - * Copy Constructor. - */ - public Score(Score other) { - sum = other.sum; - count = other.count; - } - - /** - * @return The sum of all the points. - */ - public double getSum() { - return sum; - } - - /** - * @return The number of questions included in the score. - */ - public int getCount() { - return count; - } - - /** - * @return The final score. - */ - public double getScore() { - if (count == 0) { - return 0; - } - - return sum / count; - } - - /** - * @return the lowest score in the same category as this score. - */ - public double floor() { - final double score = getScore(); - - if (score >= 3.5) { - return 3.5; // teacher - - } else if (score >= 2.5) { - return 2.5; // disciple - - } else if (score >= 1.5) { - return 1.5; // believer - - } else { - return 0; // seeker - } - } - - @Override - public String toString() { - final double score = getScore(); - - if (score >= 3.5) { - return "teacher"; - - } else if (score >= 2.5) { - return "disciple"; - - } else if (score >= 1.5) { - return "believer"; - - } else { - return "seeker"; - } - } - -} diff --git a/src/com/p4square/grow/model/ScoringEngine.java b/src/com/p4square/grow/model/ScoringEngine.java deleted file mode 100644 index 8ff18b3..0000000 --- a/src/com/p4square/grow/model/ScoringEngine.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import org.apache.log4j.Logger; - -/** - * ScoringEngine computes the score for a question and a given answer. - * - * @author Jesse Morgan - */ -public abstract class ScoringEngine { - protected static final Logger LOG = Logger.getLogger(ScoringEngine.class); - - /** - * Update the score based on the given question and answer. - * - * @param score The running score to update. - * @param question The question to compute the score for. - * @param answer The answer give to this question. - * @return true if scoring should continue, false if this answer trumps everything else. - */ - public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer); -} diff --git a/src/com/p4square/grow/model/SimpleScoringEngine.java b/src/com/p4square/grow/model/SimpleScoringEngine.java deleted file mode 100644 index 6ef2dbb..0000000 --- a/src/com/p4square/grow/model/SimpleScoringEngine.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * SimpleScoringEngine expects the user's answer to a valid answer id and - * scores accordingly. - * - * If the answer id is not valid an Exception is thrown. - * - * @author Jesse Morgan - */ -public class SimpleScoringEngine extends ScoringEngine { - - @Override - public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { - final Answer answer = question.getAnswers().get(userAnswer.getAnswerId()); - if (answer == null) { - throw new IllegalArgumentException("Not a valid answer."); - } - - return answer.score(score); - } -} diff --git a/src/com/p4square/grow/model/SliderQuestion.java b/src/com/p4square/grow/model/SliderQuestion.java deleted file mode 100644 index f0861e3..0000000 --- a/src/com/p4square/grow/model/SliderQuestion.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Slider Question. - * - * @author Jesse Morgan - */ -public class SliderQuestion extends Question { - private static final ScoringEngine ENGINE = new SliderScoringEngine(); - - @Override - public boolean scoreAnswer(Score score, RecordedAnswer answer) { - return ENGINE.scoreAnswer(score, this, answer); - } - - @Override - public QuestionType getType() { - return QuestionType.SLIDER; - } -} diff --git a/src/com/p4square/grow/model/SliderScoringEngine.java b/src/com/p4square/grow/model/SliderScoringEngine.java deleted file mode 100644 index 2961e95..0000000 --- a/src/com/p4square/grow/model/SliderScoringEngine.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * SliderScoringEngine expects the user's answer to be a decimal value in the - * range [0, 1]. The value is scaled to the range [1, 4] and added to the - * score. - * - * @author Jesse Morgan - */ -public class SliderScoringEngine extends ScoringEngine { - - @Override - public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { - int numberOfAnswers = question.getAnswers().size(); - if (numberOfAnswers == 0) { - throw new IllegalArgumentException("Question has no answers."); - } - - double answer = Double.valueOf(userAnswer.getAnswerId()); - if (answer < 0 || answer > 1) { - throw new IllegalArgumentException("Answer out of bounds."); - } - - double delta = Math.max(1, Math.ceil(answer * numberOfAnswers) / numberOfAnswers * 4); - - score.sum += delta; - score.count++; - - return true; - } -} diff --git a/src/com/p4square/grow/model/TextQuestion.java b/src/com/p4square/grow/model/TextQuestion.java deleted file mode 100644 index 88c2a34..0000000 --- a/src/com/p4square/grow/model/TextQuestion.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Text Question. - * - * @author Jesse Morgan - */ -public class TextQuestion extends Question { - private static final ScoringEngine ENGINE = new SimpleScoringEngine(); - - @Override - public boolean scoreAnswer(Score score, RecordedAnswer answer) { - return ENGINE.scoreAnswer(score, this, answer); - } - - @Override - public QuestionType getType() { - return QuestionType.TEXT; - } -} diff --git a/src/com/p4square/grow/model/TrainingRecord.java b/src/com/p4square/grow/model/TrainingRecord.java deleted file mode 100644 index bc3ffa9..0000000 --- a/src/com/p4square/grow/model/TrainingRecord.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -/** - * Representation of a user's training record. - * - * @author Jesse Morgan - */ -public class TrainingRecord { - private String mLastVideo; - private Playlist mPlaylist; - - public TrainingRecord() { - mPlaylist = new Playlist(); - } - - /** - * @return Video id of the last video watched. - */ - public String getLastVideo() { - return mLastVideo; - } - - /** - * Set the video id for the last video watched. - * @param video The new video id. - */ - public void setLastVideo(String video) { - mLastVideo = video; - } - - /** - * @return the user's Playlist. - */ - public Playlist getPlaylist() { - return mPlaylist; - } - - /** - * Set the user's playlist. - * @param playlist The new playlist. - */ - public void setPlaylist(Playlist playlist) { - mPlaylist = playlist; - } -} diff --git a/src/com/p4square/grow/model/UserRecord.java b/src/com/p4square/grow/model/UserRecord.java deleted file mode 100644 index 4399282..0000000 --- a/src/com/p4square/grow/model/UserRecord.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.apache.commons.codec.binary.Hex; - -import org.restlet.security.User; - -/** - * A simple user representation without any secrets. - */ -public class UserRecord { - private String mId; - private String mFirstName; - private String mLastName; - private String mEmail; - private String mLanding; - private boolean mNewBeliever; - - // Backend Access - private String mBackendPasswordHash; - - /** - * Create an empty UserRecord. - */ - public UserRecord() { - } - - /** - * Create a new UserRecord with the information from a User. - */ - public UserRecord(final User user) { - mId = user.getIdentifier(); - mFirstName = user.getFirstName(); - mLastName = user.getLastName(); - mEmail = user.getEmail(); - } - - /** - * @return The user's identifier. - */ - public String getId() { - return mId; - } - - /** - * Set the user's identifier. - * @param value The new id. - */ - public void setId(final String value) { - mId = value; - } - - /** - * @return The user's email. - */ - public String getEmail() { - return mEmail; - } - - /** - * Set the user's email. - * @param value The new email. - */ - public void setEmail(final String value) { - mEmail = value; - } - - /** - * @return The user's first name. - */ - public String getFirstName() { - return mFirstName; - } - - /** - * Set the user's first name. - * @param value The new first name. - */ - public void setFirstName(final String value) { - mFirstName = value; - } - - /** - * @return The user's last name. - */ - public String getLastName() { - return mLastName; - } - - /** - * Set the user's last name. - * @param value The new last name. - */ - public void setLastName(final String value) { - mLastName = value; - } - - /** - * @return The user's landing page. - */ - public String getLanding() { - return mLanding; - } - - /** - * Set the user's landing page. - * @param value The new landing page. - */ - public void setLanding(final String value) { - mLanding = value; - } - - /** - * @return true if the user came from the New Believer's landing. - */ - public boolean getNewBeliever() { - return mNewBeliever; - } - - /** - * Set the user's new believer flag. - * @param value The new flag. - */ - public void setNewBeliever(final boolean value) { - mNewBeliever = value; - } - - /** - * @return The user's backend password hash, null if he doesn't have - * access. - */ - public String getBackendPasswordHash() { - return mBackendPasswordHash; - } - - /** - * Set the user's backend password hash. - * @param value The new backend password hash or null to remove - * access. - */ - public void setBackendPasswordHash(final String value) { - mBackendPasswordHash = value; - } - - /** - * Set the user's backend password to the clear-text value given. - * @param value The new backend password. - */ - public void setBackendPassword(final String value) { - try { - mBackendPasswordHash = hashPassword(value); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - /** - * Hash the given secret. - */ - public static String hashPassword(final String secret) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - - // Convert the char[] to byte[] - // FIXME This approach is incorrectly truncating multibyte - // characters. - byte[] b = new byte[secret.length()]; - for (int i = 0; i < secret.length(); i++) { - b[i] = (byte) secret.charAt(i); - } - - md.update(b); - - byte[] hash = md.digest(); - return new String(Hex.encodeHex(hash)); - } -} diff --git a/src/com/p4square/grow/model/VideoRecord.java b/src/com/p4square/grow/model/VideoRecord.java deleted file mode 100644 index ec99d0d..0000000 --- a/src/com/p4square/grow/model/VideoRecord.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.model; - -import java.util.Date; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * Simple bean containing video completion data. - * - * @author Jesse Morgan - */ -public class VideoRecord implements Cloneable { - private Boolean mComplete; - private Boolean mRequired; - private Date mCompletionDate; - - public VideoRecord() { - mComplete = null; - mRequired = null; - mCompletionDate = null; - } - - public boolean getComplete() { - if (mComplete == null) { - return false; - } - return mComplete; - } - - public void setComplete(boolean complete) { - mComplete = complete; - } - - @JsonIgnore - public boolean isCompleteSet() { - return mComplete != null; - } - - public boolean getRequired() { - if (mRequired == null) { - return true; - } - return mRequired; - } - - public void setRequired(boolean complete) { - mRequired = complete; - } - - @JsonIgnore - public boolean isRequiredSet() { - return mRequired != null; - } - - public Date getCompletionDate() { - return mCompletionDate; - } - - public void setCompletionDate(Date date) { - mCompletionDate = date; - } - - /** - * Convenience method to mark a video complete. - */ - public void complete() { - mComplete = true; - mCompletionDate = new Date(); - } - - /** - * @return an identical clone of this record. - */ - public VideoRecord clone() throws CloneNotSupportedException { - VideoRecord r = (VideoRecord) super.clone(); - r.mComplete = mComplete; - r.mRequired = mRequired; - r.mCompletionDate = mCompletionDate; - return r; - } -} diff --git a/src/com/p4square/grow/provider/CollectionProvider.java b/src/com/p4square/grow/provider/CollectionProvider.java deleted file mode 100644 index e4e9040..0000000 --- a/src/com/p4square/grow/provider/CollectionProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; -import java.util.Map; - -/** - * ListProvider is the logical extension of Provider for dealing with lists of - * items. - * - * @param C The type of the collection key. - * @param K The type of the item key. - * @param V The type of the value. - * - * @author Jesse Morgan - */ -public interface CollectionProvider { - /** - * Retrieve a specific object from the collection. - * - * @param collection The collection key. - * @param key The key for the object in the collection. - * @return The object or null if not found. - */ - V get(C collection, K key) throws IOException; - - /** - * Retrieve a collection. - * - * The returned map will never be null. - * - * @param collection The collection key. - * @return A Map of keys to values. - */ - Map query(C collection) throws IOException; - - /** - * Retrieve a portion of a collection. - * - * The returned map will never be null. - * - * @param collection The collection key. - * @param limit Max number of items to return. - * @return A Map of keys to values. - */ - Map query(C collection, int limit) throws IOException; - - /** - * Persist the object with the given key. - * - * @param collection The collection key. - * @param key The key for the object in the collection. - * @param obj The object to persist. - */ - void put(C collection, K key, V obj) throws IOException; -} diff --git a/src/com/p4square/grow/provider/DelegateCollectionProvider.java b/src/com/p4square/grow/provider/DelegateCollectionProvider.java deleted file mode 100644 index cf697ba..0000000 --- a/src/com/p4square/grow/provider/DelegateCollectionProvider.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * - * @author Jesse Morgan - */ -public abstract class DelegateCollectionProvider - implements CollectionProvider { - - private CollectionProvider mProvider; - - public DelegateCollectionProvider(final CollectionProvider provider) { - mProvider = provider; - } - - public V get(C collection, K key) throws IOException { - return mProvider.get(makeCollectionKey(collection), makeKey(key)); - } - - public Map query(C collection) throws IOException { - return query(collection, -1); - } - - public Map query(C collection, int limit) throws IOException { - Map delegateResult = mProvider.query(makeCollectionKey(collection), limit); - Map result = new LinkedHashMap<>(); - for (Map.Entry entry : delegateResult.entrySet()) { - result.put(unmakeKey(entry.getKey()), entry.getValue()); - } - - return result; - } - - public void put(C collection, K key, V obj) throws IOException { - mProvider.put(makeCollectionKey(collection), makeKey(key), obj); - } - - /** - * Make a collection key for the delegated provider. - * - * @param input The pre-transform key. - * @return the post-transform key. - */ - protected abstract DC makeCollectionKey(final C input); - - /** - * Make a key for the delegated provider. - * - * @param input The pre-transform key. - * @return the post-transform key. - */ - protected abstract DK makeKey(final K input); - - /** - * Transform a key for the delegated provider to an input key. - * - * @param input The post-transform key. - * @return the pre-transform key. - */ - protected abstract K unmakeKey(final DK input); -} diff --git a/src/com/p4square/grow/provider/DelegateProvider.java b/src/com/p4square/grow/provider/DelegateProvider.java deleted file mode 100644 index 42dcc63..0000000 --- a/src/com/p4square/grow/provider/DelegateProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -/** - * DelegateProvider wraps an existing Provider an transforms the key from - * type K to type D. - * - * @author Jesse Morgan - */ -public abstract class DelegateProvider implements Provider { - - private Provider mProvider; - - public DelegateProvider(final Provider provider) { - mProvider = provider; - } - - @Override - public V get(final K key) throws IOException { - return mProvider.get(makeKey(key)); - } - - @Override - public void put(final K key, final V obj) throws IOException { - mProvider.put(makeKey(key), obj); - } - - /** - * Make a Key for the delegated provider. - * - * @param input The pre-transform key. - * @return the post-transform key. - */ - protected abstract D makeKey(final K input); -} diff --git a/src/com/p4square/grow/provider/JsonEncodedProvider.java b/src/com/p4square/grow/provider/JsonEncodedProvider.java deleted file mode 100644 index 500f761..0000000 --- a/src/com/p4square/grow/provider/JsonEncodedProvider.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * Provider provides a simple interface for loading and persisting - * objects. - * - * @author Jesse Morgan - */ -public abstract class JsonEncodedProvider { - public static final ObjectMapper MAPPER = new ObjectMapper(); - static { - MAPPER.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); - MAPPER.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); - MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - protected final Class mClazz; - protected final JavaType mType; - - public JsonEncodedProvider(Class clazz) { - mClazz = clazz; - mType = null; - } - - public JsonEncodedProvider(JavaType type) { - mType = type; - mClazz = null; - } - - /** - * Encode the object as JSON. - * - * @param obj The object to encode. - * @return The JSON encoding of obj. - * @throws IOException if the object cannot be encoded. - */ - protected String encode(V obj) throws IOException { - if (mClazz == String.class) { - return (String) obj; - } - - return MAPPER.writeValueAsString(obj); - } - - /** - * Decode the JSON string as an object. - * - * @param blob The JSON data to decode. - * @return The decoded object or null if blob is null. - * @throws IOException If an object cannot be decoded. - */ - protected V decode(String blob) throws IOException { - if (blob == null) { - return null; - } - - if (mClazz == String.class) { - return (V) blob; - } - - V obj; - if (mClazz != null) { - obj = MAPPER.readValue(blob, mClazz); - - } else { - obj = MAPPER.readValue(blob, mType); - } - - return obj; - } -} - diff --git a/src/com/p4square/grow/provider/MapCollectionProvider.java b/src/com/p4square/grow/provider/MapCollectionProvider.java deleted file mode 100644 index 4c5cef6..0000000 --- a/src/com/p4square/grow/provider/MapCollectionProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2015 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.HashMap; - -/** - * In-memory CollectionProvider implementation, useful for tests. - * - * @author Jesse Morgan - */ -public class MapCollectionProvider implements CollectionProvider { - private final Map> mMap; - - public MapCollectionProvider() { - mMap = new HashMap<>(); - } - - @Override - public synchronized V get(C collection, K key) throws IOException { - Map map = mMap.get(collection); - if (map != null) { - return map.get(key); - } - - return null; - } - - @Override - public synchronized Map query(C collection) throws IOException { - Map map = mMap.get(collection); - if (map == null) { - map = new HashMap(); - } - - return map; - } - - @Override - public synchronized Map query(C collection, int limit) throws IOException { - Map map = query(collection); - - if (map.size() > limit) { - Map smallMap = new HashMap<>(); - - Iterator> iterator = map.entrySet().iterator(); - for (int i = 0; i < limit; i++) { - Map.Entry entry = iterator.next(); - smallMap.put(entry.getKey(), entry.getValue()); - } - - return smallMap; - - } else { - return map; - } - } - - @Override - public synchronized void put(C collection, K key, V obj) throws IOException { - Map map = mMap.get(collection); - if (map == null) { - map = new HashMap(); - mMap.put(collection, map); - } - - map.put(key, obj); - } -} diff --git a/src/com/p4square/grow/provider/MapProvider.java b/src/com/p4square/grow/provider/MapProvider.java deleted file mode 100644 index 40f8107..0000000 --- a/src/com/p4square/grow/provider/MapProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2015 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; -import java.util.Map; -import java.util.HashMap; - -/** - * In-memory Provider implementation, useful for tests. - * - * @author Jesse Morgan - */ -public class MapProvider implements Provider { - private final Map mMap = new HashMap(); - - @Override - public V get(K key) throws IOException { - return mMap.get(key); - } - - @Override - public void put(K key, V obj) throws IOException { - mMap.put(key, obj); - } -} diff --git a/src/com/p4square/grow/provider/Provider.java b/src/com/p4square/grow/provider/Provider.java deleted file mode 100644 index ca6af25..0000000 --- a/src/com/p4square/grow/provider/Provider.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -/** - * Provider provides a simple interface for loading and persisting - * objects. - * - * @author Jesse Morgan - */ -public interface Provider { - /** - * Retrieve the object with the given key. - * - * @param key The key for the object. - * @return The object or null if not found. - */ - V get(K key) throws IOException; - - /** - * Persist the object with the given key. - * - * @param key The key for the object. - * @param obj The object to persist. - */ - void put(K key, V obj) throws IOException; -} diff --git a/src/com/p4square/grow/provider/ProvidesAssessments.java b/src/com/p4square/grow/provider/ProvidesAssessments.java deleted file mode 100644 index 62ba8f6..0000000 --- a/src/com/p4square/grow/provider/ProvidesAssessments.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import com.p4square.grow.model.RecordedAnswer; - -/** - * - * @author Jesse Morgan - */ -public interface ProvidesAssessments { - /** - * Provides a collection of user assessments. - * The collection key is the user id. - * The key is the question id. - */ - CollectionProvider getAnswerProvider(); -} diff --git a/src/com/p4square/grow/provider/ProvidesQuestions.java b/src/com/p4square/grow/provider/ProvidesQuestions.java deleted file mode 100644 index b43f649..0000000 --- a/src/com/p4square/grow/provider/ProvidesQuestions.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import com.p4square.grow.model.Question; - -/** - * Indicates the ability to provide a Question Provider. - * - * @author Jesse Morgan - */ -public interface ProvidesQuestions { - /** - * @return A Provider of Questions keyed by question id. - */ - Provider getQuestionProvider(); -} diff --git a/src/com/p4square/grow/provider/ProvidesStrings.java b/src/com/p4square/grow/provider/ProvidesStrings.java deleted file mode 100644 index 5d9976e..0000000 --- a/src/com/p4square/grow/provider/ProvidesStrings.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -/** - * Indicates the ability to provide a String provider. - * - * Strings are typically configuration settings stored as a String. - * - * @author Jesse Morgan - */ -public interface ProvidesStrings { - /** - * @return A Provider of Questions keyed by question id. - */ - Provider getStringProvider(); -} \ No newline at end of file diff --git a/src/com/p4square/grow/provider/ProvidesTrainingRecords.java b/src/com/p4square/grow/provider/ProvidesTrainingRecords.java deleted file mode 100644 index 586e649..0000000 --- a/src/com/p4square/grow/provider/ProvidesTrainingRecords.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.Playlist; - -/** - * Indicates the ability to provide a TrainingRecord Provider. - * - * @author Jesse Morgan - */ -public interface ProvidesTrainingRecords { - /** - * @return A Provider of Questions keyed by question id. - */ - Provider getTrainingRecordProvider(); - - /** - * @return the Default Playlist. - */ - Playlist getDefaultPlaylist() throws IOException; -} diff --git a/src/com/p4square/grow/provider/ProvidesUserRecords.java b/src/com/p4square/grow/provider/ProvidesUserRecords.java deleted file mode 100644 index d77c878..0000000 --- a/src/com/p4square/grow/provider/ProvidesUserRecords.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import com.p4square.grow.model.UserRecord; - -/** - * Indicates the ability to provide a UserRecord Provider. - * - * @author Jesse Morgan - */ -public interface ProvidesUserRecords { - /** - * @return A Provider of Questions keyed by question id. - */ - Provider getUserRecordProvider(); -} diff --git a/src/com/p4square/grow/provider/ProvidesVideos.java b/src/com/p4square/grow/provider/ProvidesVideos.java deleted file mode 100644 index 3d055d3..0000000 --- a/src/com/p4square/grow/provider/ProvidesVideos.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.provider; - -/** - * - * @author Jesse Morgan - */ -public interface ProvidesVideos { - /** - * @return A Provider of Questions keyed by question id. - */ - CollectionProvider getVideoProvider(); -} diff --git a/src/com/p4square/grow/provider/TrainingRecordProvider.java b/src/com/p4square/grow/provider/TrainingRecordProvider.java deleted file mode 100644 index 44dba87..0000000 --- a/src/com/p4square/grow/provider/TrainingRecordProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -import com.p4square.grow.model.TrainingRecord; - -/** - * TrainingRecordProvider wraps an existing Provider to get and put TrainingRecords. - * - * @author Jesse Morgan - */ -public abstract class TrainingRecordProvider implements Provider { - - private Provider mProvider; - - public TrainingRecordProvider(Provider provider) { - mProvider = provider; - } - - @Override - public TrainingRecord get(String key) throws IOException { - return mProvider.get(makeKey(key)); - } - - @Override - public void put(String key, TrainingRecord obj) throws IOException { - mProvider.put(makeKey(key), obj); - } - - /** - * Make a Key for a TrainingRecord.. - * - * @param userId The user id. - * @return a key for the TrainingRecord of userid. - */ - protected abstract K makeKey(String userId); -} diff --git a/src/com/p4square/grow/tools/AssessmentStats.java b/src/com/p4square/grow/tools/AssessmentStats.java deleted file mode 100644 index ca83411..0000000 --- a/src/com/p4square/grow/tools/AssessmentStats.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.tools; - - -import java.util.Map; -import java.util.HashMap; -import java.util.Queue; -import java.util.List; -import java.util.LinkedList; -import java.io.IOException; - -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; - -import com.p4square.grow.model.Answer; -import com.p4square.grow.model.Question; -import com.p4square.grow.model.RecordedAnswer; -import com.p4square.grow.model.Score; -import com.p4square.grow.provider.Provider; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * - * @author Jesse Morgan - */ -public class AssessmentStats { - public static void main(String... args) throws Exception { - if (args.length == 0) { - System.out.println("Usage: AssessmentStats directory firstQuestionId"); - System.exit(1); - } - - Map questions; - questions = loadQuestions(args[0], args[1]); - - // Find the highest possible score - List scores = findHighestFromId(questions, args[1]); - - // Print Results - System.out.printf("Found %d different paths.\n", scores.size()); - int i = 0; - for (AnswerPath path : scores) { - Score s = path.mScore; - System.out.printf("Path %d: %f points, %d questions. Score: %f (%s)\n", - i++, s.getSum(), s.getCount(), s.getScore(), s.toString()); - System.out.println(" " + path.mPath); - System.out.println(" " + path.mScores); - } - } - - private static Map loadQuestions(String baseDir, String firstId) throws IOException { - FileQuestionProvider provider = new FileQuestionProvider(baseDir); - - // Questions to find... - Queue queue = new LinkedList<>(); - queue.offer(firstId); - - Map questions = new HashMap<>(); - - - while (!queue.isEmpty()) { - Question q = provider.get(queue.poll()); - questions.put(q.getId(), q); - - if (q.getNextQuestion() != null) { - queue.offer(q.getNextQuestion()); - - } - - for (Answer a : q.getAnswers().values()) { - if (a.getNextQuestion() != null) { - queue.offer(a.getNextQuestion()); - } - } - - // Quick Sanity check - if (q.getPreviousQuestion() != null) { - if (questions.get(q.getPreviousQuestion()) == null) { - throw new IllegalStateException("Haven't seen previous question??"); - } - } - } - - return questions; - } - - private static List findHighestFromId(Map questions, String id) { - List scores = new LinkedList<>(); - doFindHighestFromId(questions, id, scores, new AnswerPath()); - return scores; - } - - private static void doFindHighestFromId(Map questions, String id, List scores, AnswerPath path) { - if (id == null) { - // End of the road! Save the score and return. - scores.add(path); - return; - } - - Question q = questions.get(id); - - // Find the best answer following this path and find other paths. - Score maxScore = path.mScore; - double max = 0; - - int answerCount = 1; - for (Map.Entry entry : q.getAnswers().entrySet()) { - Answer a = entry.getValue(); - RecordedAnswer userAnswer = new RecordedAnswer(); - - if (q.getType() == Question.QuestionType.SLIDER) { - // Special Case - userAnswer.setAnswerId(String.valueOf((float) answerCount / q.getAnswers().size())); - - } else { - userAnswer.setAnswerId(entry.getKey()); - } - - Score tempScore = new Score(path.mScore); // Always start with the initial score. - boolean endOfRoad = !q.scoreAnswer(tempScore, userAnswer); - double thisScore = tempScore.getSum() - path.mScore.getSum(); - - if (endOfRoad) { - // End of Road is a fork too. Record and pick another answer. - AnswerPath fork = new AnswerPath(path); - fork.update(id, tempScore); - scores.add(fork); - - } else if (a.getNextQuestion() != null) { - // Found a new path, follow it. - // Remember to count this answer in the score. - AnswerPath fork = new AnswerPath(path); - fork.update(id, tempScore); - doFindHighestFromId(questions, a.getNextQuestion(), scores, fork); - - } else if (thisScore > max) { - // Found a higher option that isn't a new path. - maxScore = tempScore; - max = thisScore; - } - - answerCount++; - } - - path.update(id, maxScore); - doFindHighestFromId(questions, q.getNextQuestion(), scores, path); - } - - private static class FileQuestionProvider extends JsonEncodedProvider implements Provider { - private String mBaseDir; - - public FileQuestionProvider(String directory) { - super(Question.class); - mBaseDir = directory; - } - - @Override - public Question get(String key) throws IOException { - Path qfile = FileSystems.getDefault().getPath(mBaseDir, key + ".json"); - byte[] blob = Files.readAllBytes(qfile); - return decode(new String(blob)); - } - - @Override - public void put(String key, Question obj) throws IOException { - throw new UnsupportedOperationException("Not Implemented"); - } - } - - private static class AnswerPath { - String mPath; - String mScores; - Score mScore; - - public AnswerPath() { - mPath = null; - mScores = null; - mScore = new Score(); - } - - public AnswerPath(AnswerPath other) { - mPath = other.mPath; - mScores = other.mScores; - mScore = other.mScore; - } - - public void update(String questionId, Score newScore) { - String value; - - if (mScore.getCount() == newScore.getCount()) { - value = "n/a"; - - } else { - double delta = newScore.getSum() - mScore.getSum(); - if (delta < 0) { - value = "TRUMP"; - } else { - value = String.valueOf(delta); - } - } - - if (mPath == null) { - mPath = questionId; - mScores = value; - - } else { - mPath += ", " + questionId; - mScores += " + " + value; - } - - mScore = newScore; - } - } -} diff --git a/src/com/p4square/grow/tools/AttributeBackfillTool.java b/src/com/p4square/grow/tools/AttributeBackfillTool.java deleted file mode 100644 index d7fd2ff..0000000 --- a/src/com/p4square/grow/tools/AttributeBackfillTool.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.tools; - -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.data.Protocol; - -import com.p4square.f1oauth.Attribute; -import com.p4square.f1oauth.F1API; -import com.p4square.f1oauth.F1Access; -import com.p4square.f1oauth.F1Exception; -import com.p4square.restlet.oauth.OAuthUser; - -import com.p4square.grow.backend.dynamo.DynamoDatabase; -import com.p4square.grow.backend.dynamo.DynamoKey; - -import com.p4square.grow.config.Config; - -import com.p4square.grow.model.Chapter; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.VideoRecord; -import com.p4square.grow.provider.JsonEncodedProvider; - -/** - * This utility is used to backfill F1 Attributes from the GROW database into F1. - * - * This tool currently reads from Dynamo directly. It should probably access the - * backend or use the {@link com.p4square.grow.backend.GrowData} abstraction instead. - * - * @author Jesse Morgan - */ -public class AttributeBackfillTool { - - private static Config mConfig; - private static F1API mF1API; - private static DynamoDatabase mDatabase; - - public static void usage() { - System.out.println("java com.p4square.grow.tools.AttributeBackfillTool ...\n"); - System.out.println("Commands:"); - System.out.println("\t--domain Set config domain"); - System.out.println("\t--dev Set config domain to dev"); - System.out.println("\t--config Merge in config file"); - System.out.println("\t--assessments Backfill All Assessments"); - System.out.println("\t--training Backfill All Training Records"); - } - - public static void main(String... args) { - if (args.length == 0) { - usage(); - System.exit(1); - } - - mConfig = new Config(); - - try { - mConfig.updateConfig(AttributeTool.class.getResourceAsStream("/grow.properties")); - - int offset = 0; - while (offset < args.length) { - if ("--domain".equals(args[offset])) { - mConfig.setDomain(args[offset + 1]); - mF1API = null; - mDatabase = null; - offset += 2; - - } else if ("--dev".equals(args[offset])) { - mConfig.setDomain("dev"); - mF1API = null; - mDatabase = null; - offset += 1; - - } else if ("--config".equals(args[offset])) { - mConfig.updateConfig(args[offset + 1]); - mF1API = null; - mDatabase = null; - offset += 2; - - } else if ("--assessments".equals(args[offset])) { - offset = assessments(args, ++offset); - - } else if ("--training".equals(args[offset])) { - offset = training(args, ++offset); - - } else { - throw new IllegalArgumentException("Unknown command " + args[offset]); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(2); - } - } - - private static F1API getF1API() throws Exception { - if (mF1API == null) { - Context context = new Context(); - Client client = new Client(context, Arrays.asList(Protocol.HTTP, Protocol.HTTPS)); - context.setClientDispatcher(client); - - F1Access f1Access = new F1Access(context, - mConfig.getString("f1ConsumerKey"), - mConfig.getString("f1ConsumerSecret"), - mConfig.getString("f1BaseUrl"), - mConfig.getString("f1ChurchCode"), - F1Access.UserType.WEBLINK); - - // Gather Username and Password - String username = System.console().readLine("F1 Username: "); - char[] password = System.console().readPassword("F1 Password: "); - - OAuthUser user = f1Access.getAccessToken(username, new String(password)); - Arrays.fill(password, ' '); // Lost cause, but I'll still try. - - mF1API = f1Access.getAuthenticatedApi(user); - } - - return mF1API; - } - - private static DynamoDatabase getDatabase() { - if (mDatabase == null) { - mDatabase = new DynamoDatabase(mConfig); - } - - return mDatabase; - } - - private static int assessments(String[] args, int offset) throws Exception { - final F1API f1 = getF1API(); - final DynamoDatabase db = getDatabase(); - - DynamoKey key = DynamoKey.newKey("assessments", null); - - while (key != null) { - Map> rows = db.getAll(key); - - key = null; - - for (Map.Entry> row : rows.entrySet()) { - key = row.getKey(); - - String userId = key.getHashKey(); - - String summaryString = row.getValue().get("summary"); - if (summaryString == null || summaryString.length() == 0) { - System.out.printf("%s assessment incomplete\n", userId); - continue; - } - - try { - Map summary = JsonEncodedProvider.MAPPER.readValue(summaryString, Map.class); - - String result = (String) summary.get("result"); - if (result == null) { - System.out.printf("%s assessment incomplete\n", userId); - continue; - } - - String attributeName = "Assessment Complete - " + result; - - // Check if the user already has the attribute. - List attributes = f1.getAttribute(userId, attributeName); - - if (attributes.size() == 0) { - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(new Date()); - attribute.setComment(summaryString); - - if (f1.addAttribute(userId, attribute)) { - System.out.printf("%s attribute added\n", userId); - } else { - System.out.printf("%s failed to add attribute\n", userId); - } - } else { - System.out.printf("%s already has attribute\n", userId); - } - } catch (Exception e) { - System.out.printf("%s exception: %s\n", userId, e.getMessage()); - } - } - } - - return offset; - } - - private static int training(String[] args, int offset) throws Exception { - final F1API f1 = getF1API(); - final DynamoDatabase db = getDatabase(); - - DynamoKey key = DynamoKey.newKey("training", null); - - while (key != null) { - Map> rows = db.getAll(key); - - key = null; - - for (Map.Entry> row : rows.entrySet()) { - key = row.getKey(); - - String userId = key.getHashKey(); - - String valueString = row.getValue().get("value"); - if (valueString == null || valueString.length() == 0) { - System.out.printf("%s empty training record\n", userId); - continue; - } - - try { - TrainingRecord record = - JsonEncodedProvider.MAPPER.readValue(valueString, TrainingRecord.class); - Playlist playlist = record.getPlaylist(); - -chapters: - for (Map.Entry entry : playlist.getChaptersMap().entrySet()) { - Chapter chapter = entry.getValue(); - - // Find completion date - Date complete = new Date(0); - for (VideoRecord vr : chapter.getVideos().values()) { - if (!vr.getComplete()) { - continue chapters; - } - - Date recordCompletion = vr.getCompletionDate(); - if (recordCompletion != null && complete.before(recordCompletion)) { - complete = vr.getCompletionDate(); - } - } - - String attributeName = "Training Complete - " + entry.getKey(); - - // Check if the user already has the attribute. - List attributes = f1.getAttribute(userId, attributeName); - - if (attributes.size() == 0) { - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(complete); - - if (f1.addAttribute(userId, attribute)) { - System.out.printf("%s added %s\n", userId, attributeName); - } else { - System.out.printf("%s failed to add %s\n", userId, attributeName); - } - } else { - System.out.printf("%s already has %s\n", userId, attributeName); - } - } - - } catch (Exception e) { - System.out.printf("%s exception: %s\n", userId, e.getMessage()); - e.printStackTrace(); - } - } - } - - return offset; - } -} diff --git a/src/com/p4square/grow/tools/AttributeTool.java b/src/com/p4square/grow/tools/AttributeTool.java deleted file mode 100644 index 8e0540a..0000000 --- a/src/com/p4square/grow/tools/AttributeTool.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.grow.tools; - -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.data.Protocol; - -import com.p4square.grow.config.Config; -import com.p4square.f1oauth.Attribute; -import com.p4square.f1oauth.F1Access; -import com.p4square.f1oauth.F1API; -import com.p4square.f1oauth.F1Exception; -import com.p4square.restlet.oauth.OAuthUser; - -/** - * Tool for manipulating F1 Attributes. - * - * @author Jesse Morgan - */ -public class AttributeTool { - - private static Config mConfig; - private static F1API mF1API; - - public static void usage() { - System.out.println("java com.p4square.grow.tools.AttributeTool ...\n"); - System.out.println("Commands:"); - System.out.println("\t--domain Set config domain"); - System.out.println("\t--dev Set config domain to dev"); - System.out.println("\t--config Merge in config file"); - System.out.println("\t--list List all attributes"); - System.out.println("\t--assign Assign an attribute"); - System.out.println("\t--getall Get an attribute"); - System.out.println("\t--get Get an attribute"); - } - - public static void main(String... args) { - if (args.length == 0) { - usage(); - System.exit(1); - } - - mConfig = new Config(); - - try { - mConfig.updateConfig(AttributeTool.class.getResourceAsStream("/grow.properties")); - - int offset = 0; - while (offset < args.length) { - if ("--domain".equals(args[offset])) { - mConfig.setDomain(args[offset + 1]); - mF1API = null; - offset += 2; - - } else if ("--dev".equals(args[offset])) { - mConfig.setDomain("dev"); - mF1API = null; - offset += 1; - - } else if ("--config".equals(args[offset])) { - mConfig.updateConfig(args[offset + 1]); - mF1API = null; - offset += 2; - - } else if ("--list".equals(args[offset])) { - offset = list(args, ++offset); - - } else if ("--assign".equals(args[offset])) { - offset = assign(args, ++offset); - - } else if ("--getall".equals(args[offset])) { - offset = getall(args, ++offset); - - } else if ("--get".equals(args[offset])) { - offset = get(args, ++offset); - - } else { - throw new IllegalArgumentException("Unknown command " + args[offset]); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(2); - } - } - - private static F1API getF1API() throws Exception { - if (mF1API == null) { - Context context = new Context(); - Client client = new Client(context, Arrays.asList(Protocol.HTTP, Protocol.HTTPS)); - context.setClientDispatcher(client); - - F1Access f1Access = new F1Access(context, - mConfig.getString("f1ConsumerKey"), - mConfig.getString("f1ConsumerSecret"), - mConfig.getString("f1BaseUrl"), - mConfig.getString("f1ChurchCode"), - F1Access.UserType.WEBLINK); - - // Gather Username and Password - String username = System.console().readLine("F1 Username: "); - char[] password = System.console().readPassword("F1 Password: "); - - OAuthUser user = f1Access.getAccessToken(username, new String(password)); - Arrays.fill(password, ' '); // Lost cause, but I'll still try. - - mF1API = f1Access.getAuthenticatedApi(user); - } - - return mF1API; - } - - private static int list(String[] args, int offset) throws Exception { - final F1API f1 = getF1API(); - - final Map attributes = f1.getAttributeList(); - System.out.printf("%7s %s\n", "ID", "Name"); - for (Map.Entry entry : attributes.entrySet()) { - System.out.printf("%7s %s\n", entry.getValue(), entry.getKey()); - } - - return offset; - } - - private static int assign(String[] args, int offset) throws Exception { - final String userId = args[offset++]; - final String attributeName = args[offset++]; - final String comment = args[offset++]; - - final F1API f1 = getF1API(); - - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(new Date()); - attribute.setComment(comment); - - if (f1.addAttribute(userId, attribute)) { - System.out.println("Added attribute " + attributeName + " for " + userId); - } else { - System.out.println("Failed to add attribute " + attributeName + " for " + userId); - } - - return offset; - } - - private static int getall(String[] args, int offset) throws Exception { - final String userId = args[offset++]; - - doGet(userId, null); - - return offset; - } - - private static int get(String[] args, int offset) throws Exception { - final String userId = args[offset++]; - final String attributeName = args[offset++]; - - doGet(userId, attributeName); - - return offset; - } - - private static void doGet(final String userId, final String attributeName) throws Exception { - final F1API f1 = getF1API(); - - List attributes = f1.getAttribute(userId, attributeName); - for (Attribute attribute : attributes) { - System.out.printf("%s %s %s %s %s\n%s\n\n", - userId, - attribute.getAttributeName(), - attribute.getId(), - attribute.getStartDate(), - attribute.getEndDate(), - attribute.getComment()); - } - } -} diff --git a/src/com/p4square/restlet/metrics/MetricRouter.java b/src/com/p4square/restlet/metrics/MetricRouter.java deleted file mode 100644 index d4da270..0000000 --- a/src/com/p4square/restlet/metrics/MetricRouter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.restlet.metrics; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -import org.restlet.routing.TemplateRoute; -import org.restlet.routing.Router; - -/** - * - * @author Jesse Morgan - */ -public class MetricRouter extends Router { - - private final MetricRegistry mMetricRegistry; - - public MetricRouter(Context context, MetricRegistry metrics) { - super(context); - mMetricRegistry = metrics; - } - - @Override - protected void doHandle(Restlet next, Request request, Response response) { - String baseName; - if (next instanceof TemplateRoute) { - TemplateRoute temp = (TemplateRoute) next; - baseName = MetricRegistry.name("MetricRouter", temp.getTemplate().getPattern()); - } else { - baseName = MetricRegistry.name("MetricRouter", "unknown"); - } - - final Timer.Context aggTimer = mMetricRegistry.timer("MetricRouter.time").time(); - final Timer.Context timer = mMetricRegistry.timer(baseName + ".time").time(); - - try { - super.doHandle(next, request, response); - } finally { - timer.stop(); - aggTimer.stop(); - - // Record status code - boolean success = !response.getStatus().isError(); - if (success) { - mMetricRegistry.counter("MetricRouter.success").inc(); - mMetricRegistry.counter(baseName + ".response.success").inc(); - } else { - mMetricRegistry.counter("MetricRouter.failure").inc(); - mMetricRegistry.counter(baseName + ".response.failure").inc(); - } - } - } -} diff --git a/src/com/p4square/restlet/metrics/MetricsApplication.java b/src/com/p4square/restlet/metrics/MetricsApplication.java deleted file mode 100644 index 6caf742..0000000 --- a/src/com/p4square/restlet/metrics/MetricsApplication.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.restlet.metrics; - -import java.util.concurrent.TimeUnit; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.json.MetricsModule; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.restlet.Application; -import org.restlet.Restlet; -import org.restlet.resource.Finder; - -/** - * - * @author Jesse Morgan - */ -public class MetricsApplication extends Application { - static final ObjectMapper MAPPER; - static { - MAPPER = new ObjectMapper(); - MAPPER.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, true)); - } - - private final MetricRegistry mMetricRegistry; - - public MetricsApplication(MetricRegistry metrics) { - mMetricRegistry = metrics; - } - - public MetricRegistry getMetricRegistry() { - return mMetricRegistry; - } - - @Override - public Restlet createInboundRoot() { - return new Finder(getContext(), MetricsResource.class); - } -} diff --git a/src/com/p4square/restlet/metrics/MetricsResource.java b/src/com/p4square/restlet/metrics/MetricsResource.java deleted file mode 100644 index e2ab14d..0000000 --- a/src/com/p4square/restlet/metrics/MetricsResource.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2014 Jesse Morgan - */ - -package com.p4square.restlet.metrics; - -import com.codahale.metrics.MetricRegistry; - -import org.restlet.ext.jackson.JacksonRepresentation; -import org.restlet.representation.Representation; -import org.restlet.resource.ServerResource; - -/** - * - * @author Jesse Morgan - */ -public class MetricsResource extends ServerResource { - - private MetricRegistry mMetricRegistry; - - @Override - public void doInit() { - mMetricRegistry = ((MetricsApplication) getApplication()).getMetricRegistry(); - } - - @Override - protected Representation get() { - JacksonRepresentation rep = new JacksonRepresentation<>(mMetricRegistry); - rep.setObjectMapper(MetricsApplication.MAPPER); - return rep; - } -} diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticator.java b/src/com/p4square/restlet/oauth/OAuthAuthenticator.java deleted file mode 100644 index c33bb5a..0000000 --- a/src/com/p4square/restlet/oauth/OAuthAuthenticator.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import org.apache.log4j.Logger; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; -import org.restlet.security.User; - -/** - * Authenticator which makes an OAuth request to authenticate the user. - * - * If this Authenticator is made optional than no requests are made to the - * service provider. - * - * @author Jesse Morgan - */ -public class OAuthAuthenticator extends Authenticator { - private static Logger LOG = Logger.getLogger(OAuthAuthenticator.class); - - private static final String OAUTH_TOKEN = "oauth_token"; - private static final String COOKIE_NAME = "oauth_secret"; - - private final OAuthHelper mHelper; - - /** - * Create a new Authenticator. - * - * @param Context the current context. - * @param optional If true, unauthenticated users are allowed to continue. - * @param helper The OAuthHelper which will help with the requests. - */ - public OAuthAuthenticator(Context context, boolean optional, OAuthHelper helper) { - super(context, false, optional, null); - - mHelper = helper; - } - - protected boolean authenticate(Request request, Response response) { - /* - * The authentication workflow has three steps: - * 1. Get RequestToken - * 2. Authenticate the user - * 3. Get AccessToken - * - * The authentication workflow is broken into two stages. In the first, - * we generate the RequestToken (step 1) and redirect the user to the - * authentication page. When the user comes back, we will request the - * AccessToken (step 2). - * - * We determine which half we are in by the presence of the oauth_token - * parameter in the query string. - */ - - final String token = request.getResourceRef().getQueryAsForm().getFirstValue(OAUTH_TOKEN); - final String secret = request.getCookies().getFirstValue(COOKIE_NAME); - - try { - if (token == null) { - if (isOptional()) { - return false; - } - - // 1. Get RequestToken - Token requestToken = mHelper.getRequestToken(); - - if (requestToken == null) { - return false; - } - - // 2. Redirect user - // TODO Encrypt cookie - response.getCookieSettings().add(COOKIE_NAME, requestToken.getSecret()); - response.redirectSeeOther(mHelper.getLoginUrl(requestToken, request.getResourceRef().toString())); - return false; - - } else { - // 3. Get AccessToken - Token requestToken = new Token(token, secret); - User user = mHelper.getAccessToken(requestToken); - request.getClientInfo().setUser(user); - return true; - } - - } catch (OAuthException e) { - LOG.debug("Authentication failed: " + e); - return false; - } - } -} diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java b/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java deleted file mode 100644 index 76ff044..0000000 --- a/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; - -import java.net.URLEncoder; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import java.util.Collections; -import java.util.Random; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.data.ChallengeRequest; -import org.restlet.data.ChallengeResponse; -import org.restlet.data.ChallengeScheme; -import org.restlet.data.CharacterSet; -import org.restlet.data.Form; -import org.restlet.data.Method; -import org.restlet.data.Parameter; -import org.restlet.data.Reference; -import org.restlet.engine.header.ChallengeWriter; -import org.restlet.engine.header.Header; -import org.restlet.engine.security.AuthenticatorHelper; -import org.restlet.engine.util.Base64; -import org.restlet.util.Series; - -/** - * Authentication helper for signing OAuth Requests. - * - * This implementation is limited to one consumer token/secret per restlet - * engine. In practice this means you will only be able to interact with one - * service provider unless you loaded/unloaded the AuthenticationHelper for - * each request. - * - * @author Jesse Morgan - */ -public class OAuthAuthenticatorHelper extends AuthenticatorHelper { - private static final String SIGNATURE_METHOD = "HMAC-SHA1"; - private static final String JAVA_SIGNATURE_METHOD = "HmacSHA1"; - private static final String ENCODING = "UTF-8"; - - private final Random mRandom; - private final Token mConsumerToken; - - /** - * Package-private constructor. - * - * This class should only be instantiated by OAuthHelper. - */ - OAuthAuthenticatorHelper(Token consumerToken) { - super(ChallengeScheme.HTTP_OAUTH, true, false); - - mRandom = new Random(); - mConsumerToken = consumerToken; - } - - @Override - public void formatRequest(ChallengeWriter cw, ChallengeRequest cr, - Response response, Series
httpHeaders) throws IOException { - - throw new UnsupportedOperationException("OAuth Requests are not implemented"); - } - - @Override - public void formatResponse(ChallengeWriter cw, ChallengeResponse response, - Request request, Series
httpHeaders) { - - try { - Series authParams = new Series(Parameter.class); - - String nonce = String.valueOf(mRandom.nextInt()); - String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - - authParams.add(new Parameter("oauth_consumer_key", mConsumerToken.getToken())); - authParams.add(new Parameter("oauth_nonce", nonce)); - authParams.add(new Parameter("oauth_signature_method", SIGNATURE_METHOD)); - authParams.add(new Parameter("oauth_timestamp", timestamp)); - authParams.add(new Parameter("oauth_version", "1.0")); - - String accessToken = response.getIdentifier(); - if (accessToken != null) { - authParams.add(new Parameter("oauth_token", accessToken)); - } - - // Generate Signature - String signature = generateSignature(response, request, authParams); - authParams.add(new Parameter("oauth_signature", signature)); - - // Write Header - for (Parameter p : authParams) { - cw.appendQuotedChallengeParameter(encode(p.getName()), encode(p.getValue())); - } - - } catch (IOException e) { - throw new RuntimeException(e); - - } catch (InvalidKeyException e) { - throw new RuntimeException(e); - - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - /** - * Helper method to generate an OAuth Signature. - */ - private String generateSignature(ChallengeResponse response, Request request, - Series authParams) - throws NoSuchAlgorithmException, InvalidKeyException, IOException, - UnsupportedEncodingException { - - // HTTP Request Method - String httpMethod = request.getMethod().getName(); - - // Request Url - Reference url = request.getResourceRef(); - String requestUrl = encode(url.getScheme() + ":" + url.getHierarchicalPart()); - - // Normalized parameters - Series params = new Series(Parameter.class); - - // OAUTH Params - params.addAll(authParams); - - // Query Params - Form query = url.getQueryAsForm(); - params.addAll(query); - - // Sort it - Collections.sort(params); - - StringBuilder normalizedParamsBuilder = new StringBuilder(); - for (Parameter p : params) { - normalizedParamsBuilder.append('&'); - normalizedParamsBuilder.append(p.encode(CharacterSet.UTF_8)); - } - String normalizedParams = encode(normalizedParamsBuilder.substring(1)); // remove the first & - - // Generate signature base - String sigBase = httpMethod + "&" + requestUrl + "&" + normalizedParams.toString(); - - // Sign the signature base - Mac mac = Mac.getInstance(JAVA_SIGNATURE_METHOD); - - String accessTokenSecret = ""; - if (response.getIdentifier() != null) { - accessTokenSecret = new String(response.getSecret()); - } - - byte[] keyBytes = (encode(mConsumerToken.getSecret()) + "&" + encode(accessTokenSecret)).getBytes(ENCODING); - SecretKey key = new SecretKeySpec(keyBytes, JAVA_SIGNATURE_METHOD); - mac.init(key); - - byte[] signature = mac.doFinal(sigBase.getBytes(ENCODING)); - - return Base64.encode(signature, false).trim(); - } - - /** - * Helper method to URL Encode Strings. - */ - private String encode(String input) throws UnsupportedEncodingException { - return URLEncoder.encode(input, ENCODING); - } -} diff --git a/src/com/p4square/restlet/oauth/OAuthException.java b/src/com/p4square/restlet/oauth/OAuthException.java deleted file mode 100644 index dd326d3..0000000 --- a/src/com/p4square/restlet/oauth/OAuthException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import org.restlet.data.Status; - -/** - * Exception throw when the service provider returns an error. - * - * @author Jesse Morgan - */ -public class OAuthException extends Exception { - private final Status mStatus; - - public OAuthException(Status status) { - super("Service provider failed request: " + status.getDescription()); - mStatus = status; - } - - public Status getStatus() { - return mStatus; - } -} diff --git a/src/com/p4square/restlet/oauth/OAuthHelper.java b/src/com/p4square/restlet/oauth/OAuthHelper.java deleted file mode 100644 index 67dd238..0000000 --- a/src/com/p4square/restlet/oauth/OAuthHelper.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import java.net.URLEncoder; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -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.data.Status; -import org.restlet.engine.Engine; -import org.restlet.representation.Representation; - -/** - * Helper Class for OAuth 1.0 Authentication. - * - * @author Jesse Morgan - */ -public abstract class OAuthHelper { - private final Restlet mDispatcher; - private final Token mConsumerToken; - - /** - * Create a new OAuth Helper. - * As currently implemented, there can only be one OAuthHelper per Restlet - * Engine since this class registers its own provider for the OAuth - * authentication protocol. - * - * FIXME: This could be improved by making OAuthAuthenticationHelper and - * maybe Token aware of multiple service providers. - * - * @param context The restlet context which provides a ClientDispatcher. - * @param consumerKey The OAuth consumer key for this application. - * @param consumerSecret the OAuth consumer secret for this application. - */ - public OAuthHelper(Context context, String consumerKey, String consumerSecret) { - mDispatcher = context.getClientDispatcher(); - mConsumerToken = new Token(consumerKey, consumerSecret); - - Engine.getInstance().getRegisteredAuthenticators().add(new OAuthAuthenticatorHelper(mConsumerToken)); - } - - /** - * @return the URL for the initial RequestToken request. - */ - protected abstract String getRequestTokenUrl(); - - /** - * Request a RequestToken. - * - * @return a Token containing the RequestToken. - * @throws OAuthException if the request fails. - */ - public Token getRequestToken() throws OAuthException { - Request request = new Request(Method.GET, getRequestTokenUrl()); - request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); - - Response response = mDispatcher.handle(request); - - return processTokenRequest(response); - } - - /** - * @return the URL to redirect the user to for Authentication. - */ - public abstract String getLoginUrl(Token requestToken, String callback); - - /** - * @return the URL for the AccessToken request. - */ - protected abstract String getAccessTokenUrl(); - - /** - * Request an AccessToken for a previously authenticated RequestToken. - * - * @return an OAuthUser object containing the AccessToken. - * @throws OAuthException if the request fails. - */ - public OAuthUser getAccessToken(Token requestToken) throws OAuthException { - Request request = new Request(Method.GET, getAccessTokenUrl()); - request.setChallengeResponse(requestToken.getChallengeResponse()); - - return processAccessTokenRequest(request); - } - - /** - * Helper method to decode the token returned from an OAuth Request. - * - * @param response The Response object from the Request. - * @return the Token from the oauth_token and oauth_token_secret parameters. - * @throws OAuthException is the server reported an error. - */ - protected Token processTokenRequest(Response response) throws OAuthException { - Status status = response.getStatus(); - Representation entity = response.getEntity(); - - try { - if (status.isSuccess()) { - Form form = new Form(entity); - String token = form.getFirstValue("oauth_token"); - String secret = form.getFirstValue("oauth_token_secret"); - - return new Token(token, secret); - - } else { - throw new OAuthException(status); - } - } finally { - entity.release(); - } - } - - /** - * Helper method to create an OAuthUser from the AccessToken request. - * - * The User's identifier is set to the Content-Location header, if present. - * - * @param response The Response to the AccessToken Request. - * @return An OAuthUser object wrapping the AccessToken. - * @throws OAuthException if the request failed. - */ - public OAuthUser processAccessTokenRequest(Request request) throws OAuthException { - Response response = getResponse(request); - Token accessToken = processTokenRequest(response); - - Reference ref = response.getEntity().getLocationRef(); - if (ref != null) { - return new OAuthUser(ref.toString(), accessToken); - - } else { - return new OAuthUser(accessToken); - } - } - - /** - * Helper method to get a Response for a Request. - */ - public Response getResponse(Request request) { - return mDispatcher.handle(request); - } -} diff --git a/src/com/p4square/restlet/oauth/OAuthUser.java b/src/com/p4square/restlet/oauth/OAuthUser.java deleted file mode 100644 index 11dbac1..0000000 --- a/src/com/p4square/restlet/oauth/OAuthUser.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import org.restlet.data.ChallengeResponse; -import org.restlet.security.User; - -/** - * Simple User object which also contains an OAuth AccessToken. - * - * @author Jesse Morgan - */ -public class OAuthUser extends User { - private final Token mToken; - private final String mContentLocation; - - public OAuthUser(Token token) { - this(null, token); - } - - public OAuthUser(String location, Token token) { - super(); - mToken = token; - mContentLocation = location; - } - - /** - * @return the Location associated with the user. - */ - public String getLocation() { - return mContentLocation; - } - - /** - * @return The AccessToken. - */ - public Token getToken() { - return mToken; - } - - /** - * Convenience method for getToken().getChallengeResponse(). - * @return A ChallengeResponse based upon the access token. - */ - public ChallengeResponse getChallengeResponse() { - return mToken.getChallengeResponse(); - } -} diff --git a/src/com/p4square/restlet/oauth/Token.java b/src/com/p4square/restlet/oauth/Token.java deleted file mode 100644 index 51a9087..0000000 --- a/src/com/p4square/restlet/oauth/Token.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.restlet.oauth; - -import org.restlet.data.ChallengeResponse; -import org.restlet.data.ChallengeScheme; - -/** - * Token wraps the two Strings which make up an OAuth Token: the public - * component and the private component. - * - * @author Jesse Morgan - */ -public class Token { - private final String mToken; - private final String mSecret; - - public Token(String token, String secret) { - mToken = token; - mSecret = secret; - } - - /** - * @return the public component. - */ - public String getToken() { - return mToken; - } - - /** - * @return the secret component. - */ - public String getSecret() { - return mSecret; - } - - @Override - public String toString() { - return mToken + "&" + mSecret; - } - - /** - * Generate a ChallengeResponse based on this Token. - * - * @return a ChallengeResponse object using the OAUTH ChallengeScheme. - */ - public ChallengeResponse getChallengeResponse() { - return new ChallengeResponse(ChallengeScheme.HTTP_OAUTH, mToken, mSecret); - } -} diff --git a/src/com/p4square/session/Session.java b/src/com/p4square/session/Session.java deleted file mode 100644 index 1bb65f5..0000000 --- a/src/com/p4square/session/Session.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import org.restlet.security.User; - -/** - * - * @author Jesse Morgan - */ -public class Session { - static final long LIFETIME = 86400000; - - private final String mSessionId; - private final User mUser; - private final Map mData; - private long mExpires; - - Session(User user) { - mUser = user; - mSessionId = UUID.randomUUID().toString(); - mExpires = System.currentTimeMillis() + LIFETIME; - mData = new HashMap(); - } - - void touch() { - mExpires = System.currentTimeMillis() + LIFETIME; - } - - boolean isExpired() { - return System.currentTimeMillis() > mExpires; - } - - public String getId() { - return mSessionId; - } - - public Object get(String key) { - return mData.get(key); - } - - public void put(String key, String value) { - mData.put(key, value); - } - - public User getUser() { - return mUser; - } - - public Map getMap() { - return mData; - } -} diff --git a/src/com/p4square/session/SessionAuthenticator.java b/src/com/p4square/session/SessionAuthenticator.java deleted file mode 100644 index 794e1a8..0000000 --- a/src/com/p4square/session/SessionAuthenticator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; -import org.restlet.security.User; - -/** - * - * @author Jesse Morgan - */ -public class SessionAuthenticator /*extends Authenticator*/ { - /* - @Override - protected boolean authenticate(Request request, Response response) { - // Check for authentication cookie - final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); - if (cookie != null) { - cLog.debug("Got cookie: " + cookie); - // TODO Decrypt user info - User user = new User(cookie); - request.getClientInfo().setUser(user); - return true; - } - - // Challenge the user if not authenticated - response.redirectSeeOther(mLoginPage); - return false; - } - */ -} diff --git a/src/com/p4square/session/SessionCheckingAuthenticator.java b/src/com/p4square/session/SessionCheckingAuthenticator.java deleted file mode 100644 index 489d6a0..0000000 --- a/src/com/p4square/session/SessionCheckingAuthenticator.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import org.apache.log4j.Logger; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; - -/** - * Authenticator which succeeds if a valid Session exists. - * - * @author Jesse Morgan - */ -public class SessionCheckingAuthenticator extends Authenticator { - private static final Logger LOG = Logger.getLogger(SessionCheckingAuthenticator.class); - - public SessionCheckingAuthenticator(Context context, boolean optional) { - super(context, optional); - } - - protected boolean authenticate(Request request, Response response) { - Session s = Sessions.getInstance().get(request); - - if (s != null) { - LOG.debug("Found session for user " + s.getUser()); - request.getClientInfo().setUser(s.getUser()); - return true; - - } else { - return false; - } - } - -} diff --git a/src/com/p4square/session/SessionCookieAuthenticator.java b/src/com/p4square/session/SessionCookieAuthenticator.java deleted file mode 100644 index 0074b77..0000000 --- a/src/com/p4square/session/SessionCookieAuthenticator.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import org.apache.log4j.Logger; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; - -/** - * - * @author Jesse Morgan - */ -public class SessionCookieAuthenticator extends Authenticator { - private static final Logger LOG = Logger.getLogger(SessionCookieAuthenticator.class); - - private static final String COOKIE_NAME = "S"; - - private final Sessions mSessions; - - public SessionCookieAuthenticator(Context context, boolean optional, Sessions sessions) { - super(context, optional); - - mSessions = sessions; - } - - protected boolean authenticate(Request request, Response response) { - final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); - - if (request.getClientInfo().isAuthenticated()) { - // Request is already authenticated... create session if it doesn't exist. - if (cookie == null) { - Session s = mSessions.create(request.getClientInfo().getUser()); - response.getCookieSettings().add(COOKIE_NAME, s.getId()); - } - - return true; - - } else { - // Check for authentication cookie - if (cookie != null) { - LOG.debug("Got cookie: " + cookie); - - Session s = mSessions.get(cookie); - if (s != null) { - request.getClientInfo().setUser(s.getUser()); - return true; - } - } - - return false; - } - } - -} diff --git a/src/com/p4square/session/SessionCreatingAuthenticator.java b/src/com/p4square/session/SessionCreatingAuthenticator.java deleted file mode 100644 index 3ec14b4..0000000 --- a/src/com/p4square/session/SessionCreatingAuthenticator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import org.apache.log4j.Logger; - -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.security.Authenticator; -import org.restlet.security.User; - -/** - * Authenticator which creates a Session for the request and adds a cookie - * to the response. - * - * The Request MUST be Authenticated and MUST have a User object associated. - * - * @author Jesse Morgan - */ -public class SessionCreatingAuthenticator extends Authenticator { - private static final Logger LOG = Logger.getLogger(SessionCreatingAuthenticator.class); - - public SessionCreatingAuthenticator(Context context) { - super(context, true); - } - - protected boolean authenticate(Request request, Response response) { - if (Sessions.getInstance().get(request) != null) { - return true; - } - - User user = request.getClientInfo().getUser(); - - if (request.getClientInfo().isAuthenticated() && user != null) { - Sessions.getInstance().create(request, response); - LOG.debug(response); - return true; - } - - return false; - } - -} diff --git a/src/com/p4square/session/Sessions.java b/src/com/p4square/session/Sessions.java deleted file mode 100644 index 9f9dda0..0000000 --- a/src/com/p4square/session/Sessions.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.session; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; - -import org.restlet.Response; -import org.restlet.Request; -import org.restlet.data.CookieSetting; -import org.restlet.security.User; - -/** - * Singleton Session Manager. - * - * @author Jesse Morgan - */ -public class Sessions { - private static final String COOKIE_NAME = "S"; - private static final int DELETE = 0; - - private static final Sessions THE = new Sessions(); - public static Sessions getInstance() { - return THE; - } - - private final Map mSessions; - private final Timer mCleanupTimer; - - private Sessions() { - mSessions = new ConcurrentHashMap(); - - mCleanupTimer = new Timer("sessionCleaner", true); - mCleanupTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - for (Session s : mSessions.values()) { - if (s.isExpired()) { - mSessions.remove(s.getId()); - } - } - } - }, Session.LIFETIME, Session.LIFETIME); - } - - /** - * Get a session by ID. - * - * @param sessionid - * The Session id - * @return The Session if found and not expired, null otherwise. - */ - public Session get(String sessionid) { - Session s = mSessions.get(sessionid); - - if (s != null && !s.isExpired()) { - s.touch(); - return s; - } - - return null; - } - - /** - * Get the Session associated with the Request. - * - * @param request - * The request to fetch a session for. - * @return A session or null if no session is found. - */ - public Session get(Request request) { - final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); - - if (cookie != null) { - return get(cookie); - } - - return null; - } - - /** - * Create a new Session for the given User object. - * - * @param user - * The User to associate with the Session. - * @return The new Session object. - */ - public Session create(User user) { - if (user == null) { - throw new IllegalArgumentException("Can not create session for null user."); - } - - Session s = new Session(user); - mSessions.put(s.getId(), s); - - return s; - } - - /** - * Delete a Session. - * - * @param sessionid - * The id of the Session to remove. - */ - public void delete(String sessionid) { - mSessions.remove(sessionid); - } - - /** - * Create a new Session and add the Session cookie to the response. - * - * @param request - * The request to create the Session for. - * @param response - * The response to add the session cookie to. - * @return The new Session. - */ - public Session create(Request request, Response response) { - Session s = create(request.getClientInfo().getUser()); - - CookieSetting cookie = new CookieSetting(COOKIE_NAME, s.getId()); - cookie.setPath("/"); - - request.getCookies().add(cookie); - response.getCookieSettings().add(cookie); - - return s; - } - - /** - * Remove a Session and delete the cookies. - * - * @param request - * The request with the session cookie to remove - * @param response - * The response to remove the session cookie from. - */ - public void delete(Request request, Response response) { - final String sessionid = request.getCookies().getFirstValue(COOKIE_NAME); - - delete(sessionid); - - CookieSetting cookie = new CookieSetting(COOKIE_NAME, ""); - cookie.setPath("/"); - cookie.setMaxAge(DELETE); - - request.getCookies().add(cookie); - response.getCookieSettings().add(cookie); - } - -} diff --git a/src/grow.properties b/src/grow.properties deleted file mode 100644 index 53075fe..0000000 --- a/src/grow.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Frontend Settings -*.backendUri = riap://component/backend -*.staticRoot = -*.dynamicRoot = - -prod.postAccountCreationPage = http://foursquaregrow.com/login.html - -# Backend Settings -prod.clusterName = Prod Cluster -dev.clusterName = Dev Cluster - -*.awsRegion = us-west-2 -prod.dynamoTablePrefix = grow-prod- -serverprod.dynamoTablePrefix = grow-prod- -dev.dynamoTablePrefix = grow-dev- diff --git a/src/jetty-logging.properties b/src/jetty-logging.properties deleted file mode 100644 index c291cb8..0000000 --- a/src/jetty-logging.properties +++ /dev/null @@ -1 +0,0 @@ -org.eclipse.jetty.LEVEL=INFO diff --git a/src/log4j.properties b/src/log4j.properties deleted file mode 100644 index c1e2ecb..0000000 --- a/src/log4j.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Set root logger level to DEBUG and its only appender to A1. -log4j.rootLogger=DEBUG, stdout, logfile - -log4j.loggger.org.eclipse = WARN - -# stdout appender -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n - -# service.log appender -log4j.appender.logfile=org.apache.log4j.FileAppender -log4j.appender.logfile.File=service.log -log4j.appender.logfile.layout=org.apache.log4j.PatternLayout -log4j.appender.logfile.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/src/main/java/com/p4square/f1oauth/Attribute.java b/src/main/java/com/p4square/f1oauth/Attribute.java new file mode 100644 index 0000000..64f2507 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/Attribute.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import java.util.Date; + +/** + * F1 Attribute Data. + * + * @author Jesse Morgan + */ +public class Attribute { + private final String mAttributeName; + private String mId; + private Date mStartDate; + private Date mEndDate; + private String mComment; + + /** + * @param name The attribute name. + */ + public Attribute(final String name) { + mAttributeName = name; + } + + /** + * @return the Attribute name. + */ + public String getAttributeName() { + return mAttributeName; + } + + /** + * @return the id of this specific attribute instance. + */ + public String getId() { + return mId; + } + + /** + * Set the attribute id to id. + */ + public void setId(final String id) { + mId = id; + } + + /** + * @return the start date for the attribute. + */ + public Date getStartDate() { + return mStartDate; + } + + /** + * Set the start date for the attribute. + */ + public void setStartDate(final Date date) { + mStartDate = date; + } + + /** + * @return the end date for the attribute. + */ + public Date getEndDate() { + return mEndDate; + } + + /** + * Set the end date for the attribute. + */ + public void setEndDate(final Date date) { + mEndDate = date; + } + + /** + * @return The comment on the Attribute. + */ + public String getComment() { + return mComment; + } + + /** + * Set the comment on the attribute. + */ + public void setComment(final String comment) { + mComment = comment; + } +} diff --git a/src/main/java/com/p4square/f1oauth/F1API.java b/src/main/java/com/p4square/f1oauth/F1API.java new file mode 100644 index 0000000..a525c3f --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/F1API.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.p4square.restlet.oauth.OAuthException; +import com.p4square.restlet.oauth.OAuthUser; + +/** + * F1 API methods which require an authenticated user. + * + * @author Jesse Morgan + */ +public interface F1API { + /** + * Fetch information about a user. + * + * @param user The user to fetch information about. + * @return An F1User object. + */ + F1User getF1User(OAuthUser user) throws OAuthException, IOException; + + /** + * Fetch a list of all attributes ids and names. + * + * @return A Map of attribute name to attribute id. + */ + Map getAttributeList() throws F1Exception; + + /** + * Add an attribute to the user. + * + * @param user The user to add the attribute to. + * @param attributeName The attribute to add. + * @param attribute The attribute to add. + */ + boolean addAttribute(String userId, Attribute attribute) throws F1Exception; + + /** + * Return attributes assigned to user. + * + * A user may be assigned multiple attributes with the same name, thus even if + * attributeName is specified, multiple attributes may be returned. + * + * @param userId The user to query. + * @param attributeName A specific attribute to return, null for all. + * @return A list of Attributes + */ + List getAttribute(String userId, String attributeName) throws F1Exception; + +} diff --git a/src/main/java/com/p4square/f1oauth/F1Access.java b/src/main/java/com/p4square/f1oauth/F1Access.java new file mode 100644 index 0000000..c3307f1 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/F1Access.java @@ -0,0 +1,594 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import java.io.IOException; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; + +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.MediaType; +import org.restlet.data.Method; +import org.restlet.data.Status; +import org.restlet.engine.util.Base64; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import com.p4square.restlet.oauth.OAuthException; +import com.p4square.restlet.oauth.OAuthHelper; +import com.p4square.restlet.oauth.OAuthUser; +import com.p4square.restlet.oauth.Token; + +/** + * F1 API Access. + * + * @author Jesse Morgan + */ +public class F1Access { + public enum UserType { + WEBLINK, PORTAL; + } + + private static final Logger LOG = Logger.getLogger(F1Access.class); + + private static final String VERSION_STRING = "/v1/"; + private static final String REQUESTTOKEN_URL = "Tokens/RequestToken"; + private static final String AUTHORIZATION_URL = "Login"; + private static final String ACCESSTOKEN_URL= "Tokens/AccessToken"; + private static final String TRUSTED_ACCESSTOKEN_URL = "/AccessToken"; + + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + private final String mBaseUrl; + private final String mMethod; + + private final OAuthHelper mOAuthHelper; + + private final Map mAttributeIdByName; + + private MetricRegistry mMetricRegistry; + + /** + */ + public F1Access(Context context, String consumerKey, String consumerSecret, + String baseUrl, String churchCode, UserType userType) { + + switch (userType) { + case WEBLINK: + mMethod = "WeblinkUser"; + break; + case PORTAL: + mMethod = "PortalUser"; + break; + default: + throw new IllegalArgumentException("Unknown UserType"); + } + + mBaseUrl = "https://" + churchCode + "." + baseUrl + VERSION_STRING; + + // Create the OAuthHelper. This implicitly registers the helper to + // handle outgoing requests which need OAuth authentication. + mOAuthHelper = new OAuthHelper(context, consumerKey, consumerSecret) { + @Override + protected String getRequestTokenUrl() { + return mBaseUrl + REQUESTTOKEN_URL; + } + + @Override + public String getLoginUrl(Token requestToken, String callback) { + String loginUrl = mBaseUrl + mMethod + AUTHORIZATION_URL + + "?oauth_token=" + URLEncoder.encode(requestToken.getToken()); + + if (callback != null) { + loginUrl += "&oauth_callback=" + URLEncoder.encode(callback); + } + + return loginUrl; + } + + @Override + protected String getAccessTokenUrl() { + return mBaseUrl + ACCESSTOKEN_URL; + } + }; + + mAttributeIdByName = new HashMap<>(); + } + + /** + * Set the MetricRegistry to get metrics recorded. + */ + public void setMetricRegistry(MetricRegistry metrics) { + mMetricRegistry = metrics; + } + + /** + * Request an AccessToken for a particular username and password. + * + * This is an F1 extension to OAuth: + * http://developer.fellowshipone.com/docs/v1/Util/AuthDocs.help#2creds + */ + public OAuthUser getAccessToken(String username, String password) throws OAuthException { + Timer.Context timer = getTimer("F1Access.getAccessToken.time"); + boolean success = true; + + try { + Request request = new Request(Method.POST, + mBaseUrl + mMethod + TRUSTED_ACCESSTOKEN_URL); + request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); + + String base64String = Base64.encode((username + " " + password).getBytes(), false); + request.setEntity(new StringRepresentation(base64String)); + + return mOAuthHelper.processAccessTokenRequest(request); + + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.getAccessToken.success"); + } else { + incrementCounter("F1Access.getAccessToken.failure"); + } + } + } + + /** + * Create a new Account. + * + * @param firstname The user's first name. + * @param lastname The user's last name. + * @param email The user's email address. + * @param redirect The URL to send the user to after confirming his address. + * + * @return true if created, false if the account already exists. + */ + public boolean createAccount(String firstname, String lastname, String email, String redirect) + throws OAuthException { + Timer.Context timer = getTimer("F1Access.createAccount.time"); + boolean success = true; + + try { + String req = String.format("{\n\"account\":{\n\"firstName\":\"%s\",\n" + + "\"lastName\":\"%s\",\n\"email\":\"%s\",\n" + + "\"urlRedirect\":\"%s\"\n}\n}", + firstname, lastname, email, redirect); + + Request request = new Request(Method.POST, mBaseUrl + "Accounts"); + request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); + request.setEntity(new StringRepresentation(req, MediaType.APPLICATION_JSON)); + + Response response = mOAuthHelper.getResponse(request); + + Status status = response.getStatus(); + if (Status.SUCCESS_NO_CONTENT.equals(status)) { + return true; + + } else if (Status.CLIENT_ERROR_CONFLICT.equals(status)) { + return false; + + } else { + throw new OAuthException(status); + } + + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.createAccount.success"); + } else { + incrementCounter("F1Access.createAccount.failure"); + } + } + } + + /** + * @return An F1API authenticated by the given user. + */ + public F1API getAuthenticatedApi(OAuthUser user) { + return new AuthenticatedApi(user); + } + + private class AuthenticatedApi implements F1API { + private final OAuthUser mUser; + + public AuthenticatedApi(OAuthUser user) { + mUser = user; + } + + /** + * Fetch information about a user. + * + * @param user The user to fetch information about. + * @return An F1User object. + */ + @Override + public F1User getF1User(OAuthUser user) throws OAuthException, IOException { + Timer.Context timer = getTimer("F1Access.getF1User.time"); + boolean success = true; + + try { + Request request = new Request(Method.GET, user.getLocation() + ".json"); + request.setChallengeResponse(mUser.getChallengeResponse()); + Response response = mOAuthHelper.getResponse(request); + + try { + Status status = response.getStatus(); + if (status.isSuccess()) { + JacksonRepresentation entity = + new JacksonRepresentation(response.getEntity(), Map.class); + Map data = entity.getObject(); + return new F1User(user, data); + + } else { + throw new OAuthException(status); + } + + } finally { + if (response.getEntity() != null) { + response.release(); + } + } + + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.getF1User.success"); + } else { + incrementCounter("F1Access.getF1User.failure"); + } + } + } + + @Override + public Map getAttributeList() throws F1Exception { + // Note: this list is shared by all F1 users. + synchronized (mAttributeIdByName) { + if (mAttributeIdByName.size() == 0) { + Timer.Context timer = getTimer("F1Access.getAttributeList.time"); + boolean success = true; + + try { + // Reload attributes. Maybe it will be there now... + Request request = new Request(Method.GET, + mBaseUrl + "People/AttributeGroups.json"); + request.setChallengeResponse(mUser.getChallengeResponse()); + Response response = mOAuthHelper.getResponse(request); + + Representation representation = response.getEntity(); + try { + Status status = response.getStatus(); + if (status.isSuccess()) { + JacksonRepresentation entity = + new JacksonRepresentation(response.getEntity(), Map.class); + + Map attributeGroups = (Map) entity.getObject().get("attributeGroups"); + List groups = (List) attributeGroups.get("attributeGroup"); + + for (Map group : groups) { + List attributes = (List) group.get("attribute"); + if (attributes != null) { + for (Map attribute : attributes) { + String id = (String) attribute.get("@id"); + String name = ((String) attribute.get("name")); + mAttributeIdByName.put(name.toLowerCase(), id); + LOG.debug("Caching attribute '" + name + + "' with id '" + id + "'"); + } + } + } + } + + } catch (IOException e) { + throw new F1Exception("Could not parse AttributeGroups.", e); + + } finally { + if (representation != null) { + representation.release(); + } + } + + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.getAttributeList.success"); + } else { + incrementCounter("F1Access.getAttributeList.failure"); + } + } + } + + return mAttributeIdByName; + } + } + + /** + * Add an attribute to the user. + * + * @param user The user to add the attribute to. + * @param attributeName The attribute to add. + * @param attribute The attribute to add. + */ + public boolean addAttribute(String userId, Attribute attribute) + throws F1Exception { + + // Get the attribute id. + String attributeId = getAttributeId(attribute.getAttributeName()); + if (attributeId == null) { + throw new F1Exception("Could not find id for " + attribute.getAttributeName()); + } + + // Get Attribute Template + Map attributeTemplate = null; + + Timer.Context timer = getTimer("F1Access.addAttribute.GET.time"); + boolean success = true; + + try { + Request request = new Request(Method.GET, + mBaseUrl + "People/" + userId + "/Attributes/new.json"); + request.setChallengeResponse(mUser.getChallengeResponse()); + Response response = mOAuthHelper.getResponse(request); + + Representation representation = response.getEntity(); + try { + Status status = response.getStatus(); + if (status.isSuccess()) { + JacksonRepresentation entity = + new JacksonRepresentation(response.getEntity(), Map.class); + attributeTemplate = entity.getObject(); + + } else { + throw new F1Exception("Failed to retrieve attribute template: " + + status); + } + + } catch (IOException e) { + throw new F1Exception("Could not parse attribute template.", e); + + } finally { + if (representation != null) { + representation.release(); + } + } + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.addAttribute.GET.success"); + } else { + incrementCounter("F1Access.addAttribute.GET.failure"); + } + } + + if (attributeTemplate == null) { + throw new F1Exception("Could not retrieve attribute template."); + } + + // Populate Attribute Template + Map attributeMap = (Map) attributeTemplate.get("attribute"); + Map attributeGroup = (Map) attributeMap.get("attributeGroup"); + + Map attributeIdMap = new HashMap<>(); + attributeIdMap.put("@id", attributeId); + attributeGroup.put("attribute", attributeIdMap); + + if (attribute.getStartDate() != null) { + attributeMap.put("startDate", DATE_FORMAT.format(attribute.getStartDate())); + } + + if (attribute.getStartDate() != null) { + attributeMap.put("endDate", DATE_FORMAT.format(attribute.getStartDate())); + } + + attributeMap.put("comment", attribute.getComment()); + + // POST new attribute + Status status; + timer = getTimer("F1Access.addAttribute.POST.time"); + success = true; + + try { + Request request = new Request(Method.POST, + mBaseUrl + "People/" + userId + "/Attributes.json"); + request.setChallengeResponse(mUser.getChallengeResponse()); + request.setEntity(new JacksonRepresentation(attributeTemplate)); + Response response = mOAuthHelper.getResponse(request); + + Representation representation = response.getEntity(); + try { + status = response.getStatus(); + + if (status.isSuccess()) { + return true; + } + + } finally { + if (representation != null) { + representation.release(); + } + } + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.addAttribute.POST.success"); + } else { + incrementCounter("F1Access.getAccessToken.POST.failure"); + } + } + + LOG.debug("addAttribute failed POST: " + status); + return false; + } + + @Override + public List getAttribute(String userId, String attributeNameFilter) + throws F1Exception { + + Map attributesResponse; + + // Get Attributes + Timer.Context timer = getTimer("F1Access.getAttribute.time"); + boolean success = true; + + try { + Request request = new Request(Method.GET, + mBaseUrl + "People/" + userId + "/Attributes.json"); + request.setChallengeResponse(mUser.getChallengeResponse()); + Response response = mOAuthHelper.getResponse(request); + + Representation representation = response.getEntity(); + try { + Status status = response.getStatus(); + if (status.isSuccess()) { + JacksonRepresentation entity = + new JacksonRepresentation(response.getEntity(), Map.class); + attributesResponse = entity.getObject(); + + } else { + throw new F1Exception("Failed to retrieve attributes: " + + status); + } + + } catch (IOException e) { + throw new F1Exception("Could not parse attributes.", e); + + } finally { + if (representation != null) { + representation.release(); + } + } + } catch (Exception e) { + success = false; + throw e; + + } finally { + if (timer != null) { + timer.stop(); + } + if (success) { + incrementCounter("F1Access.getAttribute.success"); + } else { + incrementCounter("F1Access.getAttribute.failure"); + } + } + + // Parse Response + List result = new ArrayList<>(); + + try { + // I feel like I'm writing lisp here... + Map attributesMap = (Map) attributesResponse.get("attributes"); + if (attributesMap == null) { + return result; + } + + List attributes = (List) (attributesMap).get("attribute"); + for (Map attributeMap : attributes) { + String id = (String) attributeMap.get("@id"); + String startDate = (String) attributeMap.get("startDate"); + String endDate = (String) attributeMap.get("endDate"); + String comment = (String) attributeMap.get("comment"); + + Map attributeIdMap = (Map) ((Map) attributeMap.get("attributeGroup")) + .get("attribute"); + String attributeName = (String) attributeIdMap.get("name"); + + if (attributeNameFilter == null + || attributeNameFilter.equalsIgnoreCase(attributeName)) { + + Attribute attribute = new Attribute(attributeName); + attribute.setId(id); + if (startDate != null) { + attribute.setStartDate(DATE_FORMAT.parse(startDate)); + } + if (endDate != null) { + attribute.setEndDate(DATE_FORMAT.parse(endDate)); + } + attribute.setComment(comment); + result.add(attribute); + } + } + } catch (Exception e) { + throw new F1Exception("Failed to parse attributes response.", e); + } + + return result; + } + + /** + * @return an attribute id for the given attribute name. + */ + private String getAttributeId(String attributeName) throws F1Exception { + Map attributeMap = getAttributeList(); + + return attributeMap.get(attributeName.toLowerCase()); + } + + } + + private Timer.Context getTimer(String name) { + if (mMetricRegistry != null) { + return mMetricRegistry.timer(name).time(); + } else { + return null; + } + } + + private void incrementCounter(String name) { + if (mMetricRegistry != null) { + mMetricRegistry.counter(name).inc(); + } + } +} diff --git a/src/main/java/com/p4square/f1oauth/F1Exception.java b/src/main/java/com/p4square/f1oauth/F1Exception.java new file mode 100644 index 0000000..54c1a77 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/F1Exception.java @@ -0,0 +1,15 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.f1oauth; + +public class F1Exception extends Exception { + public F1Exception(String message) { + super(message); + } + + public F1Exception(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java b/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java new file mode 100644 index 0000000..8382020 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java @@ -0,0 +1,57 @@ +package com.p4square.f1oauth; + +import com.p4square.grow.frontend.ProgressReporter; +import org.apache.log4j.Logger; +import org.restlet.security.User; + +import java.util.Date; + +/** + * A ProgressReporter implementation to record progress in F1. + */ +public class F1ProgressReporter implements ProgressReporter { + + private static final Logger LOG = Logger.getLogger(F1ProgressReporter.class); + + private F1Access mF1Access; + + public F1ProgressReporter(final F1Access f1access) { + mF1Access = f1access; + } + + @Override + public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) { + String attributeName = "Assessment Complete - " + level; + Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(date); + attribute.setComment(results); + addAttribute(user, attribute); + } + + @Override + public void reportChapterComplete(final User user, final String chapter, final Date date) { + final String attributeName = "Training Complete - " + chapter; + final Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(date); + addAttribute(user, attribute); + } + + private void addAttribute(final User user, final Attribute attribute) { + if (!(user instanceof F1User)) { + throw new IllegalArgumentException("User must be an F1User, but got " + user.getClass().getName()); + } + + try { + final F1User f1User = (F1User) user; + final F1API f1 = mF1Access.getAuthenticatedApi(f1User); + + if (!f1.addAttribute(user.getIdentifier(), attribute)) { + LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + + attribute.getAttributeName()); + } + } catch (Exception e) { + LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + + attribute.getAttributeName(), e); + } + } +} diff --git a/src/main/java/com/p4square/f1oauth/F1User.java b/src/main/java/com/p4square/f1oauth/F1User.java new file mode 100644 index 0000000..e5ab487 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/F1User.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import java.util.Map; + +import com.p4square.restlet.oauth.OAuthException; +import com.p4square.restlet.oauth.OAuthUser; + +/** + * + * @author Jesse Morgan + */ +public class F1User extends OAuthUser { + public static final String ID = "@id"; + public static final String FIRST_NAME = "firstName"; + public static final String LAST_NAME = "lastName"; + public static final String ICODE = "@iCode"; + + private final Map mData; + + /** + * Copy the user information from user into a new F1User. + * + * @param user Original user. + * @param data F1 Person Record. + * @throws IllegalStateException if data.get("person") is null. + */ + public F1User(OAuthUser user, Map data) { + super(user.getLocation(), user.getToken()); + + mData = (Map) data.get("person"); + if (mData == null) { + throw new IllegalStateException("Bad data"); + } + + setIdentifier(getString(ID)); + setFirstName(getString(FIRST_NAME)); + setLastName(getString(LAST_NAME)); + } + + /** + * Get a String from the map. + * + * @param key The map key. + * @return The value associated with the key, or null. + */ + public String getString(String key) { + Object blob = get(key); + + if (blob instanceof String) { + return (String) blob; + + } else { + return null; + } + } + + /** + * Fetch an object from the F1 record. + * + * @param key The map key + * @return The object in the map or null. + */ + public Object get(String key) { + return mData.get(key); + } +} diff --git a/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java b/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java new file mode 100644 index 0000000..865f5d6 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java @@ -0,0 +1,55 @@ +package com.p4square.f1oauth; + +import com.codahale.metrics.MetricRegistry; +import com.p4square.grow.config.Config; +import com.p4square.grow.frontend.IntegrationDriver; +import com.p4square.grow.frontend.ProgressReporter; +import org.restlet.Context; +import org.restlet.security.Verifier; + +/** + * The FellowshipOneIntegrationDriver creates implementations of various + * objects to support integration with Fellowship One. + */ +public class FellowshipOneIntegrationDriver implements IntegrationDriver { + + private final Context mContext; + private final MetricRegistry mMetricRegistry; + private final Config mConfig; + private final F1Access mAPI; + + private final ProgressReporter mProgressReporter; + + public FellowshipOneIntegrationDriver(final Context context) { + mContext = context; + mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); + mMetricRegistry = (MetricRegistry) context.getAttributes().get("com.p4square.grow.metrics"); + + mAPI = new F1Access(context, + mConfig.getString("f1ConsumerKey", ""), + mConfig.getString("f1ConsumerSecret", ""), + mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"), + mConfig.getString("f1ChurchCode", "pfseawa"), + F1Access.UserType.WEBLINK); + mAPI.setMetricRegistry(mMetricRegistry); + + mProgressReporter = new F1ProgressReporter(mAPI); + } + + /** + * @return An F1Access instance. + */ + public F1Access getF1Access() { + return mAPI; + } + + @Override + public Verifier newUserAuthenticationVerifier() { + return new SecondPartyVerifier(mContext, mAPI); + } + + @Override + public ProgressReporter getProgressReporter() { + return mProgressReporter; + } +} diff --git a/src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java b/src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java new file mode 100644 index 0000000..8deefec --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import org.apache.log4j.Logger; + +import com.p4square.restlet.oauth.OAuthException; +import com.p4square.restlet.oauth.OAuthUser; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; + +/** + * Restlet Authenticator for 2nd + * @author Jesse Morgan + */ +public class SecondPartyAuthenticator extends Authenticator { + private static final Logger LOG = Logger.getLogger(SecondPartyAuthenticator.class); + + private final F1Access mHelper; + + public SecondPartyAuthenticator(Context context, boolean optional, F1Access helper) { + super(context, optional); + + mHelper = helper; + } + + protected boolean authenticate(Request request, Response response) { + if (request.getChallengeResponse() == null) { + return false; // no credentials + } + + String username = request.getChallengeResponse().getIdentifier(); + String password = new String(request.getChallengeResponse().getSecret()); + + try { + OAuthUser user = mHelper.getAccessToken(username, password); + request.getClientInfo().setUser(user); + + return true; + + } catch (OAuthException e) { + LOG.info("OAuth Exception: " + e); + } + + return false; // Invalid credentials + } +} diff --git a/src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java b/src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java new file mode 100644 index 0000000..882c7e7 --- /dev/null +++ b/src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.f1oauth; + +import java.io.IOException; +import java.util.Map; + +import org.apache.log4j.Logger; + +import com.p4square.restlet.oauth.OAuthException; +import com.p4square.restlet.oauth.OAuthUser; + +import org.restlet.Context; +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.ext.jackson.JacksonRepresentation; +import org.restlet.security.Verifier; + +/** + * Restlet Verifier for F1 2nd Party Authentication + * + * @author Jesse Morgan + */ +public class SecondPartyVerifier implements Verifier { + private static final Logger LOG = Logger.getLogger(SecondPartyVerifier.class); + + private final Restlet mDispatcher; + private final F1Access mHelper; + + public SecondPartyVerifier(Context context, F1Access helper) { + if (helper == null) { + throw new IllegalArgumentException("Helper can not be null."); + } + + mDispatcher = context.getClientDispatcher(); + mHelper = helper; + } + + @Override + public int verify(Request request, Response response) { + if (request.getChallengeResponse() == null) { + return RESULT_MISSING; // no credentials + } + + String username = request.getChallengeResponse().getIdentifier(); + String password = new String(request.getChallengeResponse().getSecret()); + + try { + OAuthUser ouser = mHelper.getAccessToken(username, password); + + // Once we have a user, fetch the people record to get the user id. + F1User user = mHelper.getAuthenticatedApi(ouser).getF1User(ouser); + user.setEmail(username); + + // This seems like a hack... but it'll work + request.getClientInfo().setUser(user); + + return RESULT_VALID; + + } catch (Exception e) { + LOG.info("OAuth Exception: " + e, e); + } + + return RESULT_INVALID; // Invalid credentials + } + +} diff --git a/src/main/java/com/p4square/fmfacade/FMFacade.java b/src/main/java/com/p4square/fmfacade/FMFacade.java new file mode 100644 index 0000000..0e552b0 --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/FMFacade.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade; + +import java.io.IOException; + +import org.restlet.Application; +import org.restlet.Component; +import org.restlet.data.Protocol; +import org.restlet.Restlet; +import org.restlet.routing.Router; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; + +import org.apache.log4j.Logger; + +import com.p4square.grow.config.Config; + +/** + * + * @author Jesse Morgan + */ +public class FMFacade extends Application { + private static final Logger cLog = Logger.getLogger(FMFacade.class); + private final Configuration mFMConfig; + + public FMFacade() { + mFMConfig = new Configuration(); + mFMConfig.setClassForTemplateLoading(getClass(), "/templates"); + mFMConfig.setObjectWrapper(new DefaultObjectWrapper()); + } + + /** + * @return a Config object. + */ + public Config getConfig() { + return null; + } + + @Override + public synchronized Restlet createInboundRoot() { + return createRouter(); + } + + /** + * Retrieve a template. + * + * @param name The template name. + * @return A FreeMarker template or null on error. + */ + public Template getTemplate(String name) { + try { + return mFMConfig.getTemplate(name); + + } catch (IOException e) { + cLog.error("Could not load template \"" + name + "\"", e); + return null; + } + } + + /** + * Create the router to be used by this application. This can be overriden + * by sub-classes to add additional routes. + * + * @return The router. + */ + protected Router createRouter() { + Router router = new Router(getContext()); + router.attachDefault(FreeMarkerPageResource.class); + + return router; + } + + /** + * 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.getDefaultHost().attach(new FMFacade()); + + // Setup shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + try { + component.stop(); + } catch (Exception e) { + cLog.error("Exception during cleanup", e); + } + } + }); + + cLog.info("Starting server..."); + + try { + component.start(); + } catch (Exception e) { + cLog.fatal("Could not start: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/p4square/fmfacade/FreeMarkerPageResource.java b/src/main/java/com/p4square/fmfacade/FreeMarkerPageResource.java new file mode 100644 index 0000000..8c8948a --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/FreeMarkerPageResource.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade; + +import java.util.Map; +import java.util.HashMap; + +import freemarker.template.Template; + +import org.restlet.Context; +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.ext.freemarker.TemplateRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.ServerResource; +import org.restlet.security.User; + +import org.apache.log4j.Logger; + +import com.p4square.fmfacade.ftl.GetMethod; + +import com.p4square.session.Session; +import com.p4square.session.Sessions; + +/** + * + * @author Jesse Morgan + */ +public class FreeMarkerPageResource extends ServerResource { + private static Logger cLog = Logger.getLogger(FreeMarkerPageResource.class); + + public static Map baseRootObject(final Context context, final FMFacade fmf) { + Map root = new HashMap(); + + root.put("get", new GetMethod(context.getClientDispatcher())); + root.put("config", fmf.getConfig()); + + return root; + } + + private FMFacade mFMF; + private String mCurrentPage; + + @Override + public void doInit() { + mFMF = (FMFacade) getApplication(); + mCurrentPage = getReference().getRemainingPart(false, false); + } + + protected Representation get() { + try { + Template t = mFMF.getTemplate("pages" + mCurrentPage + ".ftl"); + + if (t == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new TemplateRepresentation(t, getRootObject(), + MediaType.TEXT_HTML); + + } catch (Exception e) { + cLog.fatal("Could not render page: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } + + /** + * Build and return the root object to pass to the FTL Template. + * @return A map of objects and methods for the template to access. + */ + protected Map getRootObject() { + Map root = baseRootObject(getContext(), mFMF); + + root.put("attributes", getRequestAttributes()); + root.put("query", getQuery().getValuesMap()); + + if (getClientInfo().isAuthenticated()) { + final User user = getClientInfo().getUser(); + final Map userMap = new HashMap(); + userMap.put("id", user.getIdentifier()); + userMap.put("firstName", user.getFirstName()); + userMap.put("lastName", user.getLastName()); + userMap.put("email", user.getEmail()); + root.put("user", userMap); + } + + Session s = Sessions.getInstance().get(getRequest()); + if (s != null) { + root.put("session", s.getMap()); + } + + return root; + } +} diff --git a/src/main/java/com/p4square/fmfacade/ftl/GetMethod.java b/src/main/java/com/p4square/fmfacade/ftl/GetMethod.java new file mode 100644 index 0000000..a47c4b0 --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/ftl/GetMethod.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade.ftl; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import java.io.IOException; + +import freemarker.core.Environment; +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import org.apache.log4j.Logger; + +import org.restlet.data.Status; +import org.restlet.data.Method; +import org.restlet.representation.Representation; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; + +import org.restlet.ext.jackson.JacksonRepresentation; + +/** + * This method allows templates to make GET requests. + * + * @author Jesse Morgan + */ +public class GetMethod implements TemplateMethodModel { + private static final Logger cLog = Logger.getLogger(GetMethod.class); + + private final Restlet mDispatcher; + + public GetMethod(Restlet dispatcher) { + mDispatcher = dispatcher; + } + + /** + * @param args List with exactly two arguments: + * * The variable in which to put the result. + * * The URI to GET. + */ + public TemplateModel exec(List args) throws TemplateModelException { + final Environment env = Environment.getCurrentEnvironment(); + + if (args.size() != 2) { + throw new TemplateModelException( + "Expecting exactly one argument containing the URI"); + } + + Request request = new Request(Method.GET, (String) args.get(1)); + Response response = mDispatcher.handle(request); + Status status = response.getStatus(); + Representation representation = response.getEntity(); + + try { + if (response.getStatus().isSuccess()) { + JacksonRepresentation mapRepresentation; + if (representation instanceof JacksonRepresentation) { + mapRepresentation = (JacksonRepresentation) representation; + } else { + mapRepresentation = new JacksonRepresentation( + representation, Map.class); + } + try { + TemplateModel mapModel = env.getObjectWrapper().wrap(mapRepresentation.getObject()); + + env.setVariable((String) args.get(0), mapModel); + + } catch (IOException e) { + cLog.warn("Exception occurred when calling getObject(): " + + e.getMessage(), e); + status = Status.SERVER_ERROR_INTERNAL; + } + } + + Map statusMap = new HashMap(); + statusMap.put("code", status.getCode()); + statusMap.put("reason", status.getReasonPhrase()); + statusMap.put("succeeded", status.isSuccess()); + return env.getObjectWrapper().wrap(statusMap); + } finally { + if (representation != null) { + representation.release(); + } + } + } +} diff --git a/src/main/java/com/p4square/fmfacade/json/ClientException.java b/src/main/java/com/p4square/fmfacade/json/ClientException.java new file mode 100644 index 0000000..c233193 --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/json/ClientException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade.json; + +/** + * + * @author Jesse Morgan + */ +public class ClientException extends Exception { + + public ClientException(final String msg) { + super(msg); + } + + public ClientException(final String msg, final Exception cause) { + super(msg, cause); + } +} diff --git a/src/main/java/com/p4square/fmfacade/json/JsonRequestClient.java b/src/main/java/com/p4square/fmfacade/json/JsonRequestClient.java new file mode 100644 index 0000000..19a394f --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/json/JsonRequestClient.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade.json; + +import java.util.Map; + +import java.io.IOException; + +import org.apache.log4j.Logger; + +import org.restlet.data.Status; +import org.restlet.data.Method; +import org.restlet.representation.Representation; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; + +import org.restlet.ext.jackson.JacksonRepresentation; + +/** + * + * @author Jesse Morgan + */ +public class JsonRequestClient { + private final Restlet mDispatcher; + + public JsonRequestClient(Restlet dispatcher) { + mDispatcher = dispatcher; + } + + /** + * Perform a GET request for the given URI and parse the response as a + * JSON map. + * + * @return A JsonResponse object which can be used to retrieve the + * response as a JSON map. + */ + public JsonResponse get(final String uri) { + final Request request = new Request(Method.GET, uri); + final Response response = mDispatcher.handle(request); + + return new JsonResponse(response); + } + + /** + * Perform a PUT request for the given URI and parse the response as a + * JSON map. + * + * @return A JsonResponse object which can be used to retrieve the + * response as a JSON map. + */ + public JsonResponse put(final String uri, Representation entity) { + final Request request = new Request(Method.PUT, uri); + request.setEntity(entity); + + final Response response = mDispatcher.handle(request); + return new JsonResponse(response); + } + + /** + * Perform a PUT request for the given URI and parse the response as a + * JSON map. + * + * @return A JsonResponse object which can be used to retrieve the + * response as a JSON map. + */ + public JsonResponse put(final String uri, Map map) { + return put(uri, new JacksonRepresentation(map)); + } + + /** + * Perform a POST request for the given URI and parse the response as a + * JSON map. + * + * @return A JsonResponse object which can be used to retrieve the + * response as a JSON map. + */ + public JsonResponse post(final String uri, Representation entity) { + final Request request = new Request(Method.POST, uri); + request.setEntity(entity); + + final Response response = mDispatcher.handle(request); + return new JsonResponse(response); + } + + /** + * Perform a POST request for the given URI and parse the response as a + * JSON map. + * + * @return A JsonResponse object which can be used to retrieve the + * response as a JSON map. + */ + public JsonResponse post(final String uri, Map map) { + return post(uri, new JacksonRepresentation(map)); + } + + /** + * Perform a DELETE request for the given URI. + * + * @return A JsonResponse object with the status of the request. + */ + public JsonResponse delete(final String uri) { + final Request request = new Request(Method.DELETE, uri); + final Response response = mDispatcher.handle(request); + return new JsonResponse(response); + } +} diff --git a/src/main/java/com/p4square/fmfacade/json/JsonResponse.java b/src/main/java/com/p4square/fmfacade/json/JsonResponse.java new file mode 100644 index 0000000..b9cb587 --- /dev/null +++ b/src/main/java/com/p4square/fmfacade/json/JsonResponse.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.fmfacade.json; + +import java.util.Map; + +import java.io.IOException; + +import org.restlet.data.Status; +import org.restlet.data.Reference; +import org.restlet.representation.Representation; +import org.restlet.Response; + +import org.restlet.ext.jackson.JacksonRepresentation; + +/** + * JsonResponse wraps a Restlet Response object and parses the entity, if any, + * as a JSON map. + * + * @author Jesse Morgan + */ +public class JsonResponse { + private final Response mResponse; + private final Representation mRepresentation; + + private Map mMap; + + JsonResponse(Response response) { + mResponse = response; + mRepresentation = response.getEntity(); + mMap = null; + + if (!response.getStatus().isSuccess()) { + if (mRepresentation != null) { + mRepresentation.release(); + } + } + } + + /** + * @return the Status info from the response. + */ + public Status getStatus() { + return mResponse.getStatus(); + } + + /** + * @return the Reference for a redirect. + */ + public Reference getRedirectLocation() { + return mResponse.getLocationRef(); + } + + /** + * Return the parsed json map from the response. + */ + public Map getMap() throws ClientException { + if (mMap == null) { + Representation representation = mRepresentation; + + // Parse response + if (representation == null) { + return null; + } + + JacksonRepresentation mapRepresentation; + if (representation instanceof JacksonRepresentation) { + mapRepresentation = (JacksonRepresentation) representation; + } else { + mapRepresentation = new JacksonRepresentation( + representation, Map.class); + } + + try { + mMap = (Map) mapRepresentation.getObject(); + + } catch (IOException e) { + throw new ClientException("Failed to parse response: " + e.getMessage(), e); + } + } + + return mMap; + } + +} diff --git a/src/main/java/com/p4square/grow/GrowProcessComponent.java b/src/main/java/com/p4square/grow/GrowProcessComponent.java new file mode 100644 index 0000000..608ec0d --- /dev/null +++ b/src/main/java/com/p4square/grow/GrowProcessComponent.java @@ -0,0 +1,166 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.ConsoleReporter; +import com.codahale.metrics.MetricRegistry; + +import org.apache.log4j.Logger; + +import org.restlet.Application; +import org.restlet.Component; +import org.restlet.Restlet; +import org.restlet.data.ChallengeScheme; +import org.restlet.data.Protocol; +import org.restlet.resource.Directory; +import org.restlet.security.ChallengeAuthenticator; + +import com.p4square.grow.backend.BackendVerifier; +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.config.Config; +import com.p4square.grow.frontend.GrowFrontend; +import com.p4square.restlet.metrics.MetricsApplication; + +/** + * + * @author Jesse Morgan + */ +public class GrowProcessComponent extends Component { + private static Logger LOG = Logger.getLogger(GrowProcessComponent.class); + + private static final String BACKEND_REALM = "Grow Backend"; + + private final Config mConfig; + private final MetricRegistry mMetricRegistry; + + /** + * Create a new Grow Process website component combining a frontend and backend. + */ + public GrowProcessComponent() throws Exception { + this(new Config()); + } + + public GrowProcessComponent(Config config) throws Exception { + // Clients + getClients().add(Protocol.FILE); + getClients().add(Protocol.HTTP); + getClients().add(Protocol.HTTPS); + + // Prepare mConfig + mConfig = config; + mConfig.updateConfig(this.getClass().getResourceAsStream("/grow.properties")); + + // Prepare Metrics + mMetricRegistry = new MetricRegistry(); + + // Frontend + GrowFrontend frontend = new GrowFrontend(mConfig, mMetricRegistry); + getDefaultHost().attach(frontend); + + // Backend + GrowBackend backend = new GrowBackend(mConfig, mMetricRegistry); + getInternalRouter().attach("/backend", backend); + + // Authenticated access to the backend + BackendVerifier verifier = new BackendVerifier(backend.getUserRecordProvider()); + ChallengeAuthenticator auth = new ChallengeAuthenticator(getContext().createChildContext(), + false, ChallengeScheme.HTTP_BASIC, BACKEND_REALM, verifier); + auth.setNext(backend); + getDefaultHost().attach("/backend", auth); + + // Authenticated access to metrics + ChallengeAuthenticator metricAuth = new ChallengeAuthenticator( + getContext().createChildContext(), false, + ChallengeScheme.HTTP_BASIC, BACKEND_REALM, verifier); + metricAuth.setNext(new MetricsApplication(mMetricRegistry)); + getDefaultHost().attach("/metrics", metricAuth); + } + + + @Override + public void start() throws Exception { + String configDomain = getContext().getParameters().getFirstValue("com.p4square.grow.configDomain"); + if (configDomain != null) { + mConfig.setDomain(configDomain); + } + + String configFilename = getContext().getParameters().getFirstValue("com.p4square.grow.configFile"); + if (configFilename != null) { + mConfig.updateConfig(configFilename); + } + + super.start(); + } + + /** + * Stand-alone main for testing. + */ + public static void main(String[] args) throws Exception { + // Load an optional config file from the first argument. + Config config = new Config(); + config.setDomain("dev"); + if (args.length >= 1) { + config.updateConfig(args[0]); + } + + // Override domain + if (args.length == 2) { + config.setDomain(args[1]); + } + + // Start the HTTP Server + final GrowProcessComponent component = new GrowProcessComponent(config); + component.getServers().add(Protocol.HTTP, 8085); + + // Static content + try { + component.getDefaultHost().attach("/images/", new FileServingApp("./src/main/webapp/images/")); + component.getDefaultHost().attach("/scripts", new FileServingApp("./src/main/webapp/scripts")); + component.getDefaultHost().attach("/style.css", new FileServingApp("./src/main/webapp/style.css")); + component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./src/main/webapp/favicon.ico")); + component.getDefaultHost().attach("/notfound.html", new FileServingApp("./src/main/webapp/notfound.html")); + component.getDefaultHost().attach("/error.html", new FileServingApp("./src/main/webapp/error.html")); + } catch (IOException e) { + LOG.error("Could not create directory for static resources: " + + e.getMessage(), e); + } + + // 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/backend/BackendVerifier.java b/src/main/java/com/p4square/grow/backend/BackendVerifier.java new file mode 100644 index 0000000..83160a9 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/BackendVerifier.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; + +import org.restlet.security.SecretVerifier; + +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.Provider; + +/** + * Verify the given credentials against the users with backend access. + */ +public class BackendVerifier extends SecretVerifier { + + private final Provider mUserProvider; + + public BackendVerifier(Provider userProvider) { + mUserProvider = userProvider; + } + + @Override + public int verify(String identifier, char[] secret) { + if (identifier == null) { + throw new IllegalArgumentException("Null identifier"); + } + + if (secret == null) { + throw new IllegalArgumentException("Null secret"); + } + + // Does the user exist? + UserRecord user; + try { + user = mUserProvider.get(identifier); + if (user == null) { + return RESULT_UNKNOWN; + } + + } catch (IOException e) { + return RESULT_UNKNOWN; + } + + // Does the user have a backend password? + String storedHash = user.getBackendPasswordHash(); + if (storedHash == null) { + // This user doesn't have access + return RESULT_INVALID; + } + + // Validate the password. + try { + String hashedInput = hashPassword(secret); + if (hashedInput.equals(storedHash)) { + return RESULT_VALID; + } + + } catch (NoSuchAlgorithmException e) { + return RESULT_UNSUPPORTED; + } + + // If all else fails, fail. + return RESULT_INVALID; + } + + /** + * Hash the given secret. + */ + public static String hashPassword(char[] secret) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + // Convert the char[] to byte[] + // FIXME This approach is incorrectly truncating multibyte + // characters. + byte[] b = new byte[secret.length]; + for (int i = 0; i < secret.length; i++) { + b[i] = (byte) secret[i]; + } + + md.update(b); + + byte[] hash = md.digest(); + return new String(Hex.encodeHex(hash)); + } +} diff --git a/src/main/java/com/p4square/grow/backend/CassandraGrowData.java b/src/main/java/com/p4square/grow/backend/CassandraGrowData.java new file mode 100644 index 0000000..22a7716 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/CassandraGrowData.java @@ -0,0 +1,172 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import java.io.IOException; + +import com.p4square.grow.config.Config; + +import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.backend.db.CassandraKey; +import com.p4square.grow.backend.db.CassandraProviderImpl; +import com.p4square.grow.backend.db.CassandraCollectionProvider; +import com.p4square.grow.backend.db.CassandraTrainingRecordProvider; + +import com.p4square.grow.model.Message; +import com.p4square.grow.model.MessageThread; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.UserRecord; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.DelegateCollectionProvider; +import com.p4square.grow.provider.DelegateProvider; +import com.p4square.grow.provider.Provider; + +/** + * + * @author Jesse Morgan + */ +class CassandraGrowData implements GrowData { + private static final String DEFAULT_COLUMN = "value"; + + private final Config mConfig; + private final CassandraDatabase mDatabase; + + private final Provider mUserRecordProvider; + + private final Provider mQuestionProvider; + private final CassandraTrainingRecordProvider mTrainingRecordProvider; + private final CollectionProvider mVideoProvider; + + private final CollectionProvider mFeedThreadProvider; + private final CollectionProvider mFeedMessageProvider; + + private final Provider mStringProvider; + + private final CollectionProvider mAnswerProvider; + + public CassandraGrowData(final Config config) { + mConfig = config; + mDatabase = new CassandraDatabase(); + + mUserRecordProvider = new DelegateProvider( + new CassandraProviderImpl(mDatabase, UserRecord.class)) { + @Override + public CassandraKey makeKey(String userid) { + return new CassandraKey("accounts", userid, DEFAULT_COLUMN); + } + }; + + mQuestionProvider = new DelegateProvider( + new CassandraProviderImpl(mDatabase, Question.class)) { + @Override + public CassandraKey makeKey(String questionId) { + return new CassandraKey("strings", "/questions/" + questionId, DEFAULT_COLUMN); + } + }; + + mFeedThreadProvider = new CassandraCollectionProvider(mDatabase, + "feedthreads", MessageThread.class); + mFeedMessageProvider = new CassandraCollectionProvider(mDatabase, + "feedmessages", Message.class); + + mTrainingRecordProvider = new CassandraTrainingRecordProvider(mDatabase); + + mVideoProvider = new DelegateCollectionProvider( + new CassandraCollectionProvider(mDatabase, "strings", String.class)) { + @Override + public String makeCollectionKey(String key) { + return "/training/" + key; + } + + @Override + public String makeKey(String key) { + return key; + } + + @Override + public String unmakeKey(String key) { + return key; + } + }; + + mStringProvider = new DelegateProvider( + new CassandraProviderImpl(mDatabase, String.class)) { + @Override + public CassandraKey makeKey(String id) { + return new CassandraKey("strings", id, DEFAULT_COLUMN); + } + }; + + mAnswerProvider = new CassandraCollectionProvider( + mDatabase, "assessments", String.class); + } + + @Override + public void start() throws Exception { + mDatabase.setClusterName(mConfig.getString("clusterName", "Dev Cluster")); + mDatabase.setKeyspaceName(mConfig.getString("keyspace", "GROW")); + mDatabase.init(); + } + + @Override + public void stop() throws Exception { + mDatabase.close(); + } + + /** + * @return the current database. + */ + public CassandraDatabase getDatabase() { + return mDatabase; + } + + @Override + public Provider getUserRecordProvider() { + return mUserRecordProvider; + } + + @Override + public Provider getQuestionProvider() { + return mQuestionProvider; + } + + @Override + public Provider getTrainingRecordProvider() { + return mTrainingRecordProvider; + } + + @Override + public CollectionProvider getVideoProvider() { + return mVideoProvider; + } + + @Override + public Playlist getDefaultPlaylist() throws IOException { + return mTrainingRecordProvider.getDefaultPlaylist(); + } + + @Override + public CollectionProvider getThreadProvider() { + return mFeedThreadProvider; + } + + @Override + public CollectionProvider getMessageProvider() { + return mFeedMessageProvider; + } + + @Override + public Provider getStringProvider() { + return mStringProvider; + } + + @Override + public CollectionProvider getAnswerProvider() { + return mAnswerProvider; + } +} diff --git a/src/main/java/com/p4square/grow/backend/DynamoGrowData.java b/src/main/java/com/p4square/grow/backend/DynamoGrowData.java new file mode 100644 index 0000000..3b38eac --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/DynamoGrowData.java @@ -0,0 +1,180 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import java.io.IOException; + +import com.amazonaws.auth.AWSCredentials; + +import com.p4square.grow.backend.dynamo.DynamoDatabase; +import com.p4square.grow.backend.dynamo.DynamoKey; +import com.p4square.grow.backend.dynamo.DynamoProviderImpl; +import com.p4square.grow.backend.dynamo.DynamoCollectionProviderImpl; + +import com.p4square.grow.config.Config; + +import com.p4square.grow.model.Message; +import com.p4square.grow.model.MessageThread; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.UserRecord; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.DelegateCollectionProvider; +import com.p4square.grow.provider.DelegateProvider; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * + * @author Jesse Morgan + */ +class DynamoGrowData implements GrowData { + private static final String DEFAULT_COLUMN = "value"; + private static final String DEFAULT_PLAYLIST_KEY = "/training/defaultplaylist"; + + private final Config mConfig; + private final DynamoDatabase mDatabase; + + private final Provider mUserRecordProvider; + + private final Provider mQuestionProvider; + private final Provider mTrainingRecordProvider; + private final CollectionProvider mVideoProvider; + + private final CollectionProvider mFeedThreadProvider; + private final CollectionProvider mFeedMessageProvider; + + private final Provider mStringProvider; + + private final CollectionProvider mAnswerProvider; + + public DynamoGrowData(final Config config) { + mConfig = config; + + mDatabase = new DynamoDatabase(config); + + mUserRecordProvider = new DelegateProvider( + new DynamoProviderImpl(mDatabase, UserRecord.class)) { + @Override + public DynamoKey makeKey(String userid) { + return DynamoKey.newAttributeKey("accounts", userid, DEFAULT_COLUMN); + } + }; + + mQuestionProvider = new DelegateProvider( + new DynamoProviderImpl(mDatabase, Question.class)) { + @Override + public DynamoKey makeKey(String questionId) { + return DynamoKey.newAttributeKey("strings", + "/questions/" + questionId, + DEFAULT_COLUMN); + } + }; + + mFeedThreadProvider = new DynamoCollectionProviderImpl( + mDatabase, "feedthreads", MessageThread.class); + mFeedMessageProvider = new DynamoCollectionProviderImpl( + mDatabase, "feedmessages", Message.class); + + mTrainingRecordProvider = new DelegateProvider( + new DynamoProviderImpl(mDatabase, TrainingRecord.class)) { + @Override + public DynamoKey makeKey(String userId) { + return DynamoKey.newAttributeKey("training", + userId, + DEFAULT_COLUMN); + } + }; + + mVideoProvider = new DelegateCollectionProvider( + new DynamoCollectionProviderImpl(mDatabase, "strings", String.class)) { + @Override + public String makeCollectionKey(String key) { + return "/training/" + key; + } + + @Override + public String makeKey(String key) { + return key; + } + + @Override + public String unmakeKey(String key) { + return key; + } + }; + + mStringProvider = new DelegateProvider( + new DynamoProviderImpl(mDatabase, String.class)) { + @Override + public DynamoKey makeKey(String id) { + return DynamoKey.newAttributeKey("strings", id, DEFAULT_COLUMN); + } + }; + + mAnswerProvider = new DynamoCollectionProviderImpl( + mDatabase, "assessments", String.class); + } + + @Override + public void start() throws Exception { + } + + @Override + public void stop() throws Exception { + } + + @Override + public Provider getUserRecordProvider() { + return mUserRecordProvider; + } + + @Override + public Provider getQuestionProvider() { + return mQuestionProvider; + } + + @Override + public Provider getTrainingRecordProvider() { + return mTrainingRecordProvider; + } + + @Override + public CollectionProvider getVideoProvider() { + return mVideoProvider; + } + + @Override + public Playlist getDefaultPlaylist() throws IOException { + String blob = mStringProvider.get(DEFAULT_PLAYLIST_KEY); + if (blob == null) { + return null; + } + + return JsonEncodedProvider.MAPPER.readValue(blob, Playlist.class); + } + + @Override + public CollectionProvider getThreadProvider() { + return mFeedThreadProvider; + } + + @Override + public CollectionProvider getMessageProvider() { + return mFeedMessageProvider; + } + + @Override + public Provider getStringProvider() { + return mStringProvider; + } + + @Override + public CollectionProvider getAnswerProvider() { + return mAnswerProvider; + } +} diff --git a/src/main/java/com/p4square/grow/backend/GrowBackend.java b/src/main/java/com/p4square/grow/backend/GrowBackend.java new file mode 100644 index 0000000..4091138 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/GrowBackend.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import java.io.IOException; + +import com.codahale.metrics.MetricRegistry; + +import org.apache.log4j.Logger; + +import org.restlet.Application; +import org.restlet.Component; +import org.restlet.Restlet; +import org.restlet.data.Protocol; +import org.restlet.data.Reference; +import org.restlet.resource.Directory; +import org.restlet.routing.Router; + +import com.p4square.grow.config.Config; + +import com.p4square.grow.model.Message; +import com.p4square.grow.model.MessageThread; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.UserRecord; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesQuestions; +import com.p4square.grow.provider.ProvidesTrainingRecords; +import com.p4square.grow.provider.ProvidesUserRecords; + +import com.p4square.grow.backend.resources.AccountResource; +import com.p4square.grow.backend.resources.BannerResource; +import com.p4square.grow.backend.resources.SurveyResource; +import com.p4square.grow.backend.resources.SurveyResultsResource; +import com.p4square.grow.backend.resources.TrainingRecordResource; +import com.p4square.grow.backend.resources.TrainingResource; + +import com.p4square.grow.backend.feed.FeedDataProvider; +import com.p4square.grow.backend.feed.ThreadResource; +import com.p4square.grow.backend.feed.TopicResource; + +import com.p4square.restlet.metrics.MetricRouter; + +/** + * Main class for the backend application. + * + * @author Jesse Morgan + */ +public class GrowBackend extends Application implements GrowData { + + private final static Logger LOG = Logger.getLogger(GrowBackend.class); + + private final MetricRegistry mMetricRegistry; + + private final Config mConfig; + private final GrowData mGrowData; + + public GrowBackend() { + this(new Config(), new MetricRegistry()); + } + + public GrowBackend(Config config, MetricRegistry metricRegistry) { + mConfig = config; + + mMetricRegistry = metricRegistry; + + mGrowData = new DynamoGrowData(config); + } + + public MetricRegistry getMetrics() { + return mMetricRegistry; + } + + @Override + public Restlet createInboundRoot() { + Router router = new MetricRouter(getContext(), mMetricRegistry); + + // Account API + router.attach("/accounts/{userId}", AccountResource.class); + + // Survey API + router.attach("/assessment/question/{questionId}", SurveyResource.class); + + router.attach("/accounts/{userId}/assessment", SurveyResultsResource.class); + router.attach("/accounts/{userId}/assessment/answers/{questionId}", + SurveyResultsResource.class); + + // Training API + router.attach("/training/{level}", TrainingResource.class); + router.attach("/training/{level}/videos/{videoId}", TrainingResource.class); + + router.attach("/accounts/{userId}/training", TrainingRecordResource.class); + router.attach("/accounts/{userId}/training/videos/{videoId}", + TrainingRecordResource.class); + + // Misc. + router.attach("/banner", BannerResource.class); + + // Feed + router.attach("/feed/{topic}", TopicResource.class); + router.attach("/feed/{topic}/{thread}", ThreadResource.class); + //router.attach("/feed/{topic/{thread}/{message}", MessageResource.class); + + router.attachDefault(new Directory(getContext(), new Reference(getClass().getResource("apiinfo.html")))); + + return router; + } + + /** + * Open the database. + */ + @Override + public void start() throws Exception { + super.start(); + + mGrowData.start(); + } + + /** + * Close the database. + */ + @Override + public void stop() throws Exception { + LOG.info("Shutting down..."); + mGrowData.stop(); + + super.stop(); + } + + @Override + public Provider getUserRecordProvider() { + return mGrowData.getUserRecordProvider(); + } + + @Override + public Provider getQuestionProvider() { + return mGrowData.getQuestionProvider(); + } + + @Override + public CollectionProvider getVideoProvider() { + return mGrowData.getVideoProvider(); + } + + @Override + public Provider getTrainingRecordProvider() { + return mGrowData.getTrainingRecordProvider(); + } + + /** + * @return the Default Playlist. + */ + public Playlist getDefaultPlaylist() throws IOException { + return mGrowData.getDefaultPlaylist(); + } + + @Override + public CollectionProvider getThreadProvider() { + return mGrowData.getThreadProvider(); + } + + @Override + public CollectionProvider getMessageProvider() { + return mGrowData.getMessageProvider(); + } + + @Override + public Provider getStringProvider() { + return mGrowData.getStringProvider(); + } + + @Override + public CollectionProvider getAnswerProvider() { + return mGrowData.getAnswerProvider(); + } + + /** + * Stand-alone main for testing. + */ + public static void main(String[] args) throws Exception { + // Start the HTTP Server + final Component component = new Component(); + component.getServers().add(Protocol.HTTP, 9095); + component.getClients().add(Protocol.HTTP); + component.getDefaultHost().attach(new GrowBackend()); + + // 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); + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/GrowData.java b/src/main/java/com/p4square/grow/backend/GrowData.java new file mode 100644 index 0000000..293bb88 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/GrowData.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import com.p4square.grow.backend.feed.FeedDataProvider; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.provider.ProvidesAssessments; +import com.p4square.grow.provider.ProvidesQuestions; +import com.p4square.grow.provider.ProvidesStrings; +import com.p4square.grow.provider.ProvidesTrainingRecords; +import com.p4square.grow.provider.ProvidesUserRecords; +import com.p4square.grow.provider.ProvidesVideos; + +/** + * Aggregate of the data provider interfaces. + * + * Used by GrowBackend to swap out implementations of the providers. + * + * @author Jesse Morgan + */ +interface GrowData extends ProvidesQuestions, ProvidesTrainingRecords, ProvidesVideos, + FeedDataProvider, ProvidesUserRecords, ProvidesStrings, + ProvidesAssessments { + + /** + * Start the data provider. + */ + void start() throws Exception; + + /** + * Stop the data provider. + */ + void stop() throws Exception; +} diff --git a/src/main/java/com/p4square/grow/backend/db/CassandraCollectionProvider.java b/src/main/java/com/p4square/grow/backend/db/CassandraCollectionProvider.java new file mode 100644 index 0000000..bfcb48d --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/db/CassandraCollectionProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.netflix.astyanax.model.Column; +import com.netflix.astyanax.model.ColumnList; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * CollectionProvider implementation backed by a Cassandra ColumnFamily. + * + * @author Jesse Morgan + */ +public class CassandraCollectionProvider implements CollectionProvider { + private final CassandraDatabase mDb; + private final String mCF; + private final Class mClazz; + + public CassandraCollectionProvider(CassandraDatabase db, String columnFamily, Class clazz) { + mDb = db; + mCF = columnFamily; + mClazz = clazz; + } + + @Override + public V get(String collection, String key) throws IOException { + String blob = mDb.getKey(mCF, collection, key); + return decode(blob); + } + + @Override + public Map query(String collection) throws IOException { + return query(collection, -1); + } + + @Override + public Map query(String collection, int limit) throws IOException { + Map result = new LinkedHashMap<>(); + + ColumnList row = mDb.getRow(mCF, collection); + if (!row.isEmpty()) { + int count = 0; + for (Column c : row) { + if (limit >= 0 && ++count > limit) { + break; // Limit reached. + } + + String key = c.getName(); + String blob = c.getStringValue(); + V obj = decode(blob); + + result.put(key, obj); + } + } + + return Collections.unmodifiableMap(result); + } + + @Override + public void put(String collection, String key, V obj) throws IOException { + String blob = encode(obj); + mDb.putKey(mCF, collection, key, blob); + } + + /** + * Encode the object as JSON. + * + * @param obj The object to encode. + * @return The JSON encoding of obj. + * @throws IOException if the object cannot be encoded. + */ + protected String encode(V obj) throws IOException { + if (mClazz == String.class) { + return (String) obj; + } else { + return JsonEncodedProvider.MAPPER.writeValueAsString(obj); + } + } + + /** + * Decode the JSON string as an object. + * + * @param blob The JSON data to decode. + * @return The decoded object or null if blob is null. + * @throws IOException If an object cannot be decoded. + */ + protected V decode(String blob) throws IOException { + if (blob == null) { + return null; + } + + if (mClazz == String.class) { + return (V) blob; + } + + V obj = JsonEncodedProvider.MAPPER.readValue(blob, mClazz); + return obj; + } +} diff --git a/src/main/java/com/p4square/grow/backend/db/CassandraDatabase.java b/src/main/java/com/p4square/grow/backend/db/CassandraDatabase.java new file mode 100644 index 0000000..b8cb6df --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/db/CassandraDatabase.java @@ -0,0 +1,212 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import com.netflix.astyanax.AstyanaxContext; +import com.netflix.astyanax.connectionpool.exceptions.ConnectionException; +import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl; +import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor; +import com.netflix.astyanax.connectionpool.NodeDiscoveryType; +import com.netflix.astyanax.connectionpool.OperationResult; +import com.netflix.astyanax.impl.AstyanaxConfigurationImpl; +import com.netflix.astyanax.Keyspace; +import com.netflix.astyanax.ColumnMutation; +import com.netflix.astyanax.model.Column; +import com.netflix.astyanax.model.ColumnFamily; +import com.netflix.astyanax.model.ColumnList; +import com.netflix.astyanax.ColumnListMutation; +import com.netflix.astyanax.MutationBatch; +import com.netflix.astyanax.serializers.StringSerializer; +import com.netflix.astyanax.thrift.ThriftFamilyFactory; + +import org.apache.log4j.Logger; + +/** + * Cassandra Database Abstraction for the Backend. + * + * @author Jesse Morgan + */ +public class CassandraDatabase { + private static Logger cLog = Logger.getLogger(CassandraDatabase.class); + + // Configuration fields. + private String mClusterName; + private String mKeyspaceName; + private String mSeedEndpoint = "127.0.0.1:9160"; + private int mPort = 9160; + + private AstyanaxContext mContext; + private Keyspace mKeyspace; + + /** + * Connect to Cassandra. + * + * Cluster and Keyspace must be set before calling init(). + */ + public void init() { + mContext = new AstyanaxContext.Builder() + .forCluster(mClusterName) + .forKeyspace(mKeyspaceName) + .withAstyanaxConfiguration(new AstyanaxConfigurationImpl() + .setDiscoveryType(NodeDiscoveryType.RING_DESCRIBE) + ) + .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("MyConnectionPool") + .setPort(mPort) + .setMaxConnsPerHost(1) + .setSeeds(mSeedEndpoint) + ) + .withConnectionPoolMonitor(new CountingConnectionPoolMonitor()) + .buildKeyspace(ThriftFamilyFactory.getInstance()); + + mContext.start(); + mKeyspace = mContext.getClient(); + } + + /** + * Close the database connection. + */ + public void close() { + mContext.shutdown(); + } + + /** + * Set the cluster name to connect to. + */ + public void setClusterName(final String cluster) { + mClusterName = cluster; + } + + /** + * Set the name of the keyspace to open. + */ + public void setKeyspaceName(final String keyspace) { + mKeyspaceName = keyspace; + } + + /** + * Change the seed endpoint. + * The default is 127.0.0.1:9160. + */ + public void setSeedEndpoint(final String endpoint) { + mSeedEndpoint = endpoint; + } + + /** + * Change the port to connect to. + * The default is 9160. + */ + public void setPort(final int port) { + mPort = port; + } + + /** + * @return The entire row associated with this key. + */ + public ColumnList getRow(final String cfName, final String key) { + try { + ColumnFamily cf = new ColumnFamily(cfName, + StringSerializer.get(), + StringSerializer.get()); + + OperationResult> result = + mKeyspace.prepareQuery(cf) + .getKey(key) + .execute(); + + return result.getResult(); + + } catch (ConnectionException e) { + cLog.error("getRow failed due to Connection Exception", e); + throw new RuntimeException(e); + } + } + + /** + * @return The value associated with the given key. + */ + public String getKey(final String cfName, final String key) { + return getKey(cfName, key, "value"); + } + + /** + * @return The value associated with the given key, column pair. + */ + public String getKey(final String cfName, final String key, final String column) { + final ColumnList row = getRow(cfName, key); + + if (row != null) { + final Column rowColumn = row.getColumnByName(column); + if (rowColumn != null) { + return rowColumn.getStringValue(); + } + } + + return null; + } + + /** + * Assign value to key. + */ + public void putKey(final String cfName, final String key, final String value) { + putKey(cfName, key, "value", value); + } + + /** + * Assign value to the key, column pair. + */ + public void putKey(final String cfName, final String key, + final String column, final String value) { + + ColumnFamily cf = new ColumnFamily(cfName, + StringSerializer.get(), + StringSerializer.get()); + + MutationBatch m = mKeyspace.prepareMutationBatch(); + m.withRow(cf, key).putColumn(column, value); + + try { + m.execute(); + } catch (ConnectionException e) { + cLog.error("putKey failed due to Connection Exception", e); + throw new RuntimeException(e); + } + } + + /** + * Remove a key, column pair. + */ + public void deleteKey(final String cfName, final String key, final String column) { + ColumnFamily cf = new ColumnFamily(cfName, + StringSerializer.get(), + StringSerializer.get()); + + try { + ColumnMutation m = mKeyspace.prepareColumnMutation(cf, key, column); + m.deleteColumn().execute(); + } catch (ConnectionException e) { + cLog.error("deleteKey failed due to Connection Exception", e); + throw new RuntimeException(e); + } + } + + /** + * Remove a row + */ + public void deleteRow(final String cfName, final String key) { + ColumnFamily cf = new ColumnFamily(cfName, + StringSerializer.get(), + StringSerializer.get()); + + try { + MutationBatch batch = mKeyspace.prepareMutationBatch(); + ColumnListMutation cfm = batch.withRow(cf, key).delete(); + batch.execute(); + + } catch (ConnectionException e) { + cLog.error("deleteRow failed due to Connection Exception", e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/db/CassandraKey.java b/src/main/java/com/p4square/grow/backend/db/CassandraKey.java new file mode 100644 index 0000000..853fe96 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/db/CassandraKey.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +/** + * CassandraKey represents a Cassandra key / column pair. + * + * @author Jesse Morgan + */ +public class CassandraKey { + private final String mColumnFamily; + private final String mId; + private final String mColumn; + + public CassandraKey(String columnFamily, String id, String column) { + mColumnFamily = columnFamily; + mId = id; + mColumn = column; + } + + public String getColumnFamily() { + return mColumnFamily; + } + + public String getId() { + return mId; + } + + public String getColumn() { + return mColumn; + } +} diff --git a/src/main/java/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/main/java/com/p4square/grow/backend/db/CassandraProviderImpl.java new file mode 100644 index 0000000..da5a9f2 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/db/CassandraProviderImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Provider implementation backed by a Cassandra ColumnFamily. + * + * @author Jesse Morgan + */ +public class CassandraProviderImpl extends JsonEncodedProvider implements Provider { + private final CassandraDatabase mDb; + + public CassandraProviderImpl(CassandraDatabase db, Class clazz) { + super(clazz); + + mDb = db; + } + + @Override + public V get(CassandraKey key) throws IOException { + String blob = mDb.getKey(key.getColumnFamily(), key.getId(), key.getColumn()); + return decode(blob); + } + + @Override + public void put(CassandraKey key, V obj) throws IOException { + String blob = encode(obj); + mDb.putKey(key.getColumnFamily(), key.getId(), key.getColumn(), blob); + } +} diff --git a/src/main/java/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java b/src/main/java/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java new file mode 100644 index 0000000..4face52 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.TrainingRecord; + +import com.p4square.grow.provider.JsonEncodedProvider; +import com.p4square.grow.provider.Provider; + +/** + * + * @author Jesse Morgan + */ +public class CassandraTrainingRecordProvider implements Provider { + private static final CassandraKey DEFAULT_PLAYLIST_KEY = new CassandraKey("strings", "defaultPlaylist", "value"); + + private static final String COLUMN_FAMILY = "training"; + private static final String PLAYLIST_KEY = "playlist"; + private static final String LAST_VIDEO_KEY = "lastVideo"; + + private final CassandraDatabase mDb; + private final Provider mPlaylistProvider; + + public CassandraTrainingRecordProvider(CassandraDatabase db) { + mDb = db; + mPlaylistProvider = new CassandraProviderImpl<>(db, Playlist.class); + } + + @Override + public TrainingRecord get(String userid) throws IOException { + Playlist playlist = mPlaylistProvider.get(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY)); + + if (playlist == null) { + // We consider no playlist to mean no record whatsoever. + return null; + } + + TrainingRecord r = new TrainingRecord(); + r.setPlaylist(playlist); + r.setLastVideo(mDb.getKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY)); + + return r; + } + + @Override + public void put(String userid, TrainingRecord record) throws IOException { + String lastVideo = record.getLastVideo(); + Playlist playlist = record.getPlaylist(); + + mDb.putKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY, lastVideo); + mPlaylistProvider.put(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY), playlist); + } + + /** + * @return the default playlist stored in the database. + */ + public Playlist getDefaultPlaylist() throws IOException { + Playlist playlist = mPlaylistProvider.get(DEFAULT_PLAYLIST_KEY); + + if (playlist == null) { + playlist = new Playlist(); + } + + return playlist; + } +} diff --git a/src/main/java/com/p4square/grow/backend/dynamo/DbTool.java b/src/main/java/com/p4square/grow/backend/dynamo/DbTool.java new file mode 100644 index 0000000..374fa83 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/dynamo/DbTool.java @@ -0,0 +1,481 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend.dynamo; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import com.p4square.grow.backend.dynamo.DynamoDatabase; +import com.p4square.grow.backend.dynamo.DynamoKey; +import com.p4square.grow.config.Config; +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.Provider; + +/** + * + * @author Jesse Morgan + */ +public class DbTool { + private static final FilenameFilter JSON_FILTER = new JsonFilter(); + + private static Config mConfig; + private static DynamoDatabase mDatabase; + + public static void usage() { + System.out.println("java com.p4square.grow.backend.dynamo.DbTool ...\n"); + System.out.println("Commands:"); + System.out.println("\t--domain Set config domain"); + System.out.println("\t--dev Set config domain to dev"); + System.out.println("\t--config Merge in config file"); + System.out.println("\t--list List all tables"); + System.out.println("\t--create
Create a table"); + System.out.println("\t--update
Update table throughput"); + System.out.println("\t--drop
Delete a table"); + System.out.println("\t--get
Get a value"); + System.out.println("\t--put
Put a value"); + System.out.println("\t--delete
Delete a value"); + System.out.println("\t--scan
List all rows"); + System.out.println("\t--scanf
List all rows"); + System.out.println(); + System.out.println("Bootstrap Commands:"); + System.out.println("\t--bootstrap Create all tables and import all data"); + System.out.println("\t--loadStrings Load all videos and questions"); + System.out.println("\t--destroy Drop all tables"); + System.out.println("\t--addadmin Add a backend account"); + System.out.println("\t--import
Backfill a table"); + } + + public static void main(String... args) { + if (args.length == 0) { + usage(); + System.exit(1); + } + + mConfig = new Config(); + + try { + mConfig.updateConfig(DbTool.class.getResourceAsStream("/grow.properties")); + + int offset = 0; + while (offset < args.length) { + if ("--domain".equals(args[offset])) { + mConfig.setDomain(args[offset + 1]); + mDatabase = null; + offset += 2; + + } else if ("--dev".equals(args[offset])) { + mConfig.setDomain("dev"); + mDatabase = null; + offset += 1; + + } else if ("--config".equals(args[offset])) { + mConfig.updateConfig(args[offset + 1]); + mDatabase = null; + offset += 2; + + } else if ("--list".equals(args[offset])) { + //offset = list(args, ++offset); + + } else if ("--create".equals(args[offset])) { + offset = create(args, ++offset); + + } else if ("--update".equals(args[offset])) { + offset = update(args, ++offset); + + } else if ("--drop".equals(args[offset])) { + offset = drop(args, ++offset); + + } else if ("--get".equals(args[offset])) { + offset = get(args, ++offset); + + } else if ("--put".equals(args[offset])) { + offset = put(args, ++offset); + + } else if ("--delete".equals(args[offset])) { + offset = delete(args, ++offset); + + } else if ("--scan".equals(args[offset])) { + offset = scan(args, ++offset); + + } else if ("--scanf".equals(args[offset])) { + offset = scanf(args, ++offset); + + /* Bootstrap Commands */ + } else if ("--bootstrap".equals(args[offset])) { + if ("dev".equals(mConfig.getDomain())) { + offset = bootstrapDevTables(args, ++offset); + } else { + offset = bootstrapTables(args, ++offset); + } + offset = loadStrings(args, offset); + + } else if ("--loadStrings".equals(args[offset])) { + offset = loadStrings(args, ++offset); + + } else if ("--destroy".equals(args[offset])) { + offset = destroy(args, ++offset); + + } else if ("--addadmin".equals(args[offset])) { + offset = addAdmin(args, ++offset); + + } else if ("--import".equals(args[offset])) { + offset = importTable(args, ++offset); + + } else { + throw new IllegalArgumentException("Unknown command " + args[offset]); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(2); + } + } + + private static DynamoDatabase getDatabase() { + if (mDatabase == null) { + mDatabase = new DynamoDatabase(mConfig); + } + + return mDatabase; + } + + private static int create(String[] args, int offset) { + String name = args[offset++]; + long reads = Long.parseLong(args[offset++]); + long writes = Long.parseLong(args[offset++]); + + DynamoDatabase db = getDatabase(); + + db.createTable(name, reads, writes); + + return offset; + } + + private static int update(String[] args, int offset) { + String name = args[offset++]; + long reads = Long.parseLong(args[offset++]); + long writes = Long.parseLong(args[offset++]); + + DynamoDatabase db = getDatabase(); + + db.updateTable(name, reads, writes); + + return offset; + } + + private static int drop(String[] args, int offset) { + String name = args[offset++]; + + DynamoDatabase db = getDatabase(); + + db.deleteTable(name); + + return offset; + } + + private static int get(String[] args, int offset) { + String table = args[offset++]; + String key = args[offset++]; + String attribute = args[offset++]; + + DynamoDatabase db = getDatabase(); + + String value = db.getAttribute(DynamoKey.newAttributeKey(table, key, attribute)); + + if (value == null) { + value = ""; + } + + System.out.printf("%s %s:%s\n%s\n\n", table, key, attribute, value); + + return offset; + } + + private static int put(String[] args, int offset) { + String table = args[offset++]; + String key = args[offset++]; + String attribute = args[offset++]; + String value = args[offset++]; + + DynamoDatabase db = getDatabase(); + + db.putAttribute(DynamoKey.newAttributeKey(table, key, attribute), value); + + return offset; + } + + private static int delete(String[] args, int offset) { + String table = args[offset++]; + String key = args[offset++]; + String attribute = args[offset++]; + + DynamoDatabase db = getDatabase(); + + db.deleteAttribute(DynamoKey.newAttributeKey(table, key, attribute)); + + System.out.printf("Deleted %s %s:%s\n\n", table, key, attribute); + + return offset; + } + + private static int scan(String[] args, int offset) { + String table = args[offset++]; + + DynamoKey key = DynamoKey.newKey(table, null); + + doScan(key); + + return offset; + } + + private static int scanf(String[] args, int offset) { + String table = args[offset++]; + String attribute = args[offset++]; + + DynamoKey key = DynamoKey.newAttributeKey(table, null, attribute); + + doScan(key); + + return offset; + } + + private static void doScan(DynamoKey key) { + DynamoDatabase db = getDatabase(); + + String attributeFilter = key.getAttribute(); + + while (key != null) { + Map> result = db.getAll(key); + + key = null; // If there are no results, exit + + for (Map.Entry> entry : result.entrySet()) { + key = entry.getKey(); // Save the last key + + for (Map.Entry attribute : entry.getValue().entrySet()) { + if (attributeFilter == null || attributeFilter.equals(attribute.getKey())) { + String keyString = key.getHashKey(); + if (key.getRangeKey() != null) { + keyString += "(" + key.getRangeKey() + ")"; + } + System.out.printf("%s %s:%s\n%s\n\n", + key.getTable(), keyString, attribute.getKey(), + attribute.getValue()); + } + } + } + } + } + + + private static int bootstrapTables(String[] args, int offset) { + DynamoDatabase db = getDatabase(); + + db.createTable("strings", 5, 1); + db.createTable("accounts", 5, 1); + db.createTable("assessments", 5, 5); + db.createTable("training", 5, 5); + db.createTable("feedthreads", 5, 1); + db.createTable("feedmessages", 5, 1); + + return offset; + } + + private static int bootstrapDevTables(String[] args, int offset) { + DynamoDatabase db = getDatabase(); + + db.createTable("strings", 1, 1); + db.createTable("accounts", 1, 1); + db.createTable("assessments", 1, 1); + db.createTable("training", 1, 1); + db.createTable("feedthreads", 1, 1); + db.createTable("feedmessages", 1, 1); + + return offset; + } + + private static int loadStrings(String[] args, int offset) throws IOException { + String data = args[offset++]; + File baseDir = new File(data); + + DynamoDatabase db = getDatabase(); + + insertQuestions(baseDir); + insertVideos(baseDir); + insertDefaultPlaylist(baseDir); + + return offset; + } + + private static int destroy(String[] args, int offset) { + DynamoDatabase db = getDatabase(); + + final String[] tables = { "strings", + "accounts", + "assessments", + "training", + "feedthreads", + "feedmessages" + }; + + for (String table : tables) { + try { + db.deleteTable(table); + } catch (Exception e) { + System.err.println("Deleting " + table + ": " + e.getMessage()); + } + } + + return offset; + } + + private static int addAdmin(String[] args, int offset) throws IOException { + String user = args[offset++]; + String pass = args[offset++]; + + DynamoDatabase db = getDatabase(); + + UserRecord record = new UserRecord(); + record.setId(user); + record.setBackendPassword(pass); + + Provider provider = new DynamoProviderImpl(db, UserRecord.class); + provider.put(DynamoKey.newAttributeKey("accounts", user, "value"), record); + + return offset; + } + + private static int importTable(String[] args, int offset) throws IOException { + String table = args[offset++]; + String filename = args[offset++]; + + DynamoDatabase db = getDatabase(); + + List lines = Files.readAllLines(new File(filename).toPath(), + StandardCharsets.UTF_8); + + int count = 0; + + String key = null; + Map attributes = new HashMap<>(); + for (String line : lines) { + if (line.length() == 0) { + if (attributes.size() > 0) { + db.putKey(DynamoKey.newKey(table, key), attributes); + count++; + + if (count % 50 == 0) { + System.out.printf("Imported %d records into %s...\n", count, table); + } + } + key = null; + attributes = new HashMap<>(); + continue; + } + + if (key == null) { + key = line; + continue; + } + + int space = line.indexOf(' '); + String attribute = line.substring(0, space); + String value = line.substring(space + 1); + + attributes.put(attribute, value); + } + + // Finish up the remaining attributes. + if (key != null && attributes.size() > 0) { + db.putKey(DynamoKey.newKey(table, key), attributes); + count++; + } + + System.out.printf("Imported %d records into %s.\n", count, table); + + return offset; + } + + private static void insertQuestions(File baseDir) throws IOException { + DynamoDatabase db = getDatabase(); + File questions = new File(baseDir, "questions"); + + File[] files = questions.listFiles(JSON_FILTER); + Arrays.sort(files); + + for (File file : files) { + String filename = file.getName(); + String questionId = filename.substring(0, filename.lastIndexOf('.')); + + byte[] encoded = Files.readAllBytes(file.toPath()); + String value = new String(encoded, StandardCharsets.UTF_8); + db.putAttribute(DynamoKey.newAttributeKey("strings", + "/questions/" + questionId, "value"), value); + System.out.println("Inserted /questions/" + questionId); + } + + String filename = files[0].getName(); + String first = filename.substring(0, filename.lastIndexOf('.')); + int count = files.length; + String summary = "{\"first\": \"" + first + "\", \"count\": " + count + "}"; + db.putAttribute(DynamoKey.newAttributeKey("strings", "/questions", "value"), summary); + System.out.println("Inserted /questions"); + } + + private static void insertVideos(File baseDir) throws IOException { + DynamoDatabase db = getDatabase(); + File videos = new File(baseDir, "videos"); + + for (File topic : videos.listFiles()) { + if (!topic.isDirectory()) { + continue; + } + + String topicName = topic.getName(); + + Map attributes = new HashMap<>(); + File[] files = topic.listFiles(JSON_FILTER); + for (File file : files) { + String filename = file.getName(); + String videoId = filename.substring(0, filename.lastIndexOf('.')); + + byte[] encoded = Files.readAllBytes(file.toPath()); + String value = new String(encoded, StandardCharsets.UTF_8); + + attributes.put(videoId, value); + System.out.println("Found /training/" + topicName + ":" + videoId); + } + + db.putKey(DynamoKey.newKey("strings", + "/training/" + topicName), attributes); + System.out.println("Inserted /training/" + topicName); + } + } + + private static void insertDefaultPlaylist(File baseDir) throws IOException { + DynamoDatabase db = getDatabase(); + File file = new File(baseDir, "videos/playlist.json"); + + byte[] encoded = Files.readAllBytes(file.toPath()); + String value = new String(encoded, StandardCharsets.UTF_8); + db.putAttribute(DynamoKey.newAttributeKey("strings", + "/training/defaultplaylist", "value"), value); + System.out.println("Inserted /training/defaultplaylist"); + } + + private static class JsonFilter implements FilenameFilter { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".json"); + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java b/src/main/java/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java new file mode 100644 index 0000000..b53e9f7 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/dynamo/DynamoCollectionProviderImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend.dynamo; + +import java.io.IOException; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * + * @author Jesse Morgan + */ +public class DynamoCollectionProviderImpl implements CollectionProvider { + private final DynamoDatabase mDb; + private final String mTable; + private final Class mClazz; + + public DynamoCollectionProviderImpl(DynamoDatabase db, String table, Class clazz) { + mDb = db; + mTable = table; + mClazz = clazz; + } + + @Override + public V get(String collection, String key) throws IOException { + String blob = mDb.getAttribute(DynamoKey.newAttributeKey(mTable, collection, key)); + return decode(blob); + } + + @Override + public Map query(String collection) throws IOException { + return query(collection, -1); + } + + @Override + public Map query(String collection, int limit) throws IOException { + Map result = new LinkedHashMap<>(); + + Map row = mDb.getKey(DynamoKey.newKey(mTable, collection)); + if (row.size() > 0) { + int count = 0; + for (Map.Entry c : row.entrySet()) { + if (limit >= 0 && ++count > limit) { + break; // Limit reached. + } + + String key = c.getKey(); + String blob = c.getValue(); + V obj = decode(blob); + + result.put(key, obj); + } + } + + return Collections.unmodifiableMap(result); + } + + @Override + public void put(String collection, String key, V obj) throws IOException { + if (obj == null) { + mDb.deleteAttribute(DynamoKey.newAttributeKey(mTable, collection, key)); + } else { + String blob = encode(obj); + mDb.putAttribute(DynamoKey.newAttributeKey(mTable, collection, key), blob); + } + } + + /** + * Encode the object as JSON. + * + * @param obj The object to encode. + * @return The JSON encoding of obj. + * @throws IOException if the object cannot be encoded. + */ + protected String encode(V obj) throws IOException { + if (mClazz == String.class) { + return (String) obj; + } else { + return JsonEncodedProvider.MAPPER.writeValueAsString(obj); + } + } + + /** + * Decode the JSON string as an object. + * + * @param blob The JSON data to decode. + * @return The decoded object or null if blob is null. + * @throws IOException If an object cannot be decoded. + */ + protected V decode(String blob) throws IOException { + if (blob == null) { + return null; + } + + if (mClazz == String.class) { + return (V) blob; + } + + V obj = JsonEncodedProvider.MAPPER.readValue(blob, mClazz); + return obj; + } +} diff --git a/src/main/java/com/p4square/grow/backend/dynamo/DynamoDatabase.java b/src/main/java/com/p4square/grow/backend/dynamo/DynamoDatabase.java new file mode 100644 index 0000000..68a165d --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/dynamo/DynamoDatabase.java @@ -0,0 +1,307 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend.dynamo; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import com.amazonaws.services.dynamodbv2.model.AttributeAction; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.CreateTableResult; +import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteItemResult; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableResult; +import com.amazonaws.services.dynamodbv2.model.GetItemRequest; +import com.amazonaws.services.dynamodbv2.model.GetItemResult; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.PutItemRequest; +import com.amazonaws.services.dynamodbv2.model.PutItemResult; +import com.amazonaws.services.dynamodbv2.model.ScanRequest; +import com.amazonaws.services.dynamodbv2.model.ScanResult; +import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; +import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; +import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; +import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; + +import com.p4square.grow.config.Config; + +/** + * A wrapper around the Dynamo API. + */ +public class DynamoDatabase { + private final AmazonDynamoDBClient mClient; + private final String mTablePrefix; + + public DynamoDatabase(final Config config) { + AWSCredentials creds; + + String awsAccessKey = config.getString("awsAccessKey"); + if (awsAccessKey != null) { + creds = new AWSCredentials() { + @Override + public String getAWSAccessKeyId() { + return config.getString("awsAccessKey"); + } + @Override + public String getAWSSecretKey() { + return config.getString("awsSecretKey"); + } + }; + } else { + creds = new DefaultAWSCredentialsProviderChain().getCredentials(); + } + + mClient = new AmazonDynamoDBClient(creds); + + String endpoint = config.getString("dynamoEndpoint"); + if (endpoint != null) { + mClient.setEndpoint(endpoint); + } + + String region = config.getString("awsRegion"); + if (region != null) { + mClient.setRegion(Region.getRegion(Regions.fromName(region))); + } + + mTablePrefix = config.getString("dynamoTablePrefix", ""); + } + + public void createTable(String name, long reads, long writes) { + ArrayList attributeDefinitions = new ArrayList<>(); + attributeDefinitions.add(new AttributeDefinition() + .withAttributeName("id") + .withAttributeType("S")); + + ArrayList ks = new ArrayList<>(); + ks.add(new KeySchemaElement().withAttributeName("id").withKeyType(KeyType.HASH)); + + ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput() + .withReadCapacityUnits(reads) + .withWriteCapacityUnits(writes); + + CreateTableRequest request = new CreateTableRequest() + .withTableName(mTablePrefix + name) + .withAttributeDefinitions(attributeDefinitions) + .withKeySchema(ks) + .withProvisionedThroughput(provisionedThroughput); + + CreateTableResult result = mClient.createTable(request); + } + + public void updateTable(String name, long reads, long writes) { + ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput() + .withReadCapacityUnits(reads) + .withWriteCapacityUnits(writes); + + UpdateTableRequest request = new UpdateTableRequest() + .withTableName(mTablePrefix + name) + .withProvisionedThroughput(provisionedThroughput); + + UpdateTableResult result = mClient.updateTable(request); + } + + public void deleteTable(String name) { + DeleteTableRequest deleteTableRequest = new DeleteTableRequest() + .withTableName(mTablePrefix + name); + + DeleteTableResult result = mClient.deleteTable(deleteTableRequest); + } + + /** + * Get all rows from a table. + * + * The key parameter must specify a table. If hash/range key is specified, + * the scan will begin after that key. + * + * @param key Previous key to start with. + * @return An ordered map of all results. + */ + public Map> getAll(final DynamoKey key) { + ScanRequest scanRequest = new ScanRequest().withTableName(mTablePrefix + key.getTable()); + + if (key.getHashKey() != null) { + scanRequest.setExclusiveStartKey(generateKey(key)); + } + + ScanResult scanResult = mClient.scan(scanRequest); + + Map> result = new LinkedHashMap<>(); + for (Map map : scanResult.getItems()) { + String id = null; + String range = null; + Map row = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if ("id".equals(entry.getKey())) { + id = entry.getValue().getS(); + } else if ("range".equals(entry.getKey())) { + range = entry.getValue().getS(); + } else { + row.put(entry.getKey(), entry.getValue().getS()); + } + } + result.put(DynamoKey.newRangeKey(key.getTable(), id, range), row); + } + + return result; + } + + public Map getKey(final DynamoKey key) { + GetItemRequest getItemRequest = new GetItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withKey(generateKey(key)); + + GetItemResult getItemResult = mClient.getItem(getItemRequest); + Map map = getItemResult.getItem(); + + Map result = new LinkedHashMap<>(); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + if (!"id".equals(entry.getKey())) { + result.put(entry.getKey(), entry.getValue().getS()); + } + } + } + + return result; + } + + public String getAttribute(final DynamoKey key) { + checkAttributeKey(key); + + GetItemRequest getItemRequest = new GetItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withKey(generateKey(key)) + .withAttributesToGet(key.getAttribute()); + + GetItemResult result = mClient.getItem(getItemRequest); + Map map = result.getItem(); + + if (map == null) { + return null; + } + + AttributeValue value = map.get(key.getAttribute()); + if (value != null) { + return value.getS(); + + } else { + return null; + } + } + + /** + * Set all attributes for the given key. + * + * @param key The key. + * @param values Map of attributes to values. + */ + public void putKey(final DynamoKey key, final Map values) { + Map item = new HashMap<>(); + for (Map.Entry entry : values.entrySet()) { + item.put(entry.getKey(), new AttributeValue().withS(entry.getValue())); + } + + // Set the Key + item.putAll(generateKey(key)); + + PutItemRequest putItemRequest = new PutItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withItem(item); + + PutItemResult result = mClient.putItem(putItemRequest); + } + + /** + * Set the particular attributes of the given key. + * + * @param key The key. + * @param value The new value. + */ + public void putAttribute(final DynamoKey key, final String value) { + checkAttributeKey(key); + + Map updateItem = new HashMap<>(); + updateItem.put(key.getAttribute(), + new AttributeValueUpdate() + .withAction(AttributeAction.PUT) + .withValue(new AttributeValue().withS(value))); + + UpdateItemRequest updateItemRequest = new UpdateItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withKey(generateKey(key)) + .withAttributeUpdates(updateItem); + // TODO: Check conditions. + + UpdateItemResult result = mClient.updateItem(updateItemRequest); + } + + /** + * Delete the given key. + * + * @param key The key. + */ + public void deleteKey(final DynamoKey key) { + DeleteItemRequest deleteItemRequest = new DeleteItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withKey(generateKey(key)); + + DeleteItemResult result = mClient.deleteItem(deleteItemRequest); + } + + /** + * Delete an attribute from the given key. + * + * @param key The key. + */ + public void deleteAttribute(final DynamoKey key) { + checkAttributeKey(key); + + Map updateItem = new HashMap<>(); + updateItem.put(key.getAttribute(), + new AttributeValueUpdate().withAction(AttributeAction.DELETE)); + + UpdateItemRequest updateItemRequest = new UpdateItemRequest() + .withTableName(mTablePrefix + key.getTable()) + .withKey(generateKey(key)) + .withAttributeUpdates(updateItem); + + UpdateItemResult result = mClient.updateItem(updateItemRequest); + } + + /** + * Generate a DynamoDB Key Map from the DynamoKey. + */ + private Map generateKey(final DynamoKey key) { + HashMap keyMap = new HashMap<>(); + keyMap.put("id", new AttributeValue().withS(key.getHashKey())); + + String range = key.getRangeKey(); + if (range != null) { + keyMap.put("range", new AttributeValue().withS(range)); + } + + return keyMap; + } + + private void checkAttributeKey(DynamoKey key) { + if (null == key.getAttribute()) { + throw new IllegalArgumentException("Attribute must be non-null"); + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/dynamo/DynamoKey.java b/src/main/java/com/p4square/grow/backend/dynamo/DynamoKey.java new file mode 100644 index 0000000..5cdbacd --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/dynamo/DynamoKey.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend.dynamo; + +/** + * DynamoKey represents a table, hash key, and range key tupl. + */ +public class DynamoKey { + private final String mTable; + private final String mHashKey; + private final String mRangeKey; + private final String mAttribute; + + public static DynamoKey newKey(final String table, final String hashKey) { + return new DynamoKey(table, hashKey, null, null); + } + + public static DynamoKey newRangeKey(final String table, final String hashKey, + final String rangeKey) { + + return new DynamoKey(table, hashKey, rangeKey, null); + } + + public static DynamoKey newAttributeKey(final String table, final String hashKey, + final String attribute) { + + return new DynamoKey(table, hashKey, null, attribute); + } + + public DynamoKey(final String table, final String hashKey, final String rangeKey, + final String attribute) { + + mTable = table; + mHashKey = hashKey; + mRangeKey = rangeKey; + mAttribute = attribute; + } + + public String getTable() { + return mTable; + } + + public String getHashKey() { + return mHashKey; + } + + public String getRangeKey() { + return mRangeKey; + } + + public String getAttribute() { + return mAttribute; + } +} diff --git a/src/main/java/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java b/src/main/java/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java new file mode 100644 index 0000000..93a535f --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/dynamo/DynamoProviderImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.dynamo; + +import java.io.IOException; + +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Provider implementation backed by a DynamoDB Table. + * + * @author Jesse Morgan + */ +public class DynamoProviderImpl extends JsonEncodedProvider implements Provider { + private final DynamoDatabase mDb; + + public DynamoProviderImpl(DynamoDatabase db, Class clazz) { + super(clazz); + + mDb = db; + } + + @Override + public V get(DynamoKey key) throws IOException { + String blob = mDb.getAttribute(key); + return decode(blob); + } + + @Override + public void put(DynamoKey key, V obj) throws IOException { + String blob = encode(obj); + mDb.putAttribute(key, blob); + } +} diff --git a/src/main/java/com/p4square/grow/backend/feed/FeedDataProvider.java b/src/main/java/com/p4square/grow/backend/feed/FeedDataProvider.java new file mode 100644 index 0000000..6f090c0 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/feed/FeedDataProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.feed; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import com.p4square.grow.model.MessageThread; +import com.p4square.grow.model.Message; +import com.p4square.grow.provider.CollectionProvider; + +/** + * Implementing this interface indicates you can provide a data source for the Feed. + * + * @author Jesse Morgan + */ +public interface FeedDataProvider { + public static final Collection TOPICS = Collections.unmodifiableCollection( + Arrays.asList(new String[] { "seeker", "believer", "disciple", "teacher", "leader" })); + + /** + * @return a CollectionProvider of Threads. + */ + CollectionProvider getThreadProvider(); + + /** + * @return a CollectionProvider of Messages. + */ + CollectionProvider getMessageProvider(); +} diff --git a/src/main/java/com/p4square/grow/backend/feed/ThreadResource.java b/src/main/java/com/p4square/grow/backend/feed/ThreadResource.java new file mode 100644 index 0000000..e8f46c2 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/feed/ThreadResource.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.feed; + +import java.io.IOException; + +import java.util.Date; +import java.util.Map; + +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; + +import org.restlet.ext.jackson.JacksonRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.model.Message; + +/** + * ThreadResource manages the messages that make up a thread. + * + * @author Jesse Morgan + */ +public class ThreadResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(ThreadResource.class); + + private FeedDataProvider mBackend; + private String mTopic; + private String mThreadId; + + @Override + public void doInit() { + super.doInit(); + + mBackend = (FeedDataProvider) getApplication(); + mTopic = getAttribute("topic"); + mThreadId = getAttribute("thread"); + } + + /** + * GET a list of messages in a thread. + */ + @Override + protected Representation get() { + // If the topic or threadId are missing, return a 404. + if (mTopic == null || mTopic.length() == 0 || + mThreadId == null || mThreadId.length() == 0) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + // TODO: Support limit query parameter. + + try { + String collectionKey = mTopic + "/" + mThreadId; + Map messages = mBackend.getMessageProvider().query(collectionKey); + return new JacksonRepresentation(messages.values()); + + } catch (IOException e) { + LOG.error("Unexpected exception: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } + + /** + * POST a new message to the thread. + */ + @Override + protected Representation post(Representation entity) { + // If the topic and thread are not provided, respond with not allowed. + // TODO: Check if the thread exists. + if (mTopic == null || !mBackend.TOPICS.contains(mTopic) || + mThreadId == null || mThreadId.length() == 0) { + setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + return null; + } + + try { + JacksonRepresentation jsonRep = new JacksonRepresentation(entity, Message.class); + Message message = jsonRep.getObject(); + + // Force the thread id and message to be what we expect. + message.setThreadId(mThreadId); + message.setId(Message.generateId()); + + if (message.getCreated() == null) { + message.setCreated(new Date()); + } + + String collectionKey = mTopic + "/" + mThreadId; + mBackend.getMessageProvider().put(collectionKey, message.getId(), message); + + setLocationRef(mThreadId + "/" + message.getId()); + return new JacksonRepresentation(message); + + } catch (IOException e) { + LOG.error("Unexpected exception: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/feed/TopicResource.java b/src/main/java/com/p4square/grow/backend/feed/TopicResource.java new file mode 100644 index 0000000..24b6a92 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/feed/TopicResource.java @@ -0,0 +1,117 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.feed; + +import java.io.IOException; + +import java.util.Date; +import java.util.Map; + +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; + +import org.restlet.ext.jackson.JacksonRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.model.Message; +import com.p4square.grow.model.MessageThread; + +/** + * TopicResource manages the threads contained in a topic. + * + * @author Jesse Morgan + */ +public class TopicResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(TopicResource.class); + + private FeedDataProvider mBackend; + private String mTopic; + + @Override + public void doInit() { + super.doInit(); + + mBackend = (FeedDataProvider) getApplication(); + mTopic = getAttribute("topic"); + } + + /** + * GET a list of threads in the topic. + */ + @Override + protected Representation get() { + // If no topic is provided, return a list of topics. + if (mTopic == null || mTopic.length() == 0) { + return new JacksonRepresentation(FeedDataProvider.TOPICS); + } + + // Parse limit query parameter. + int limit = -1; + String limitString = getQueryValue("limit"); + if (limitString != null) { + try { + limit = Integer.parseInt(limitString); + } catch (NumberFormatException e) { + setStatus(Status.CLIENT_ERROR_BAD_REQUEST); + return null; + } + } + + try { + Map threads = mBackend.getThreadProvider().query(mTopic, limit); + return new JacksonRepresentation(threads.values()); + + } catch (IOException e) { + LOG.error("Unexpected exception: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } + + /** + * POST a new thread to the topic. + */ + @Override + protected Representation post(Representation entity) { + // If no topic is provided, respond with not allowed. + if (mTopic == null || !mBackend.TOPICS.contains(mTopic)) { + setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + return null; + } + + try { + // Deserialize the incoming message. + JacksonRepresentation jsonRep = + new JacksonRepresentation(entity, MessageThread.class); + + // Get the message from the request. + // Throw away the wrapping MessageThread because we'll create our own later. + Message message = jsonRep.getObject().getMessage(); + if (message.getCreated() == null) { + message.setCreated(new Date()); + } + + // Create the new thread. + MessageThread newThread = MessageThread.createNew(); + + // Force the thread id and message to be what we expect. + message.setId(Message.generateId()); + message.setThreadId(newThread.getId()); + newThread.setMessage(message); + + mBackend.getThreadProvider().put(mTopic, newThread.getId(), newThread); + + setLocationRef(mTopic + "/" + newThread.getId()); + return new JacksonRepresentation(newThread); + + } catch (IOException e) { + LOG.error("Unexpected exception: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/AccountResource.java b/src/main/java/com/p4square/grow/backend/resources/AccountResource.java new file mode 100644 index 0000000..2ac7061 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/AccountResource.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; + +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; + +import org.restlet.ext.jackson.JacksonRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesUserRecords; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Stores a document about a user. + * + * @author Jesse Morgan + */ +public class AccountResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(AccountResource.class); + + private Provider mUserRecordProvider; + + private String mUserId; + + @Override + public void doInit() { + super.doInit(); + + final ProvidesUserRecords backend = (ProvidesUserRecords) getApplication(); + mUserRecordProvider = backend.getUserRecordProvider(); + + mUserId = getAttribute("userId"); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + try { + UserRecord result = mUserRecordProvider.get(mUserId); + + if (result == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + JacksonRepresentation rep = new JacksonRepresentation(result); + rep.setObjectMapper(JsonEncodedProvider.MAPPER); + return rep; + + } catch (IOException e) { + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + try { + JacksonRepresentation representation = + new JacksonRepresentation<>(entity, UserRecord.class); + representation.setObjectMapper(JsonEncodedProvider.MAPPER); + UserRecord record = representation.getObject(); + + mUserRecordProvider.put(mUserId, record); + setStatus(Status.SUCCESS_NO_CONTENT); + + } catch (IOException e) { + setStatus(Status.SERVER_ERROR_INTERNAL); + } + + return null; + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/BannerResource.java b/src/main/java/com/p4square/grow/backend/resources/BannerResource.java new file mode 100644 index 0000000..2b9c8e6 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/BannerResource.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; + +import org.restlet.data.Status; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; +import org.restlet.resource.ServerResource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.model.Banner; +import com.p4square.grow.provider.JsonEncodedProvider; +import com.p4square.grow.provider.Provider; + +/** + * Fetches or sets the banner string. + * + * @author Jesse Morgan + */ +public class BannerResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(BannerResource.class); + + public static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; + + private Provider mStringProvider; + + @Override + public void doInit() { + super.doInit(); + + final GrowBackend backend = (GrowBackend) getApplication(); + mStringProvider = backend.getStringProvider(); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = null; + try { + result = mStringProvider.get("banner"); + + } catch (IOException e) { + LOG.warn("Exception loading banner: " + e); + } + + if (result == null || result.length() == 0) { + result = "{\"html\":null}"; + } + + return new StringRepresentation(result); + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + try { + JacksonRepresentation representation = + new JacksonRepresentation<>(entity, Banner.class); + representation.setObjectMapper(MAPPER); + + Banner banner = representation.getObject(); + + mStringProvider.put("banner", MAPPER.writeValueAsString(banner)); + setStatus(Status.SUCCESS_NO_CONTENT); + + } catch (IOException e) { + setStatus(Status.SERVER_ERROR_INTERNAL); + } + + return null; + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/SurveyResource.java b/src/main/java/com/p4square/grow/backend/resources/SurveyResource.java new file mode 100644 index 0000000..8723ee2 --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/SurveyResource.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; + +import java.util.Map; +import java.util.HashMap; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; +import org.restlet.resource.ServerResource; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.model.Question; +import com.p4square.grow.provider.JsonEncodedProvider; +import com.p4square.grow.provider.Provider; + +/** + * This resource manages assessment questions. + * + * @author Jesse Morgan + */ +public class SurveyResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(SurveyResource.class); + + private static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; + + private Provider mQuestionProvider; + private Provider mStringProvider; + + private String mQuestionId; + + @Override + public void doInit() { + super.doInit(); + + final GrowBackend backend = (GrowBackend) getApplication(); + mQuestionProvider = backend.getQuestionProvider(); + mStringProvider = backend.getStringProvider(); + + mQuestionId = getAttribute("questionId"); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = "{}"; + + if (mQuestionId == null) { + // TODO: List all question ids + + } else if (mQuestionId.equals("first")) { + // Get the first question id from db? + Map questionSummary = getQuestionsSummary(); + mQuestionId = (String) questionSummary.get("first"); + + } else if (mQuestionId.equals("count")) { + // Get the first question id from db? + Map questionSummary = getQuestionsSummary(); + + return new StringRepresentation("{\"count\":" + + String.valueOf((Integer) questionSummary.get("count")) + "}"); + } + + if (mQuestionId != null) { + // Get a question by id + Question question = null; + try { + question = mQuestionProvider.get(mQuestionId); + } catch (IOException e) { + LOG.error("IOException loading question: " + e); + } + + if (question == null) { + // 404 + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + JacksonRepresentation rep = new JacksonRepresentation<>(question); + rep.setObjectMapper(MAPPER); + return rep; + } + + return new StringRepresentation(result); + } + + private Map getQuestionsSummary() { + try { + // TODO: This could be better. Quick fix for provider support. + String json = mStringProvider.get("/questions"); + + if (json != null) { + return MAPPER.readValue(json, Map.class); + } + + } catch (IOException e) { + LOG.info("Exception reading questions summary.", e); + } + + return null; + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/main/java/com/p4square/grow/backend/resources/SurveyResultsResource.java new file mode 100644 index 0000000..7c15cfd --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/SurveyResultsResource.java @@ -0,0 +1,253 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.restlet.data.MediaType; +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.grow.backend.GrowBackend; +import com.p4square.grow.model.Answer; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.RecordedAnswer; +import com.p4square.grow.model.Score; +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.Provider; + + +/** + * Store the user's answers to the assessment and generate their score. + * + * @author Jesse Morgan + */ +public class SurveyResultsResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(SurveyResultsResource.class); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static enum RequestType { + ASSESSMENT, ANSWER + } + + private CollectionProvider mAnswerProvider; + private Provider mQuestionProvider; + private Provider mUserRecordProvider; + + private RequestType mRequestType; + private String mUserId; + private String mQuestionId; + + @Override + public void doInit() { + super.doInit(); + + final GrowBackend backend = (GrowBackend) getApplication(); + mAnswerProvider = backend.getAnswerProvider(); + mQuestionProvider = backend.getQuestionProvider(); + mUserRecordProvider = backend.getUserRecordProvider(); + + mUserId = getAttribute("userId"); + mQuestionId = getAttribute("questionId"); + + mRequestType = RequestType.ASSESSMENT; + if (mQuestionId != null) { + mRequestType = RequestType.ANSWER; + } + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + try { + String result = null; + + switch (mRequestType) { + case ANSWER: + result = mAnswerProvider.get(mUserId, mQuestionId); + break; + + case ASSESSMENT: + result = mAnswerProvider.get(mUserId, "summary"); + if (result == null || result.length() == 0) { + result = buildAssessment(); + } + break; + } + + if (result == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new StringRepresentation(result); + } catch (IOException e) { + LOG.error("IOException getting answer: ", e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + boolean success = false; + + switch (mRequestType) { + case ANSWER: + try { + mAnswerProvider.put(mUserId, mQuestionId, entity.getText()); + mAnswerProvider.put(mUserId, "lastAnswered", mQuestionId); + mAnswerProvider.put(mUserId, "summary", null); + success = true; + + } catch (Exception e) { + LOG.warn("Caught exception putting answer: " + e.getMessage(), e); + } + break; + + default: + setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + return null; + } + + if (success) { + setStatus(Status.SUCCESS_NO_CONTENT); + + } else { + setStatus(Status.SERVER_ERROR_INTERNAL); + } + + return null; + } + + /** + * Clear assessment results. + */ + @Override + protected Representation delete() { + boolean success = false; + + switch (mRequestType) { + case ANSWER: + try { + mAnswerProvider.put(mUserId, mQuestionId, null); + mAnswerProvider.put(mUserId, "summary", null); + success = true; + + } catch (Exception e) { + LOG.warn("Caught exception putting answer: " + e.getMessage(), e); + } + break; + + case ASSESSMENT: + try { + mAnswerProvider.put(mUserId, "summary", null); + mAnswerProvider.put(mUserId, "lastAnswered", null); + // TODO Delete answers + + UserRecord record = mUserRecordProvider.get(mUserId); + if (record != null) { + record.setLanding("assessment"); + mUserRecordProvider.put(mUserId, record); + } + + success = true; + + } catch (Exception e) { + LOG.warn("Caught exception putting answer: " + e.getMessage(), e); + } + break; + + default: + setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + return null; + } + + if (success) { + setStatus(Status.SUCCESS_NO_CONTENT); + + } else { + setStatus(Status.SERVER_ERROR_INTERNAL); + } + + return null; + + } + + /** + * This method compiles assessment results. + */ + private String buildAssessment() throws IOException { + StringBuilder sb = new StringBuilder("{ "); + + // Last question answered + final String lastAnswered = mAnswerProvider.get(mUserId, "lastAnswered"); + if (lastAnswered != null && lastAnswered.length() > 0) { + sb.append("\"lastAnswered\": \"" + lastAnswered + "\", "); + } + + // Compute score + Map row = mAnswerProvider.query(mUserId); + if (row.size() > 0) { + Score score = new Score(); + boolean scoringDone = false; + int totalAnswers = 0; + for (Map.Entry c : row.entrySet()) { + if (c.getKey().equals("lastAnswered") || c.getKey().equals("summary")) { + continue; + } + + try { + Question question = mQuestionProvider.get(c.getKey()); + RecordedAnswer userAnswer = MAPPER.readValue(c.getValue(), RecordedAnswer.class); + + if (question == null) { + LOG.warn("Answer for unknown question: " + c.getKey()); + continue; + } + + LOG.debug("Scoring questionId: " + c.getKey()); + scoringDone = !question.scoreAnswer(score, userAnswer); + + } catch (Exception e) { + LOG.error("Failed to score question: {userid: \"" + mUserId + + "\", questionid:\"" + c.getKey() + + "\", userAnswer:\"" + c.getValue() + "\"}", e); + } + + totalAnswers++; + } + + sb.append("\"score\":" + score.getScore()); + sb.append(", \"sum\":" + score.getSum()); + sb.append(", \"count\":" + score.getCount()); + sb.append(", \"totalAnswers\":" + totalAnswers); + sb.append(", \"result\":\"" + score.toString() + "\""); + } + + sb.append(" }"); + String summary = sb.toString(); + + // Persist summary + mAnswerProvider.put(mUserId, "summary", summary); + + return summary; + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/main/java/com/p4square/grow/backend/resources/TrainingRecordResource.java new file mode 100644 index 0000000..51ba56a --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/TrainingRecordResource.java @@ -0,0 +1,235 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.restlet.ext.jackson.JacksonRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; + +import com.p4square.grow.model.Chapter; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.VideoRecord; +import com.p4square.grow.model.TrainingRecord; + +import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.JsonEncodedProvider; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesAssessments; +import com.p4square.grow.provider.ProvidesTrainingRecords; + +import com.p4square.grow.model.Score; + +/** + * + * @author Jesse Morgan + */ +public class TrainingRecordResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(TrainingRecordResource.class); + private static final ObjectMapper MAPPER = JsonEncodedProvider.MAPPER; + + static enum RequestType { + SUMMARY, VIDEO + } + + private Provider mTrainingRecordProvider; + private CollectionProvider mAnswerProvider; + + private RequestType mRequestType; + private String mUserId; + private String mVideoId; + private TrainingRecord mRecord; + + @Override + public void doInit() { + super.doInit(); + + mTrainingRecordProvider = ((ProvidesTrainingRecords) getApplication()).getTrainingRecordProvider(); + mAnswerProvider = ((ProvidesAssessments) getApplication()).getAnswerProvider(); + + mUserId = getAttribute("userId"); + mVideoId = getAttribute("videoId"); + + try { + Playlist defaultPlaylist = ((ProvidesTrainingRecords) getApplication()).getDefaultPlaylist(); + + mRecord = mTrainingRecordProvider.get(mUserId); + if (mRecord == null) { + mRecord = new TrainingRecord(); + mRecord.setPlaylist(defaultPlaylist); + skipAssessedChapters(mUserId, mRecord); + } else { + // Merge the playlist with the most recent version. + mRecord.getPlaylist().merge(defaultPlaylist); + } + + } catch (IOException e) { + LOG.error("IOException loading TrainingRecord: " + e.getMessage(), e); + mRecord = null; + } + + mRequestType = RequestType.SUMMARY; + if (mVideoId != null) { + mRequestType = RequestType.VIDEO; + } + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + JacksonRepresentation rep = null; + + if (mRecord == null) { + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + + switch (mRequestType) { + case VIDEO: + VideoRecord video = mRecord.getPlaylist().find(mVideoId); + if (video == null) { + break; // Fall through and return 404 + } + rep = new JacksonRepresentation(video); + break; + + case SUMMARY: + rep = new JacksonRepresentation(mRecord); + break; + } + + if (rep == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + + } else { + rep.setObjectMapper(JsonEncodedProvider.MAPPER); + return rep; + } + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + if (mRecord == null) { + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + + switch (mRequestType) { + case VIDEO: + try { + JacksonRepresentation representation = + new JacksonRepresentation<>(entity, VideoRecord.class); + representation.setObjectMapper(JsonEncodedProvider.MAPPER); + VideoRecord update = representation.getObject(); + VideoRecord video = mRecord.getPlaylist().find(mVideoId); + + if (video == null) { + // TODO: Video isn't on their playlist... + LOG.warn("Skipping video completion for video missing from playlist."); + + } else if (update.getComplete() && !video.getComplete()) { + // Video was newly completed + video.complete(); + mRecord.setLastVideo(mVideoId); + + mTrainingRecordProvider.put(mUserId, mRecord); + } + + setStatus(Status.SUCCESS_NO_CONTENT); + + } catch (Exception e) { + LOG.warn("Caught exception updating training record: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + } + break; + + default: + setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + } + + return null; + } + + private Score getAssessedScore(String userId) throws IOException { + // Get the user's score. + Score assessedScore = new Score(0, 0); + + String summaryString = mAnswerProvider.get(userId, "summary"); + if (summaryString == null) { + throw new IOException("Asked to create training record for unassessed user " + userId); + } + + Map summary = MAPPER.readValue(summaryString, Map.class); + + if (summary.containsKey("sum") && summary.containsKey("count")) { + double sum = (Double) summary.get("sum"); + int count = (Integer) summary.get("count"); + assessedScore = new Score(sum, count); + } + + return assessedScore; + } + + /** + * Mark the chapters which the user assessed through as not required. + */ + private void skipAssessedChapters(String userId, TrainingRecord record) { + // Get the user's score. + Score assessedScore = new Score(0, 0); + + try { + assessedScore = getAssessedScore(userId); + } catch (IOException e) { + LOG.error("IOException fetching assessment record for " + userId, e); + return; + } + + // Mark the correct videos as not required. + Playlist playlist = record.getPlaylist(); + + for (Map.Entry entry : playlist.getChaptersMap().entrySet()) { + String chapterId = entry.getKey(); + Chapter chapter = entry.getValue(); + boolean required; + + if ("introduction".equals(chapter)) { + // Introduction chapter is always required + required = true; + + } else { + // Chapter required if the floor of the score is <= the chapter's numeric value. + required = assessedScore.floor() <= Score.numericScore(chapterId); + } + + if (!required) { + for (VideoRecord video : chapter.getVideos().values()) { + video.setRequired(required); + } + } + } + } +} diff --git a/src/main/java/com/p4square/grow/backend/resources/TrainingResource.java b/src/main/java/com/p4square/grow/backend/resources/TrainingResource.java new file mode 100644 index 0000000..6efdfab --- /dev/null +++ b/src/main/java/com/p4square/grow/backend/resources/TrainingResource.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.io.IOException; +import java.util.Map; + +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.backend.db.CassandraDatabase; + +import com.p4square.grow.provider.CollectionProvider; +/** + * This resource returns a listing of training items for a particular level. + * + * @author Jesse Morgan + */ +public class TrainingResource extends ServerResource { + private final static Logger LOG = Logger.getLogger(TrainingResource.class); + + private CollectionProvider mVideoProvider; + + private String mLevel; + private String mVideoId; + + @Override + public void doInit() { + super.doInit(); + + GrowBackend backend = (GrowBackend) getApplication(); + mVideoProvider = backend.getVideoProvider(); + + mLevel = getAttribute("level"); + mVideoId = getAttribute("videoId"); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = null; + + if (mLevel == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + try { + if (mVideoId == null) { + // Get all videos + // TODO: This could be improved, but this is the quickest way to get + // providers working. + Map videos = mVideoProvider.query(mLevel); + if (videos.size() > 0) { + StringBuilder sb = new StringBuilder("{ \"level\": \"" + mLevel + "\""); + sb.append(", \"videos\": ["); + boolean first = true; + for (String value : videos.values()) { + if (!first) { + sb.append(", "); + } + sb.append(value); + first = false; + } + sb.append("] }"); + result = sb.toString(); + } + + } else { + // Get single video + result = mVideoProvider.get(mLevel, mVideoId); + } + + if (result == null) { + // 404 + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new StringRepresentation(result); + + } catch (IOException e) { + LOG.error("IOException fetch video: " + e.getMessage(), e); + setStatus(Status.SERVER_ERROR_INTERNAL); + return null; + } + } +} diff --git a/src/main/java/com/p4square/grow/ccb/CCBProgressReporter.java b/src/main/java/com/p4square/grow/ccb/CCBProgressReporter.java new file mode 100644 index 0000000..d2826eb --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/CCBProgressReporter.java @@ -0,0 +1,104 @@ +package com.p4square.grow.ccb; + +import com.p4square.ccbapi.CCBAPI; +import com.p4square.ccbapi.model.*; +import com.p4square.grow.frontend.ProgressReporter; +import com.p4square.grow.model.Score; +import org.apache.log4j.Logger; +import org.restlet.security.User; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +/** + * A ProgressReporter which records progress in CCB. + * + * Except not really, because it's not implemented yet. + * This is just a placeholder until ccb-api-client-java has support for updating an individual. + */ +public class CCBProgressReporter implements ProgressReporter { + + private static final Logger LOG = Logger.getLogger(CCBProgressReporter.class); + + private static final String GROW_LEVEL = "GrowLevelTrain"; + private static final String GROW_ASSESSMENT = "GrowLevelAsmnt"; + + private final CCBAPI mAPI; + private final CustomFieldCache mCache; + + public CCBProgressReporter(final CCBAPI api, final CustomFieldCache cache) { + mAPI = api; + mCache = cache; + } + + @Override + public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) { + if (!(user instanceof CCBUser)) { + throw new IllegalArgumentException("Expected CCBUser but got " + user.getClass().getCanonicalName()); + } + final CCBUser ccbuser = (CCBUser) user; + + updateLevelAndDate(ccbuser, GROW_ASSESSMENT, level, date); + } + + @Override + public void reportChapterComplete(final User user, final String chapter, final Date date) { + if (!(user instanceof CCBUser)) { + throw new IllegalArgumentException("Expected CCBUser but got " + user.getClass().getCanonicalName()); + } + final CCBUser ccbuser = (CCBUser) user; + + // Only update the level if it is increasing. + final CustomPulldownFieldValue currentLevel = ccbuser.getProfile() + .getCustomPulldownFields().getByLabel(GROW_LEVEL); + + if (currentLevel != null) { + if (Score.numericScore(chapter) <= Score.numericScore(currentLevel.getSelection().getLabel())) { + LOG.info("Not updating level for " + user.getIdentifier() + + " because current level (" + currentLevel.getSelection().getLabel() + + ") is greater than new level (" + chapter + ")"); + return; + } + } + + updateLevelAndDate(ccbuser, GROW_LEVEL, chapter, date); + } + + private void updateLevelAndDate(final CCBUser user, final String field, final String level, final Date date) { + boolean modified = false; + + final UpdateIndividualProfileRequest req = new UpdateIndividualProfileRequest() + .withIndividualId(user.getProfile().getId()); + + final CustomField pulldownField = mCache.getIndividualPulldownByLabel(field); + if (pulldownField != null) { + final LookupTableType type = LookupTableType.valueOf(pulldownField.getName().toUpperCase()); + final LookupTableItem item = mCache.getPulldownItemByName(type, level); + if (item != null) { + req.withCustomPulldownField(pulldownField.getName(), item.getId()); + modified = true; + } + } + + final CustomField dateField = mCache.getDateFieldByLabel(field); + if (dateField != null) { + req.withCustomDateField(dateField.getName(), date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + modified = true; + } + + try { + // Only update if a field exists. + if (modified) { + mAPI.updateIndividualProfile(req); + } + + } catch (IOException e) { + LOG.error("updateIndividual failed for " + user.getIdentifier() + + ", field " + field + + ", level " + level + + ", date " + date.toString()); + } + } +} diff --git a/src/main/java/com/p4square/grow/ccb/CCBUser.java b/src/main/java/com/p4square/grow/ccb/CCBUser.java new file mode 100644 index 0000000..7313172 --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/CCBUser.java @@ -0,0 +1,37 @@ +package com.p4square.grow.ccb; + +import com.p4square.ccbapi.model.IndividualProfile; +import org.restlet.security.User; + +/** + * CCBUser is an adapter between a CCB IndividualProfile and a Restlet User. + * + * Note: CCBUser prefixes the user's identifier with "CCB-". This is done to + * ensure the identifier does not collide with identifiers from other + * systems. + */ +public class CCBUser extends User { + + private final IndividualProfile mProfile; + + /** + * Wrap an IndividualProfile inside a User object. + * + * @param profile The CCB IndividualProfile for the user. + */ + public CCBUser(final IndividualProfile profile) { + mProfile = profile; + + setIdentifier("CCB-" + mProfile.getId()); + setFirstName(mProfile.getFirstName()); + setLastName(mProfile.getLastName()); + setEmail(mProfile.getEmail()); + } + + /** + * @return The IndividualProfile of the user. + */ + public IndividualProfile getProfile() { + return mProfile; + } +} diff --git a/src/main/java/com/p4square/grow/ccb/CCBUserVerifier.java b/src/main/java/com/p4square/grow/ccb/CCBUserVerifier.java new file mode 100644 index 0000000..db10b75 --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/CCBUserVerifier.java @@ -0,0 +1,50 @@ +package com.p4square.grow.ccb; + +import com.p4square.ccbapi.CCBAPI; +import com.p4square.ccbapi.model.GetIndividualProfilesRequest; +import com.p4square.ccbapi.model.GetIndividualProfilesResponse; +import org.apache.log4j.Logger; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Verifier; + +/** + * CCBUserVerifier authenticates a user through the CCB individual_profile_from_login_password API. + */ +public class CCBUserVerifier implements Verifier { + private static final Logger LOG = Logger.getLogger(CCBUserVerifier.class); + + private final CCBAPI mAPI; + + public CCBUserVerifier(final CCBAPI api) { + mAPI = api; + } + + @Override + public int verify(Request request, Response response) { + if (request.getChallengeResponse() == null) { + return RESULT_MISSING; // no credentials + } + + final String username = request.getChallengeResponse().getIdentifier(); + final char[] password = request.getChallengeResponse().getSecret(); + + try { + GetIndividualProfilesResponse resp = mAPI.getIndividualProfiles( + new GetIndividualProfilesRequest().withLoginPassword(username, password)); + + if (resp.getIndividuals().size() == 1) { + // Wrap the IndividualProfile up in an User and update the user on the request. + final CCBUser user = new CCBUser(resp.getIndividuals().get(0)); + LOG.info("Successfully authenticated " + user.getIdentifier()); + request.getClientInfo().setUser(user); + return RESULT_VALID; + } + + } catch (Exception e) { + LOG.error("CCB API Exception: " + e, e); + } + + return RESULT_INVALID; // Invalid credentials + } +} diff --git a/src/main/java/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java b/src/main/java/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java new file mode 100644 index 0000000..fc6148f --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java @@ -0,0 +1,61 @@ +package com.p4square.grow.ccb; + +import com.codahale.metrics.MetricRegistry; +import com.p4square.ccbapi.CCBAPI; +import com.p4square.ccbapi.CCBAPIClient; +import com.p4square.grow.config.Config; +import com.p4square.grow.frontend.IntegrationDriver; +import com.p4square.grow.frontend.ProgressReporter; +import org.restlet.Context; +import org.restlet.security.Verifier; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * The ChurchCommunityBuilderIntegrationDriver is used to integrate Grow with Church Community Builder. + */ +public class ChurchCommunityBuilderIntegrationDriver implements IntegrationDriver { + + private final Context mContext; + private final MetricRegistry mMetricRegistry; + private final Config mConfig; + + private final CCBAPI mAPI; + + private final CCBProgressReporter mProgressReporter; + + public ChurchCommunityBuilderIntegrationDriver(final Context context) { + mContext = context; + mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); + mMetricRegistry = (MetricRegistry) context.getAttributes().get("com.p4square.grow.metrics"); + + try { + CCBAPI api = new CCBAPIClient(new URI(mConfig.getString("CCBAPIURL", "")), + mConfig.getString("CCBAPIUser", ""), + mConfig.getString("CCBAPIPassword", "")); + + if (mMetricRegistry != null) { + api = new MonitoredCCBAPI(api, mMetricRegistry); + } + + mAPI = api; + + final CustomFieldCache cache = new CustomFieldCache(mAPI); + mProgressReporter = new CCBProgressReporter(mAPI, cache); + + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Override + public Verifier newUserAuthenticationVerifier() { + return new CCBUserVerifier(mAPI); + } + + @Override + public ProgressReporter getProgressReporter() { + return mProgressReporter; + } +} diff --git a/src/main/java/com/p4square/grow/ccb/CustomFieldCache.java b/src/main/java/com/p4square/grow/ccb/CustomFieldCache.java new file mode 100644 index 0000000..d93e6d9 --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/CustomFieldCache.java @@ -0,0 +1,126 @@ +package com.p4square.grow.ccb; + +import com.p4square.ccbapi.CCBAPI; +import com.p4square.ccbapi.model.*; +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * CustomFieldCache maintains an index from custom field labels to names. + */ +public class CustomFieldCache { + + private static final Logger LOG = Logger.getLogger(CustomFieldCache.class); + + private final CCBAPI mAPI; + + private CustomFieldCollection mTextFields; + private CustomFieldCollection mDateFields; + private CustomFieldCollection mIndividualPulldownFields; + private CustomFieldCollection mGroupPulldownFields; + + private final Map> mItemByNameTable; + + public CustomFieldCache(final CCBAPI api) { + mAPI = api; + mTextFields = new CustomFieldCollection<>(); + mDateFields = new CustomFieldCollection<>(); + mIndividualPulldownFields = new CustomFieldCollection<>(); + mGroupPulldownFields = new CustomFieldCollection<>(); + mItemByNameTable = new HashMap<>(); + } + + public CustomField getTextFieldByLabel(final String label) { + if (mTextFields.size() == 0) { + refresh(); + } + return mTextFields.getByLabel(label); + } + + public CustomField getDateFieldByLabel(final String label) { + if (mDateFields.size() == 0) { + refresh(); + } + return mDateFields.getByLabel(label); + } + + public CustomField getIndividualPulldownByLabel(final String label) { + if (mIndividualPulldownFields.size() == 0) { + refresh(); + } + return mIndividualPulldownFields.getByLabel(label); + } + + public CustomField getGroupPulldownByLabel(final String label) { + if (mGroupPulldownFields.size() == 0) { + refresh(); + } + return mGroupPulldownFields.getByLabel(label); + } + + public LookupTableItem getPulldownItemByName(final LookupTableType type, final String name) { + Map items = mItemByNameTable.get(type); + if (items == null) { + if (!cacheLookupTable(type)) { + return null; + } + items = mItemByNameTable.get(type); + } + + return items.get(name.toLowerCase()); + } + + private synchronized void refresh() { + try { + // Get all of the custom fields. + final GetCustomFieldLabelsResponse resp = mAPI.getCustomFieldLabels(); + + final CustomFieldCollection newTextFields = new CustomFieldCollection<>(); + final CustomFieldCollection newDateFields = new CustomFieldCollection<>(); + final CustomFieldCollection newIndPulldownFields = new CustomFieldCollection<>(); + final CustomFieldCollection newGrpPulldownFields = new CustomFieldCollection<>(); + + for (final CustomField field : resp.getCustomFields()) { + if (field.getName().startsWith("udf_ind_text_")) { + newTextFields.add(field); + } else if (field.getName().startsWith("udf_ind_date_")) { + newDateFields.add(field); + } else if (field.getName().startsWith("udf_ind_pulldown_")) { + newIndPulldownFields.add(field); + } else if (field.getName().startsWith("udf_grp_pulldown_")) { + newGrpPulldownFields.add(field); + } else { + LOG.warn("Unknown custom field type " + field.getName()); + } + } + + this.mTextFields = newTextFields; + this.mDateFields = newDateFields; + this.mIndividualPulldownFields = newIndPulldownFields; + this.mGroupPulldownFields = newGrpPulldownFields; + + } catch (IOException e) { + // Error fetching labels. + LOG.error("Error fetching custom fields: " + e.getMessage(), e); + } + } + + private synchronized boolean cacheLookupTable(final LookupTableType type) { + try { + final GetLookupTableResponse resp = mAPI.getLookupTable(new GetLookupTableRequest().withType(type)); + mItemByNameTable.put(type, resp.getItems().stream().collect( + Collectors.toMap(item -> item.getName().toLowerCase(), Function.identity()))); + return true; + + } catch (IOException e) { + LOG.error("Exception caching lookup table of type " + type, e); + } + + return false; + } +} diff --git a/src/main/java/com/p4square/grow/ccb/MonitoredCCBAPI.java b/src/main/java/com/p4square/grow/ccb/MonitoredCCBAPI.java new file mode 100644 index 0000000..43b6433 --- /dev/null +++ b/src/main/java/com/p4square/grow/ccb/MonitoredCCBAPI.java @@ -0,0 +1,96 @@ +package com.p4square.grow.ccb; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.p4square.ccbapi.CCBAPI; +import com.p4square.ccbapi.model.*; + +import java.io.IOException; + +/** + * MonitoredCCBAPI is a CCBAPI decorator which records metrics for each API call. + */ +public class MonitoredCCBAPI implements CCBAPI { + + private final CCBAPI mAPI; + private final MetricRegistry mMetricRegistry; + + public MonitoredCCBAPI(final CCBAPI api, final MetricRegistry metricRegistry) { + if (api == null) { + throw new IllegalArgumentException("api must not be null."); + } + mAPI = api; + + if (metricRegistry == null) { + throw new IllegalArgumentException("metricRegistry must not be null."); + } + mMetricRegistry = metricRegistry; + } + + @Override + public GetCustomFieldLabelsResponse getCustomFieldLabels() throws IOException { + final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getCustomFieldLabels.time").time(); + boolean success = false; + try { + final GetCustomFieldLabelsResponse resp = mAPI.getCustomFieldLabels(); + success = true; + return resp; + } finally { + timer.stop(); + mMetricRegistry.counter("CCBAPI.getCustomFieldLabels.success").inc(success ? 1 : 0); + mMetricRegistry.counter("CCBAPI.getCustomFieldLabels.failure").inc(!success ? 1 : 0); + } + } + + @Override + public GetLookupTableResponse getLookupTable(final GetLookupTableRequest request) throws IOException { + final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getLookupTable.time").time(); + boolean success = false; + try { + final GetLookupTableResponse resp = mAPI.getLookupTable(request); + success = true; + return resp; + } finally { + timer.stop(); + mMetricRegistry.counter("CCBAPI.getLookupTable.success").inc(success ? 1 : 0); + mMetricRegistry.counter("CCBAPI.getLookupTable.failure").inc(!success ? 1 : 0); + } + } + + @Override + public GetIndividualProfilesResponse getIndividualProfiles(GetIndividualProfilesRequest request) + throws IOException { + final Timer.Context timer = mMetricRegistry.timer("CCBAPI.getIndividualProfiles").time(); + boolean success = false; + try { + final GetIndividualProfilesResponse resp = mAPI.getIndividualProfiles(request); + mMetricRegistry.counter("CCBAPI.getIndividualProfiles.count").inc(resp.getIndividuals().size()); + success = true; + return resp; + } finally { + timer.stop(); + mMetricRegistry.counter("CCBAPI.getIndividualProfiles.success").inc(success ? 1 : 0); + mMetricRegistry.counter("CCBAPI.getIndividualProfiles.failure").inc(!success ? 1 : 0); + } + } + + @Override + public UpdateIndividualProfileResponse updateIndividualProfile(UpdateIndividualProfileRequest request) throws IOException { + final Timer.Context timer = mMetricRegistry.timer("CCBAPI.updateIndividualProfile").time(); + boolean success = false; + try { + final UpdateIndividualProfileResponse resp = mAPI.updateIndividualProfile(request); + success = true; + return resp; + } finally { + timer.stop(); + mMetricRegistry.counter("CCBAPI.updateIndividualProfile.success").inc(success ? 1 : 0); + mMetricRegistry.counter("CCBAPI.updateIndividualProfile.failure").inc(!success ? 1 : 0); + } + } + + @Override + public void close() throws IOException { + mAPI.close(); + } +} diff --git a/src/main/java/com/p4square/grow/config/Config.java b/src/main/java/com/p4square/grow/config/Config.java new file mode 100644 index 0000000..2fc2ea3 --- /dev/null +++ b/src/main/java/com/p4square/grow/config/Config.java @@ -0,0 +1,203 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.util.Properties; + +import org.apache.log4j.Logger; + +/** + * Manage configuration for an application. + * + * Config reads one or more property files as the application config. Duplicate + * properties loaded later override properties loaded earlier. Config has the + * concept of a domain to distinguish settings for development and production. + * The default domain is prod for production. Domain can be any String such as + * dev for development or test for testing. + * + * The property files are processed like java.util.Properties except that the + * keys are specified as DOMAIN.KEY. An asterisk (*) can be used in place of a + * domain to indicate it should apply to all domains. If a domain specific entry + * exists for the current domain, it will override any global config. + * + * @author Jesse Morgan + */ +public class Config { + private static final Logger LOG = Logger.getLogger(Config.class); + + private String mDomain; + private Properties mProperties; + + /** + * Construct a new Config object. + * + * Sets the domain to the value of the system property CONFIG_DOMAIN, if present. + * If the system property is not set then the environment variable CONFIG_DOMAIN is checked. + * If neither are set the domain defaults to prod. + */ + public Config() { + // Check the command line for a domain property. + mDomain = System.getProperty("CONFIG_DOMAIN"); + + // If the domain was not set with a property, check for an environment variable. + if (mDomain == null) { + mDomain = System.getenv("CONFIG_DOMAIN"); + } + + // If neither were set, default to prod + if (mDomain == null) { + mDomain = "prod"; + } + + mProperties = new Properties(); + } + + /** + * Change the domain from the default string "prod". + * + * @param domain The new domain. + */ + public void setDomain(String domain) { + LOG.info("Setting Config domain to " + domain); + mDomain = domain; + } + + /** + * @return the current domain. + */ + public String getDomain() { + return mDomain; + } + + /** + * Load properties from a file. + * Any exception are logged and suppressed. + */ + public void updateConfig(String propertyFilename) { + final File propFile = new File(propertyFilename); + + LOG.info("Loading properties from " + propFile); + + try { + InputStream in = new FileInputStream(propFile); + updateConfig(in); + + } catch (IOException e) { + LOG.error("Could not load properties file: " + e.getMessage(), e); + } + } + + /** + * Load properties from an InputStream. + * This method closes the InputStream when it completes. + * + * @param in The InputStream + */ + public void updateConfig(InputStream in) throws IOException { + LOG.info("Loading properties from InputStream"); + mProperties.load(in); + in.close(); + } + + /** + * Get a String from the config. + * + * @return The config value or null if it is not found. + */ + public String getString(String key) { + return getString(key, null); + } + + /** + * Get a String from the config. + * + * @return The config value or defaultValue if it can not be found. + */ + public String getString(final String key, final String defaultValue) { + String result; + + // Command line properties trump all. + result = System.getProperty(key); + if (result != null) { + LOG.debug("Reading System.getProperty(" + key + "). Got result = { " + result + " }"); + return result; + } + + // Environment variables can also override configs + result = System.getenv(key); + if (result != null) { + LOG.debug("Reading System.getenv(" + key + "). Got result = { " + result + " }"); + return result; + } + + final String domainKey = mDomain + "." + key; + result = mProperties.getProperty(domainKey); + if (result != null) { + LOG.debug("Reading config for key = { " + key + " }. Got result = { " + result + " }"); + return result; + } + + final String globalKey = "*." + key; + result = mProperties.getProperty(globalKey); + if (result != null) { + LOG.debug("Reading config for key = { " + key + " }. Got result = { " + result + " }"); + return result; + } + + LOG.debug("Reading config for key = { " + key + " }. Got default value = { " + defaultValue + " }"); + return defaultValue; + } + + /** + * Get an integer from the config. + * + * @return The config value or Integer.MIN_VALUE if the key is not present or the + * config can not be parsed. + */ + public int getInt(String key) { + return getInt(key, Integer.MIN_VALUE); + } + + /** + * Get an integer from the config. + * + * @return The config value or defaultValue if the key is not present or the + * config can not be parsed. + */ + public int getInt(String key, int defaultValue) { + final String propertyValue = getString(key); + + if (propertyValue != null) { + try { + final int result = Integer.valueOf(propertyValue); + return result; + + } catch (NumberFormatException e) { + LOG.warn("Expected property to be an integer: " + + key + " = { " + propertyValue + " }"); + } + } + + return defaultValue; + } + + public boolean getBoolean(String key) { + return getBoolean(key, false); + } + + public boolean getBoolean(String key, boolean defaultValue) { + final String propertyValue = getString(key); + + if (propertyValue != null) { + return (propertyValue.charAt(0) & 0xDF) == 'T'; + } + + return defaultValue; + } +} 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 + */ +public class AccountRedirectResource extends ServerResource { + private static final Logger LOG = Logger.getLogger(AccountRedirectResource.class); + + private Config mConfig; + private Provider 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( + new JsonRequestProvider(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 + */ +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 + */ +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 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 + */ +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 + */ +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 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( + new JsonRequestProvider( + 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 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 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 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 + */ +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 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 root) { + cTemplate = template; + cRoot = root; + } + + protected Representation getRepresentation() { + if (cTemplate == null) { + return new StringRepresentation(mMessage); + + } else { + Map root = new HashMap(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 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> mThreadsProvider; + private final JsonRequestProvider mThreadProvider; + + private final JsonRequestProvider> mMessagesProvider; + private final JsonRequestProvider 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>(clientDispatcher, threadType); + mThreadProvider = new JsonRequestProvider(clientDispatcher, MessageThread.class); + + JavaType messageType = factory.constructCollectionType(List.class, Message.class); + mMessagesProvider = new JsonRequestProvider>(clientDispatcher, messageType); + mMessageProvider = new JsonRequestProvider(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 getThreads(final String topic, final int limit) throws IOException { + return mThreadsProvider.get(makeUrl(limit, topic)); + } + + public List 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 + */ +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 + */ + +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 + */ +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 + */ +public class JsonRequestProvider extends JsonEncodedProvider implements Provider { + + private final Restlet mDispatcher; + + public JsonRequestProvider(Restlet dispatcher, Class 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 + */ +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 + */ +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 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 + */ +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 + */ +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 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 + */ +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 + */ +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 mQuestionProvider; + private Provider 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( + new JsonRequestProvider(getContext().getClientDispatcher(), + Question.class)) { + @Override + public String makeKey(String questionId) { + return getBackendEndpoint() + "/assessment/question/" + questionId; + } + }; + + mUserRecordProvider = new DelegateProvider( + new JsonRequestProvider(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 answer = new HashMap(); + 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 + */ +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> VIDEO_COMPARATOR = new Comparator>() { + @Override + public int compare(Map left, Map 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 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(new JsonRequestProvider(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 chapters = playlist.getChapterStatuses(); + Map allowedChapters = new LinkedHashMap(); + + // 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> videos = null; + { + JsonResponse response = backendGet("/training/" + mChapter); + if (!response.getStatus().isSuccess()) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + videos = (List>) response.getMap().get("videos"); + Collections.sort(videos, VIDEO_COMPARATOR); + } + + // Mark the completed videos as completed + int chapterProgress = 0; + for (Map 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 + */ +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(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 data = new HashMap(); + 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; + } +} diff --git a/src/main/java/com/p4square/grow/model/Answer.java b/src/main/java/com/p4square/grow/model/Answer.java new file mode 100644 index 0000000..a818365 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Answer.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * This is the model of an assessment question's answer. + * + * @author Jesse Morgan + */ +public class Answer { + private static final Logger LOG = Logger.getLogger(Answer.class); + + /** + * ScoreType determines how the answer will be scored. + * + */ + public static enum ScoreType { + /** + * This question has no effect on the score. + */ + NONE, + + /** + * The score of this question is part of the average. + */ + AVERAGE, + + /** + * The score of this question is the total score, no other questions + * matter after this point. + */ + TRUMP; + + @Override + public String toString() { + return name().toLowerCase(); + } + } + + private String mAnswerText; + private ScoreType mType; + private float mScoreFactor; + private String mNextQuestionId; + + public Answer() { + mType = ScoreType.AVERAGE; + } + + /** + * @return The text associated with the answer. + */ + public String getText() { + return mAnswerText; + } + + /** + * Set the text associated with the answer. + * @param text The new text. + */ + public void setText(String text) { + mAnswerText = text; + } + + /** + * @return the ScoreType for the Answer. + */ + public ScoreType getType() { + return mType; + } + + /** + * Set the ScoreType for the answer. + * @param type The new ScoreType. + */ + public void setType(ScoreType type) { + mType = type; + } + + /** + * @return the delta of the score if this answer is selected. + */ + public float getScore() { + if (mType == ScoreType.NONE) { + return 0; + } + + return mScoreFactor; + } + + /** + * Set the score delta for this answer. + * @param score The new delta. + */ + public void setScore(float score) { + mScoreFactor = score; + } + + /** + * @return the id of the next question if this answer is selected, or null + * if selecting this answer has no effect. + */ + public String getNextQuestion() { + return mNextQuestionId; + } + + /** + * Set the id of the next question when this answer is selected. + * @param id The next question id or null to proceed as usual. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; + } + + /** + * Adjust the running score for the selection of this answer. + * @param score The running score to adjust. + * @return true if scoring should continue, false if this answer trumps all. + */ + public boolean score(final Score score) { + switch (getType()) { + case TRUMP: + score.sum = getScore(); + score.count = 1; + return false; // Quit scoring. + + case AVERAGE: + LOG.debug("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }"); + score.sum += getScore(); + score.count++; + break; + + case NONE: + break; + } + + return true; // Continue scoring + } +} diff --git a/src/main/java/com/p4square/grow/model/Banner.java b/src/main/java/com/p4square/grow/model/Banner.java new file mode 100644 index 0000000..b786b36 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Banner.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Page Banner Data. + */ +public class Banner { + private String mHtml; + + public String getHtml() { + return mHtml; + } + + public void setHtml(final String html) { + mHtml = html; + } +} diff --git a/src/main/java/com/p4square/grow/model/Chapter.java b/src/main/java/com/p4square/grow/model/Chapter.java new file mode 100644 index 0000000..3a08e4c --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Chapter.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Chapter is a list of VideoRecords in a Playlist. + * + * @author Jesse Morgan + */ +public class Chapter implements Cloneable { + private String mName; + private Map mVideos; + + public Chapter(String name) { + mName = name; + mVideos = new HashMap(); + } + + /** + * Private constructor for JSON decoding. + */ + private Chapter() { + this(null); + } + + /** + * @return The Chapter name. + */ + public String getName() { + return mName; + } + + /** + * Set the chapter name. + * + * @param name The name of the chapter. + */ + public void setName(final String name) { + mName = name; + } + + /** + * @return The VideoRecord for videoid or null if videoid is not in the chapter. + */ + public VideoRecord getVideoRecord(String videoid) { + return mVideos.get(videoid); + } + + /** + * @return A map of video ids to VideoRecords. + */ + @JsonAnyGetter + public Map getVideos() { + return mVideos; + } + + /** + * Set the VideoRecord for a video id. + * @param videoId the video id. + * @param video the VideoRecord. + */ + @JsonAnySetter + public void setVideoRecord(String videoId, VideoRecord video) { + mVideos.put(videoId, video); + } + + /** + * Remove the VideoRecord for a video id. + * @param videoId The id to remove. + */ + public void removeVideoRecord(String videoId) { + mVideos.remove(videoId); + } + + /** + * @return true if every required video has been completed. + */ + @JsonIgnore + public boolean isComplete() { + boolean complete = true; + + for (VideoRecord r : mVideos.values()) { + if (r.getRequired() && !r.getComplete()) { + return false; + } + } + + return complete; + } + + /** + * Deeply clone a chapter. + * + * @return a new Chapter object identical but independent of this one. + */ + public Chapter clone() throws CloneNotSupportedException { + Chapter c = new Chapter(mName); + for (Map.Entry videoEntry : mVideos.entrySet()) { + c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone()); + } + return c; + } +} diff --git a/src/main/java/com/p4square/grow/model/CircleQuestion.java b/src/main/java/com/p4square/grow/model/CircleQuestion.java new file mode 100644 index 0000000..71acc14 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/CircleQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Circle Question. + * + * @author Jesse Morgan + */ +public class CircleQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTopLeft; + private String mTopRight; + private String mBottomLeft; + private String mBottomRight; + + /** + * @return the Top Left label. + */ + public String getTopLeft() { + return mTopLeft; + } + + /** + * Set the Top Left label. + * @param s The new top left label. + */ + public void setTopLeft(String s) { + mTopLeft = s; + } + + /** + * @return the Top Right label. + */ + public String getTopRight() { + return mTopRight; + } + + /** + * Set the Top Right label. + * @param s The new top left label. + */ + public void setTopRight(String s) { + mTopRight = s; + } + + /** + * @return the Bottom Left label. + */ + public String getBottomLeft() { + return mBottomLeft; + } + + /** + * Set the Bottom Left label. + * @param s The new top left label. + */ + public void setBottomLeft(String s) { + mBottomLeft = s; + } + + /** + * @return the Bottom Right label. + */ + public String getBottomRight() { + return mBottomRight; + } + + /** + * Set the Bottom Right label. + * @param s The new top left label. + */ + public void setBottomRight(String s) { + mBottomRight = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.CIRCLE; + } +} diff --git a/src/main/java/com/p4square/grow/model/ImageQuestion.java b/src/main/java/com/p4square/grow/model/ImageQuestion.java new file mode 100644 index 0000000..d94c32c --- /dev/null +++ b/src/main/java/com/p4square/grow/model/ImageQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Image Question. + * + * @author Jesse Morgan + */ +public class ImageQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.IMAGE; + } +} diff --git a/src/main/java/com/p4square/grow/model/Message.java b/src/main/java/com/p4square/grow/model/Message.java new file mode 100644 index 0000000..9d33320 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Message.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; +import java.util.UUID; + +/** + * A feed message. + * + * @author Jesse Morgan + */ +public class Message { + private String mThreadId; + private String mId; + private UserRecord mAuthor; + private Date mCreated; + private String mMessage; + + /** + * @return a new message id. + */ + public static String generateId() { + return String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString()); + } + + /** + * @return The id of the thread that the message belongs to. + */ + public String getThreadId() { + return mThreadId; + } + + /** + * Set the id of the thread that the message belongs to. + * @param id The new thread id. + */ + public void setThreadId(String id) { + mThreadId = id; + } + + /** + * @return The id the message. + */ + public String getId() { + return mId; + } + + /** + * Set the id of the message. + * @param id The new message id. + */ + public void setId(String id) { + mId = id; + } + + /** + * @return The author of the message. + */ + public UserRecord getAuthor() { + return mAuthor; + } + + /** + * Set the author of the message. + * @param author The new author. + */ + public void setAuthor(UserRecord author) { + mAuthor = author; + } + + /** + * @return The Date the message was created. + */ + public Date getCreated() { + return mCreated; + } + + /** + * Set the Date the message was created. + * @param date The new creation date. + */ + public void setCreated(Date date) { + mCreated = date; + } + + /** + * @return The message text. + */ + public String getMessage() { + return mMessage; + } + + /** + * Set the message text. + * @param text The message text. + */ + public void setMessage(String text) { + mMessage = text; + } +} diff --git a/src/main/java/com/p4square/grow/model/MessageThread.java b/src/main/java/com/p4square/grow/model/MessageThread.java new file mode 100644 index 0000000..9542a18 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/MessageThread.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.UUID; + +/** + * + * @author Jesse Morgan + */ +public class MessageThread { + private String mId; + private Message mMessage; + + /** + * Create a new thread with a probably unique id. + * + * @return the new thread. + */ + public static MessageThread createNew() { + MessageThread t = new MessageThread(); + // IDs are keyed to sort lexicographically from latest to oldest. + t.setId(String.format("%016x-%s", Long.MAX_VALUE - System.currentTimeMillis(), + UUID.randomUUID().toString())); + + return t; + } + + /** + * @return The id the message. + */ + public String getId() { + return mId; + } + + /** + * Set the id of the message. + * @param id The new message id. + */ + public void setId(String id) { + mId = id; + } + + /** + * @return The original message. + */ + public Message getMessage() { + return mMessage; + } + + /** + * Set the original message. + * @param id The new message. + */ + public void setMessage(Message message) { + mMessage = message; + } +} diff --git a/src/main/java/com/p4square/grow/model/Playlist.java b/src/main/java/com/p4square/grow/model/Playlist.java new file mode 100644 index 0000000..3e77ada --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Playlist.java @@ -0,0 +1,192 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Representation of a user's playlist. + * + * @author Jesse Morgan + */ +public class Playlist { + /** + * Map of Chapter ID to map of Video ID to VideoRecord. + */ + private Map mPlaylist; + + private Date mLastUpdated; + + /** + * Construct an empty playlist. + */ + public Playlist() { + mPlaylist = new HashMap(); + mLastUpdated = new Date(0); // Default to a prehistoric date if we don't have one. + } + + /** + * Find the VideoRecord for a video id. + */ + public VideoRecord find(String videoId) { + for (Chapter chapter : mPlaylist.values()) { + VideoRecord r = chapter.getVideoRecord(videoId); + + if (r != null) { + return r; + } + } + + return null; + } + + /** + * @param videoId The video to search for. + * @return the Chapter containing videoId. + */ + private Chapter findChapter(String videoId) { + for (Chapter chapter : mPlaylist.values()) { + VideoRecord r = chapter.getVideoRecord(videoId); + + if (r != null) { + return chapter; + } + } + + return null; + } + + /** + * @return The last modified date of the source playlist. + */ + public Date getLastUpdated() { + return mLastUpdated; + } + + /** + * Set the last updated date. + * @param date the new last updated date. + */ + public void setLastUpdated(Date date) { + mLastUpdated = date; + } + + /** + * Add a video to the playlist. + */ + public VideoRecord add(String chapterId, String videoId) { + Chapter chapter = mPlaylist.get(chapterId); + + if (chapter == null) { + chapter = new Chapter(chapterId); + mPlaylist.put(chapterId, chapter); + } + + VideoRecord r = new VideoRecord(); + chapter.setVideoRecord(videoId, r); + return r; + } + + /** + * Add a Chapter to the Playlist. + * @param chapterId The name of the chapter. + * @param chapter The Chapter object to add. + */ + @JsonAnySetter + public void addChapter(String chapterId, Chapter chapter) { + chapter.setName(chapterId); + mPlaylist.put(chapterId, chapter); + } + + /** + * @return a map of chapter id to chapter. + */ + @JsonAnyGetter + public Map getChaptersMap() { + return mPlaylist; + } + + /** + * @return The last chapter to be completed. + */ + @JsonIgnore + public Map getChapterStatuses() { + Map completed = new HashMap(); + + for (Map.Entry entry : mPlaylist.entrySet()) { + completed.put(entry.getKey(), entry.getValue().isComplete()); + } + + return completed; + } + + /** + * @return true if all required videos in the chapter have been watched. + */ + public boolean isChapterComplete(String chapterId) { + Chapter chapter = mPlaylist.get(chapterId); + if (chapter != null) { + return chapter.isComplete(); + } + + return false; + } + + /** + * Merge a playlist into this playlist. + * + * Merge is accomplished by adding all missing Chapters and VideoRecords to + * this playlist. + */ + public void merge(Playlist source) { + if (source.getLastUpdated().before(mLastUpdated)) { + // Already up to date. + return; + } + + for (Map.Entry entry : source.getChaptersMap().entrySet()) { + String chapterName = entry.getKey(); + Chapter theirChapter = entry.getValue(); + Chapter myChapter = mPlaylist.get(entry.getKey()); + + if (myChapter == null) { + // Add new chapter + myChapter = new Chapter(chapterName); + addChapter(chapterName, myChapter); + } + + // Check chapter for missing videos + for (Map.Entry videoEntry : theirChapter.getVideos().entrySet()) { + String videoId = videoEntry.getKey(); + VideoRecord myVideo = myChapter.getVideoRecord(videoId); + + if (myVideo == null) { + myVideo = find(videoId); + if (myVideo == null) { + // New Video + try { + myVideo = videoEntry.getValue().clone(); + myChapter.setVideoRecord(videoId, myVideo); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); // Unexpected... + } + } else { + // Video moved + findChapter(videoId).removeVideoRecord(videoId); + myChapter.setVideoRecord(videoId, myVideo); + } + } + } + } + + mLastUpdated = source.getLastUpdated(); + } +} diff --git a/src/main/java/com/p4square/grow/model/Point.java b/src/main/java/com/p4square/grow/model/Point.java new file mode 100644 index 0000000..e9fc0ca --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Point.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple double based point class. + * + * @author Jesse Morgan + */ +public class Point { + /** + * Parse a comma separated x,y pair into a point. + * + * @return The point represented by the string. + * @throws IllegalArgumentException if the input is malformed. + */ + public static Point valueOf(String str) { + final int comma = str.indexOf(','); + if (comma == -1 || comma == 0 || comma == str.length() - 1) { + throw new IllegalArgumentException("Malformed point string"); + } + + final String sX = str.substring(0, comma); + final String sY = str.substring(comma + 1); + + return new Point(Double.valueOf(sX), Double.valueOf(sY)); + } + + private final double mX; + private final double mY; + + /** + * Create a new point with the given coordinates. + * + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Point(double x, double y) { + mX = x; + mY = y; + } + + /** + * Compute the distance between this point and another. + * + * @param other The other point. + * @return The distance between this point and other. + */ + public double distance(Point other) { + final double dx = mX - other.mX; + final double dy = mY - other.mY; + + return Math.sqrt(dx*dx + dy*dy); + } + + /** + * @return The x coordinate. + */ + public double getX() { + return mX; + } + + /** + * @return The y coordinate. + */ + public double getY() { + return mY; + } + + /** + * @return The point represented as a comma separated pair. + */ + @Override + public String toString() { + return String.format("%.2f,%.2f", mX, mY); + } +} diff --git a/src/main/java/com/p4square/grow/model/QuadQuestion.java b/src/main/java/com/p4square/grow/model/QuadQuestion.java new file mode 100644 index 0000000..a7b4179 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/QuadQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Two-dimensional Question. + * + * @author Jesse Morgan + */ +public class QuadQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTop; + private String mRight; + private String mBottom; + private String mLeft; + + /** + * @return the top label. + */ + public String getTop() { + return mTop; + } + + /** + * Set the top label. + * @param s The new top label. + */ + public void setTop(String s) { + mTop = s; + } + + /** + * @return the right label. + */ + public String getRight() { + return mRight; + } + + /** + * Set the right label. + * @param s The new right label. + */ + public void setRight(String s) { + mRight = s; + } + + /** + * @return the bottom label. + */ + public String getBottom() { + return mBottom; + } + + /** + * Set the bottom label. + * @param s The new bottom label. + */ + public void setBottom(String s) { + mBottom = s; + } + + /** + * @return the left label. + */ + public String getLeft() { + return mLeft; + } + + /** + * Set the left label. + * @param s The new left label. + */ + public void setLeft(String s) { + mLeft = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.QUAD; + } +} diff --git a/src/main/java/com/p4square/grow/model/QuadScoringEngine.java b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java new file mode 100644 index 0000000..33403b5 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import com.p4square.grow.model.Point; + +/** + * QuadScoringEngine expects the user's answer to be a Point string. We find + * the closest answer Point to the user's answer and treat that as the answer. + * + * @author Jesse Morgan + */ +public class QuadScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + // Find all of the answer points. + Point[] answers = new Point[question.getAnswers().size()]; + { + int i = 0; + for (String answerStr : question.getAnswers().keySet()) { + answers[i++] = Point.valueOf(answerStr); + } + } + + // Parse the user's answer. + Point userPoint = Point.valueOf(userAnswer.getAnswerId()); + + // Find the closest answer point to the user's answer. + double minDistance = Double.MAX_VALUE; + int answerIndex = 0; + for (int i = 0; i < answers.length; i++) { + final double distance = userPoint.distance(answers[i]); + if (distance < minDistance) { + minDistance = distance; + answerIndex = i; + } + } + + LOG.debug("Quad " + question.getId() + ": Got answer " + + answers[answerIndex].toString() + " for user point " + userAnswer); + + // Get the answer and update the score. + final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); + return answer.score(score); + } +} diff --git a/src/main/java/com/p4square/grow/model/Question.java b/src/main/java/com/p4square/grow/model/Question.java new file mode 100644 index 0000000..f4b9458 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Question.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Model of an assessment question. + * + * @author Jesse Morgan + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @Type(value = TextQuestion.class, name = "text"), + @Type(value = ImageQuestion.class, name = "image"), + @Type(value = SliderQuestion.class, name = "slider"), + @Type(value = QuadQuestion.class, name = "quad"), + @Type(value = CircleQuestion.class, name = "circle"), +}) +public abstract class Question { + /** + * QuestionType indicates the type of Question. + * + * @author Jesse Morgan + */ + public enum QuestionType { + TEXT, + IMAGE, + SLIDER, + QUAD, + CIRCLE; + + @Override + public String toString() { + return name().toLowerCase(); + } + } + + private String mQuestionId; + private QuestionType mType; + private String mQuestionText; + private Map mAnswers; + + private String mPreviousQuestionId; + private String mNextQuestionId; + + public Question() { + mAnswers = new HashMap(); + } + + /** + * @return the id String for this question. + */ + public String getId() { + return mQuestionId; + } + + /** + * Set the id String for this question. + * @param id New id + */ + public void setId(String id) { + mQuestionId = id; + } + + /** + * @return The Question text. + */ + public String getQuestion() { + return mQuestionText; + } + + /** + * Set the question text. + * @param value The new question text. + */ + public void setQuestion(String value) { + mQuestionText = value; + } + + /** + * @return The id String of the previous question or null if no previous question exists. + */ + public String getPreviousQuestion() { + return mPreviousQuestionId; + } + + /** + * Set the id string of the previous question. + * @param id Previous question id or null if there is no previous question. + */ + public void setPreviousQuestion(String id) { + mPreviousQuestionId = id; + } + + /** + * @return The id String of the next question or null if no next question exists. + */ + public String getNextQuestion() { + return mNextQuestionId; + } + + /** + * Set the id string of the next question. + * @param id next question id or null if there is no next question. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; + } + + /** + * @return a map of Answer id Strings to Answer objects. + */ + public Map getAnswers() { + return mAnswers; + } + + /** + * Determine the id of the next question based on the answer to this + * question. + * + * @param answerid + * The id of the selected answer. + * @return a question id or null if this is the last question. + */ + public String getNextQuestion(String answerid) { + String nextQuestion = null; + + Answer a = mAnswers.get(answerid); + if (a != null) { + nextQuestion = a.getNextQuestion(); + } + + if (nextQuestion == null) { + nextQuestion = mNextQuestionId; + } + + return nextQuestion; + } + + /** + * Update the score based on the answer to this question. + * + * @param score The running score to update. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, RecordedAnswer answer); + + /** + * @return the QuestionType of this question. + */ + public abstract QuestionType getType(); + +} diff --git a/src/main/java/com/p4square/grow/model/RecordedAnswer.java b/src/main/java/com/p4square/grow/model/RecordedAnswer.java new file mode 100644 index 0000000..7d9905d --- /dev/null +++ b/src/main/java/com/p4square/grow/model/RecordedAnswer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple model for a user's assessment answer. + * + * @author Jesse Morgan + */ +public class RecordedAnswer { + private String mAnswerId; + + /** + * @return The user's answer. + */ + public String getAnswerId() { + return mAnswerId; + } + + /** + * Set the answer id field. + * @param id The new id. + */ + public void setAnswerId(String id) { + mAnswerId = id; + } + + @Override + public String toString() { + return mAnswerId; + } +} diff --git a/src/main/java/com/p4square/grow/model/Score.java b/src/main/java/com/p4square/grow/model/Score.java new file mode 100644 index 0000000..031c309 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Score.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple structure containing a score's sum and count. + * + * @author Jesse Morgan + */ +public class Score { + /** + * Return the decimal value for the given Score String. + * + * This method satisfies the invariant for Score x: + * numericScore(x.toString()) <= x.getScore() + */ + public static double numericScore(String score) { + score = score.toLowerCase(); + + if ("teacher".equals(score)) { + return 3.5; + } else if ("disciple".equals(score)) { + return 2.5; + } else if ("believer".equals(score)) { + return 1.5; + } else if ("seeker".equals(score)) { + return 0; + } else { + return Integer.MAX_VALUE; + } + } + + double sum; + int count; + + public Score() { + sum = 0; + count = 0; + } + + public Score(double sum, int count) { + this.sum = sum; + this.count = count; + } + + /** + * Copy Constructor. + */ + public Score(Score other) { + sum = other.sum; + count = other.count; + } + + /** + * @return The sum of all the points. + */ + public double getSum() { + return sum; + } + + /** + * @return The number of questions included in the score. + */ + public int getCount() { + return count; + } + + /** + * @return The final score. + */ + public double getScore() { + if (count == 0) { + return 0; + } + + return sum / count; + } + + /** + * @return the lowest score in the same category as this score. + */ + public double floor() { + final double score = getScore(); + + if (score >= 3.5) { + return 3.5; // teacher + + } else if (score >= 2.5) { + return 2.5; // disciple + + } else if (score >= 1.5) { + return 1.5; // believer + + } else { + return 0; // seeker + } + } + + @Override + public String toString() { + final double score = getScore(); + + if (score >= 3.5) { + return "teacher"; + + } else if (score >= 2.5) { + return "disciple"; + + } else if (score >= 1.5) { + return "believer"; + + } else { + return "seeker"; + } + } + +} diff --git a/src/main/java/com/p4square/grow/model/ScoringEngine.java b/src/main/java/com/p4square/grow/model/ScoringEngine.java new file mode 100644 index 0000000..8ff18b3 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/ScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * ScoringEngine computes the score for a question and a given answer. + * + * @author Jesse Morgan + */ +public abstract class ScoringEngine { + protected static final Logger LOG = Logger.getLogger(ScoringEngine.class); + + /** + * Update the score based on the given question and answer. + * + * @param score The running score to update. + * @param question The question to compute the score for. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer); +} diff --git a/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java new file mode 100644 index 0000000..6ef2dbb --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SimpleScoringEngine expects the user's answer to a valid answer id and + * scores accordingly. + * + * If the answer id is not valid an Exception is thrown. + * + * @author Jesse Morgan + */ +public class SimpleScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + final Answer answer = question.getAnswers().get(userAnswer.getAnswerId()); + if (answer == null) { + throw new IllegalArgumentException("Not a valid answer."); + } + + return answer.score(score); + } +} diff --git a/src/main/java/com/p4square/grow/model/SliderQuestion.java b/src/main/java/com/p4square/grow/model/SliderQuestion.java new file mode 100644 index 0000000..f0861e3 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SliderQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Slider Question. + * + * @author Jesse Morgan + */ +public class SliderQuestion extends Question { + private static final ScoringEngine ENGINE = new SliderScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.SLIDER; + } +} diff --git a/src/main/java/com/p4square/grow/model/SliderScoringEngine.java b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java new file mode 100644 index 0000000..2961e95 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SliderScoringEngine expects the user's answer to be a decimal value in the + * range [0, 1]. The value is scaled to the range [1, 4] and added to the + * score. + * + * @author Jesse Morgan + */ +public class SliderScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + int numberOfAnswers = question.getAnswers().size(); + if (numberOfAnswers == 0) { + throw new IllegalArgumentException("Question has no answers."); + } + + double answer = Double.valueOf(userAnswer.getAnswerId()); + if (answer < 0 || answer > 1) { + throw new IllegalArgumentException("Answer out of bounds."); + } + + double delta = Math.max(1, Math.ceil(answer * numberOfAnswers) / numberOfAnswers * 4); + + score.sum += delta; + score.count++; + + return true; + } +} diff --git a/src/main/java/com/p4square/grow/model/TextQuestion.java b/src/main/java/com/p4square/grow/model/TextQuestion.java new file mode 100644 index 0000000..88c2a34 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/TextQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Text Question. + * + * @author Jesse Morgan + */ +public class TextQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.TEXT; + } +} diff --git a/src/main/java/com/p4square/grow/model/TrainingRecord.java b/src/main/java/com/p4square/grow/model/TrainingRecord.java new file mode 100644 index 0000000..bc3ffa9 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/TrainingRecord.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Representation of a user's training record. + * + * @author Jesse Morgan + */ +public class TrainingRecord { + private String mLastVideo; + private Playlist mPlaylist; + + public TrainingRecord() { + mPlaylist = new Playlist(); + } + + /** + * @return Video id of the last video watched. + */ + public String getLastVideo() { + return mLastVideo; + } + + /** + * Set the video id for the last video watched. + * @param video The new video id. + */ + public void setLastVideo(String video) { + mLastVideo = video; + } + + /** + * @return the user's Playlist. + */ + public Playlist getPlaylist() { + return mPlaylist; + } + + /** + * Set the user's playlist. + * @param playlist The new playlist. + */ + public void setPlaylist(Playlist playlist) { + mPlaylist = playlist; + } +} diff --git a/src/main/java/com/p4square/grow/model/UserRecord.java b/src/main/java/com/p4square/grow/model/UserRecord.java new file mode 100644 index 0000000..4399282 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/UserRecord.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; + +import org.restlet.security.User; + +/** + * A simple user representation without any secrets. + */ +public class UserRecord { + private String mId; + private String mFirstName; + private String mLastName; + private String mEmail; + private String mLanding; + private boolean mNewBeliever; + + // Backend Access + private String mBackendPasswordHash; + + /** + * Create an empty UserRecord. + */ + public UserRecord() { + } + + /** + * Create a new UserRecord with the information from a User. + */ + public UserRecord(final User user) { + mId = user.getIdentifier(); + mFirstName = user.getFirstName(); + mLastName = user.getLastName(); + mEmail = user.getEmail(); + } + + /** + * @return The user's identifier. + */ + public String getId() { + return mId; + } + + /** + * Set the user's identifier. + * @param value The new id. + */ + public void setId(final String value) { + mId = value; + } + + /** + * @return The user's email. + */ + public String getEmail() { + return mEmail; + } + + /** + * Set the user's email. + * @param value The new email. + */ + public void setEmail(final String value) { + mEmail = value; + } + + /** + * @return The user's first name. + */ + public String getFirstName() { + return mFirstName; + } + + /** + * Set the user's first name. + * @param value The new first name. + */ + public void setFirstName(final String value) { + mFirstName = value; + } + + /** + * @return The user's last name. + */ + public String getLastName() { + return mLastName; + } + + /** + * Set the user's last name. + * @param value The new last name. + */ + public void setLastName(final String value) { + mLastName = value; + } + + /** + * @return The user's landing page. + */ + public String getLanding() { + return mLanding; + } + + /** + * Set the user's landing page. + * @param value The new landing page. + */ + public void setLanding(final String value) { + mLanding = value; + } + + /** + * @return true if the user came from the New Believer's landing. + */ + public boolean getNewBeliever() { + return mNewBeliever; + } + + /** + * Set the user's new believer flag. + * @param value The new flag. + */ + public void setNewBeliever(final boolean value) { + mNewBeliever = value; + } + + /** + * @return The user's backend password hash, null if he doesn't have + * access. + */ + public String getBackendPasswordHash() { + return mBackendPasswordHash; + } + + /** + * Set the user's backend password hash. + * @param value The new backend password hash or null to remove + * access. + */ + public void setBackendPasswordHash(final String value) { + mBackendPasswordHash = value; + } + + /** + * Set the user's backend password to the clear-text value given. + * @param value The new backend password. + */ + public void setBackendPassword(final String value) { + try { + mBackendPasswordHash = hashPassword(value); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Hash the given secret. + */ + public static String hashPassword(final String secret) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + // Convert the char[] to byte[] + // FIXME This approach is incorrectly truncating multibyte + // characters. + byte[] b = new byte[secret.length()]; + for (int i = 0; i < secret.length(); i++) { + b[i] = (byte) secret.charAt(i); + } + + md.update(b); + + byte[] hash = md.digest(); + return new String(Hex.encodeHex(hash)); + } +} diff --git a/src/main/java/com/p4square/grow/model/VideoRecord.java b/src/main/java/com/p4square/grow/model/VideoRecord.java new file mode 100644 index 0000000..ec99d0d --- /dev/null +++ b/src/main/java/com/p4square/grow/model/VideoRecord.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Simple bean containing video completion data. + * + * @author Jesse Morgan + */ +public class VideoRecord implements Cloneable { + private Boolean mComplete; + private Boolean mRequired; + private Date mCompletionDate; + + public VideoRecord() { + mComplete = null; + mRequired = null; + mCompletionDate = null; + } + + public boolean getComplete() { + if (mComplete == null) { + return false; + } + return mComplete; + } + + public void setComplete(boolean complete) { + mComplete = complete; + } + + @JsonIgnore + public boolean isCompleteSet() { + return mComplete != null; + } + + public boolean getRequired() { + if (mRequired == null) { + return true; + } + return mRequired; + } + + public void setRequired(boolean complete) { + mRequired = complete; + } + + @JsonIgnore + public boolean isRequiredSet() { + return mRequired != null; + } + + public Date getCompletionDate() { + return mCompletionDate; + } + + public void setCompletionDate(Date date) { + mCompletionDate = date; + } + + /** + * Convenience method to mark a video complete. + */ + public void complete() { + mComplete = true; + mCompletionDate = new Date(); + } + + /** + * @return an identical clone of this record. + */ + public VideoRecord clone() throws CloneNotSupportedException { + VideoRecord r = (VideoRecord) super.clone(); + r.mComplete = mComplete; + r.mRequired = mRequired; + r.mCompletionDate = mCompletionDate; + return r; + } +} diff --git a/src/main/java/com/p4square/grow/provider/CollectionProvider.java b/src/main/java/com/p4square/grow/provider/CollectionProvider.java new file mode 100644 index 0000000..e4e9040 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/CollectionProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; +import java.util.Map; + +/** + * ListProvider is the logical extension of Provider for dealing with lists of + * items. + * + * @param C The type of the collection key. + * @param K The type of the item key. + * @param V The type of the value. + * + * @author Jesse Morgan + */ +public interface CollectionProvider { + /** + * Retrieve a specific object from the collection. + * + * @param collection The collection key. + * @param key The key for the object in the collection. + * @return The object or null if not found. + */ + V get(C collection, K key) throws IOException; + + /** + * Retrieve a collection. + * + * The returned map will never be null. + * + * @param collection The collection key. + * @return A Map of keys to values. + */ + Map query(C collection) throws IOException; + + /** + * Retrieve a portion of a collection. + * + * The returned map will never be null. + * + * @param collection The collection key. + * @param limit Max number of items to return. + * @return A Map of keys to values. + */ + Map query(C collection, int limit) throws IOException; + + /** + * Persist the object with the given key. + * + * @param collection The collection key. + * @param key The key for the object in the collection. + * @param obj The object to persist. + */ + void put(C collection, K key, V obj) throws IOException; +} diff --git a/src/main/java/com/p4square/grow/provider/DelegateCollectionProvider.java b/src/main/java/com/p4square/grow/provider/DelegateCollectionProvider.java new file mode 100644 index 0000000..cf697ba --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/DelegateCollectionProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * + * @author Jesse Morgan + */ +public abstract class DelegateCollectionProvider + implements CollectionProvider { + + private CollectionProvider mProvider; + + public DelegateCollectionProvider(final CollectionProvider provider) { + mProvider = provider; + } + + public V get(C collection, K key) throws IOException { + return mProvider.get(makeCollectionKey(collection), makeKey(key)); + } + + public Map query(C collection) throws IOException { + return query(collection, -1); + } + + public Map query(C collection, int limit) throws IOException { + Map delegateResult = mProvider.query(makeCollectionKey(collection), limit); + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : delegateResult.entrySet()) { + result.put(unmakeKey(entry.getKey()), entry.getValue()); + } + + return result; + } + + public void put(C collection, K key, V obj) throws IOException { + mProvider.put(makeCollectionKey(collection), makeKey(key), obj); + } + + /** + * Make a collection key for the delegated provider. + * + * @param input The pre-transform key. + * @return the post-transform key. + */ + protected abstract DC makeCollectionKey(final C input); + + /** + * Make a key for the delegated provider. + * + * @param input The pre-transform key. + * @return the post-transform key. + */ + protected abstract DK makeKey(final K input); + + /** + * Transform a key for the delegated provider to an input key. + * + * @param input The post-transform key. + * @return the pre-transform key. + */ + protected abstract K unmakeKey(final DK input); +} diff --git a/src/main/java/com/p4square/grow/provider/DelegateProvider.java b/src/main/java/com/p4square/grow/provider/DelegateProvider.java new file mode 100644 index 0000000..42dcc63 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/DelegateProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +/** + * DelegateProvider wraps an existing Provider an transforms the key from + * type K to type D. + * + * @author Jesse Morgan + */ +public abstract class DelegateProvider implements Provider { + + private Provider mProvider; + + public DelegateProvider(final Provider provider) { + mProvider = provider; + } + + @Override + public V get(final K key) throws IOException { + return mProvider.get(makeKey(key)); + } + + @Override + public void put(final K key, final V obj) throws IOException { + mProvider.put(makeKey(key), obj); + } + + /** + * Make a Key for the delegated provider. + * + * @param input The pre-transform key. + * @return the post-transform key. + */ + protected abstract D makeKey(final K input); +} diff --git a/src/main/java/com/p4square/grow/provider/JsonEncodedProvider.java b/src/main/java/com/p4square/grow/provider/JsonEncodedProvider.java new file mode 100644 index 0000000..500f761 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/JsonEncodedProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Provider provides a simple interface for loading and persisting + * objects. + * + * @author Jesse Morgan + */ +public abstract class JsonEncodedProvider { + public static final ObjectMapper MAPPER = new ObjectMapper(); + static { + MAPPER.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + MAPPER.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); + MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + protected final Class mClazz; + protected final JavaType mType; + + public JsonEncodedProvider(Class clazz) { + mClazz = clazz; + mType = null; + } + + public JsonEncodedProvider(JavaType type) { + mType = type; + mClazz = null; + } + + /** + * Encode the object as JSON. + * + * @param obj The object to encode. + * @return The JSON encoding of obj. + * @throws IOException if the object cannot be encoded. + */ + protected String encode(V obj) throws IOException { + if (mClazz == String.class) { + return (String) obj; + } + + return MAPPER.writeValueAsString(obj); + } + + /** + * Decode the JSON string as an object. + * + * @param blob The JSON data to decode. + * @return The decoded object or null if blob is null. + * @throws IOException If an object cannot be decoded. + */ + protected V decode(String blob) throws IOException { + if (blob == null) { + return null; + } + + if (mClazz == String.class) { + return (V) blob; + } + + V obj; + if (mClazz != null) { + obj = MAPPER.readValue(blob, mClazz); + + } else { + obj = MAPPER.readValue(blob, mType); + } + + return obj; + } +} + diff --git a/src/main/java/com/p4square/grow/provider/MapCollectionProvider.java b/src/main/java/com/p4square/grow/provider/MapCollectionProvider.java new file mode 100644 index 0000000..4c5cef6 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/MapCollectionProvider.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.HashMap; + +/** + * In-memory CollectionProvider implementation, useful for tests. + * + * @author Jesse Morgan + */ +public class MapCollectionProvider implements CollectionProvider { + private final Map> mMap; + + public MapCollectionProvider() { + mMap = new HashMap<>(); + } + + @Override + public synchronized V get(C collection, K key) throws IOException { + Map map = mMap.get(collection); + if (map != null) { + return map.get(key); + } + + return null; + } + + @Override + public synchronized Map query(C collection) throws IOException { + Map map = mMap.get(collection); + if (map == null) { + map = new HashMap(); + } + + return map; + } + + @Override + public synchronized Map query(C collection, int limit) throws IOException { + Map map = query(collection); + + if (map.size() > limit) { + Map smallMap = new HashMap<>(); + + Iterator> iterator = map.entrySet().iterator(); + for (int i = 0; i < limit; i++) { + Map.Entry entry = iterator.next(); + smallMap.put(entry.getKey(), entry.getValue()); + } + + return smallMap; + + } else { + return map; + } + } + + @Override + public synchronized void put(C collection, K key, V obj) throws IOException { + Map map = mMap.get(collection); + if (map == null) { + map = new HashMap(); + mMap.put(collection, map); + } + + map.put(key, obj); + } +} diff --git a/src/main/java/com/p4square/grow/provider/MapProvider.java b/src/main/java/com/p4square/grow/provider/MapProvider.java new file mode 100644 index 0000000..40f8107 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/MapProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +/** + * In-memory Provider implementation, useful for tests. + * + * @author Jesse Morgan + */ +public class MapProvider implements Provider { + private final Map mMap = new HashMap(); + + @Override + public V get(K key) throws IOException { + return mMap.get(key); + } + + @Override + public void put(K key, V obj) throws IOException { + mMap.put(key, obj); + } +} diff --git a/src/main/java/com/p4square/grow/provider/Provider.java b/src/main/java/com/p4square/grow/provider/Provider.java new file mode 100644 index 0000000..ca6af25 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/Provider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +/** + * Provider provides a simple interface for loading and persisting + * objects. + * + * @author Jesse Morgan + */ +public interface Provider { + /** + * Retrieve the object with the given key. + * + * @param key The key for the object. + * @return The object or null if not found. + */ + V get(K key) throws IOException; + + /** + * Persist the object with the given key. + * + * @param key The key for the object. + * @param obj The object to persist. + */ + void put(K key, V obj) throws IOException; +} diff --git a/src/main/java/com/p4square/grow/provider/ProvidesAssessments.java b/src/main/java/com/p4square/grow/provider/ProvidesAssessments.java new file mode 100644 index 0000000..62ba8f6 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesAssessments.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.RecordedAnswer; + +/** + * + * @author Jesse Morgan + */ +public interface ProvidesAssessments { + /** + * Provides a collection of user assessments. + * The collection key is the user id. + * The key is the question id. + */ + CollectionProvider getAnswerProvider(); +} diff --git a/src/main/java/com/p4square/grow/provider/ProvidesQuestions.java b/src/main/java/com/p4square/grow/provider/ProvidesQuestions.java new file mode 100644 index 0000000..b43f649 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesQuestions.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.Question; + +/** + * Indicates the ability to provide a Question Provider. + * + * @author Jesse Morgan + */ +public interface ProvidesQuestions { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider getQuestionProvider(); +} diff --git a/src/main/java/com/p4square/grow/provider/ProvidesStrings.java b/src/main/java/com/p4square/grow/provider/ProvidesStrings.java new file mode 100644 index 0000000..5d9976e --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesStrings.java @@ -0,0 +1,19 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +/** + * Indicates the ability to provide a String provider. + * + * Strings are typically configuration settings stored as a String. + * + * @author Jesse Morgan + */ +public interface ProvidesStrings { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider getStringProvider(); +} \ No newline at end of file diff --git a/src/main/java/com/p4square/grow/provider/ProvidesTrainingRecords.java b/src/main/java/com/p4square/grow/provider/ProvidesTrainingRecords.java new file mode 100644 index 0000000..586e649 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesTrainingRecords.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.Playlist; + +/** + * Indicates the ability to provide a TrainingRecord Provider. + * + * @author Jesse Morgan + */ +public interface ProvidesTrainingRecords { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider getTrainingRecordProvider(); + + /** + * @return the Default Playlist. + */ + Playlist getDefaultPlaylist() throws IOException; +} diff --git a/src/main/java/com/p4square/grow/provider/ProvidesUserRecords.java b/src/main/java/com/p4square/grow/provider/ProvidesUserRecords.java new file mode 100644 index 0000000..d77c878 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesUserRecords.java @@ -0,0 +1,19 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.UserRecord; + +/** + * Indicates the ability to provide a UserRecord Provider. + * + * @author Jesse Morgan + */ +public interface ProvidesUserRecords { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider getUserRecordProvider(); +} diff --git a/src/main/java/com/p4square/grow/provider/ProvidesVideos.java b/src/main/java/com/p4square/grow/provider/ProvidesVideos.java new file mode 100644 index 0000000..3d055d3 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/ProvidesVideos.java @@ -0,0 +1,16 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +/** + * + * @author Jesse Morgan + */ +public interface ProvidesVideos { + /** + * @return A Provider of Questions keyed by question id. + */ + CollectionProvider getVideoProvider(); +} diff --git a/src/main/java/com/p4square/grow/provider/TrainingRecordProvider.java b/src/main/java/com/p4square/grow/provider/TrainingRecordProvider.java new file mode 100644 index 0000000..44dba87 --- /dev/null +++ b/src/main/java/com/p4square/grow/provider/TrainingRecordProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import com.p4square.grow.model.TrainingRecord; + +/** + * TrainingRecordProvider wraps an existing Provider to get and put TrainingRecords. + * + * @author Jesse Morgan + */ +public abstract class TrainingRecordProvider implements Provider { + + private Provider mProvider; + + public TrainingRecordProvider(Provider provider) { + mProvider = provider; + } + + @Override + public TrainingRecord get(String key) throws IOException { + return mProvider.get(makeKey(key)); + } + + @Override + public void put(String key, TrainingRecord obj) throws IOException { + mProvider.put(makeKey(key), obj); + } + + /** + * Make a Key for a TrainingRecord.. + * + * @param userId The user id. + * @return a key for the TrainingRecord of userid. + */ + protected abstract K makeKey(String userId); +} diff --git a/src/main/java/com/p4square/grow/tools/AssessmentStats.java b/src/main/java/com/p4square/grow/tools/AssessmentStats.java new file mode 100644 index 0000000..ca83411 --- /dev/null +++ b/src/main/java/com/p4square/grow/tools/AssessmentStats.java @@ -0,0 +1,218 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.tools; + + +import java.util.Map; +import java.util.HashMap; +import java.util.Queue; +import java.util.List; +import java.util.LinkedList; +import java.io.IOException; + +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.p4square.grow.model.Answer; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.RecordedAnswer; +import com.p4square.grow.model.Score; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * + * @author Jesse Morgan + */ +public class AssessmentStats { + public static void main(String... args) throws Exception { + if (args.length == 0) { + System.out.println("Usage: AssessmentStats directory firstQuestionId"); + System.exit(1); + } + + Map questions; + questions = loadQuestions(args[0], args[1]); + + // Find the highest possible score + List scores = findHighestFromId(questions, args[1]); + + // Print Results + System.out.printf("Found %d different paths.\n", scores.size()); + int i = 0; + for (AnswerPath path : scores) { + Score s = path.mScore; + System.out.printf("Path %d: %f points, %d questions. Score: %f (%s)\n", + i++, s.getSum(), s.getCount(), s.getScore(), s.toString()); + System.out.println(" " + path.mPath); + System.out.println(" " + path.mScores); + } + } + + private static Map loadQuestions(String baseDir, String firstId) throws IOException { + FileQuestionProvider provider = new FileQuestionProvider(baseDir); + + // Questions to find... + Queue queue = new LinkedList<>(); + queue.offer(firstId); + + Map questions = new HashMap<>(); + + + while (!queue.isEmpty()) { + Question q = provider.get(queue.poll()); + questions.put(q.getId(), q); + + if (q.getNextQuestion() != null) { + queue.offer(q.getNextQuestion()); + + } + + for (Answer a : q.getAnswers().values()) { + if (a.getNextQuestion() != null) { + queue.offer(a.getNextQuestion()); + } + } + + // Quick Sanity check + if (q.getPreviousQuestion() != null) { + if (questions.get(q.getPreviousQuestion()) == null) { + throw new IllegalStateException("Haven't seen previous question??"); + } + } + } + + return questions; + } + + private static List findHighestFromId(Map questions, String id) { + List scores = new LinkedList<>(); + doFindHighestFromId(questions, id, scores, new AnswerPath()); + return scores; + } + + private static void doFindHighestFromId(Map questions, String id, List scores, AnswerPath path) { + if (id == null) { + // End of the road! Save the score and return. + scores.add(path); + return; + } + + Question q = questions.get(id); + + // Find the best answer following this path and find other paths. + Score maxScore = path.mScore; + double max = 0; + + int answerCount = 1; + for (Map.Entry entry : q.getAnswers().entrySet()) { + Answer a = entry.getValue(); + RecordedAnswer userAnswer = new RecordedAnswer(); + + if (q.getType() == Question.QuestionType.SLIDER) { + // Special Case + userAnswer.setAnswerId(String.valueOf((float) answerCount / q.getAnswers().size())); + + } else { + userAnswer.setAnswerId(entry.getKey()); + } + + Score tempScore = new Score(path.mScore); // Always start with the initial score. + boolean endOfRoad = !q.scoreAnswer(tempScore, userAnswer); + double thisScore = tempScore.getSum() - path.mScore.getSum(); + + if (endOfRoad) { + // End of Road is a fork too. Record and pick another answer. + AnswerPath fork = new AnswerPath(path); + fork.update(id, tempScore); + scores.add(fork); + + } else if (a.getNextQuestion() != null) { + // Found a new path, follow it. + // Remember to count this answer in the score. + AnswerPath fork = new AnswerPath(path); + fork.update(id, tempScore); + doFindHighestFromId(questions, a.getNextQuestion(), scores, fork); + + } else if (thisScore > max) { + // Found a higher option that isn't a new path. + maxScore = tempScore; + max = thisScore; + } + + answerCount++; + } + + path.update(id, maxScore); + doFindHighestFromId(questions, q.getNextQuestion(), scores, path); + } + + private static class FileQuestionProvider extends JsonEncodedProvider implements Provider { + private String mBaseDir; + + public FileQuestionProvider(String directory) { + super(Question.class); + mBaseDir = directory; + } + + @Override + public Question get(String key) throws IOException { + Path qfile = FileSystems.getDefault().getPath(mBaseDir, key + ".json"); + byte[] blob = Files.readAllBytes(qfile); + return decode(new String(blob)); + } + + @Override + public void put(String key, Question obj) throws IOException { + throw new UnsupportedOperationException("Not Implemented"); + } + } + + private static class AnswerPath { + String mPath; + String mScores; + Score mScore; + + public AnswerPath() { + mPath = null; + mScores = null; + mScore = new Score(); + } + + public AnswerPath(AnswerPath other) { + mPath = other.mPath; + mScores = other.mScores; + mScore = other.mScore; + } + + public void update(String questionId, Score newScore) { + String value; + + if (mScore.getCount() == newScore.getCount()) { + value = "n/a"; + + } else { + double delta = newScore.getSum() - mScore.getSum(); + if (delta < 0) { + value = "TRUMP"; + } else { + value = String.valueOf(delta); + } + } + + if (mPath == null) { + mPath = questionId; + mScores = value; + + } else { + mPath += ", " + questionId; + mScores += " + " + value; + } + + mScore = newScore; + } + } +} diff --git a/src/main/java/com/p4square/grow/tools/AttributeBackfillTool.java b/src/main/java/com/p4square/grow/tools/AttributeBackfillTool.java new file mode 100644 index 0000000..d7fd2ff --- /dev/null +++ b/src/main/java/com/p4square/grow/tools/AttributeBackfillTool.java @@ -0,0 +1,268 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.tools; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.restlet.Client; +import org.restlet.Context; +import org.restlet.data.Protocol; + +import com.p4square.f1oauth.Attribute; +import com.p4square.f1oauth.F1API; +import com.p4square.f1oauth.F1Access; +import com.p4square.f1oauth.F1Exception; +import com.p4square.restlet.oauth.OAuthUser; + +import com.p4square.grow.backend.dynamo.DynamoDatabase; +import com.p4square.grow.backend.dynamo.DynamoKey; + +import com.p4square.grow.config.Config; + +import com.p4square.grow.model.Chapter; +import com.p4square.grow.model.Playlist; +import com.p4square.grow.model.TrainingRecord; +import com.p4square.grow.model.VideoRecord; +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * This utility is used to backfill F1 Attributes from the GROW database into F1. + * + * This tool currently reads from Dynamo directly. It should probably access the + * backend or use the {@link com.p4square.grow.backend.GrowData} abstraction instead. + * + * @author Jesse Morgan + */ +public class AttributeBackfillTool { + + private static Config mConfig; + private static F1API mF1API; + private static DynamoDatabase mDatabase; + + public static void usage() { + System.out.println("java com.p4square.grow.tools.AttributeBackfillTool ...\n"); + System.out.println("Commands:"); + System.out.println("\t--domain Set config domain"); + System.out.println("\t--dev Set config domain to dev"); + System.out.println("\t--config Merge in config file"); + System.out.println("\t--assessments Backfill All Assessments"); + System.out.println("\t--training Backfill All Training Records"); + } + + public static void main(String... args) { + if (args.length == 0) { + usage(); + System.exit(1); + } + + mConfig = new Config(); + + try { + mConfig.updateConfig(AttributeTool.class.getResourceAsStream("/grow.properties")); + + int offset = 0; + while (offset < args.length) { + if ("--domain".equals(args[offset])) { + mConfig.setDomain(args[offset + 1]); + mF1API = null; + mDatabase = null; + offset += 2; + + } else if ("--dev".equals(args[offset])) { + mConfig.setDomain("dev"); + mF1API = null; + mDatabase = null; + offset += 1; + + } else if ("--config".equals(args[offset])) { + mConfig.updateConfig(args[offset + 1]); + mF1API = null; + mDatabase = null; + offset += 2; + + } else if ("--assessments".equals(args[offset])) { + offset = assessments(args, ++offset); + + } else if ("--training".equals(args[offset])) { + offset = training(args, ++offset); + + } else { + throw new IllegalArgumentException("Unknown command " + args[offset]); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(2); + } + } + + private static F1API getF1API() throws Exception { + if (mF1API == null) { + Context context = new Context(); + Client client = new Client(context, Arrays.asList(Protocol.HTTP, Protocol.HTTPS)); + context.setClientDispatcher(client); + + F1Access f1Access = new F1Access(context, + mConfig.getString("f1ConsumerKey"), + mConfig.getString("f1ConsumerSecret"), + mConfig.getString("f1BaseUrl"), + mConfig.getString("f1ChurchCode"), + F1Access.UserType.WEBLINK); + + // Gather Username and Password + String username = System.console().readLine("F1 Username: "); + char[] password = System.console().readPassword("F1 Password: "); + + OAuthUser user = f1Access.getAccessToken(username, new String(password)); + Arrays.fill(password, ' '); // Lost cause, but I'll still try. + + mF1API = f1Access.getAuthenticatedApi(user); + } + + return mF1API; + } + + private static DynamoDatabase getDatabase() { + if (mDatabase == null) { + mDatabase = new DynamoDatabase(mConfig); + } + + return mDatabase; + } + + private static int assessments(String[] args, int offset) throws Exception { + final F1API f1 = getF1API(); + final DynamoDatabase db = getDatabase(); + + DynamoKey key = DynamoKey.newKey("assessments", null); + + while (key != null) { + Map> rows = db.getAll(key); + + key = null; + + for (Map.Entry> row : rows.entrySet()) { + key = row.getKey(); + + String userId = key.getHashKey(); + + String summaryString = row.getValue().get("summary"); + if (summaryString == null || summaryString.length() == 0) { + System.out.printf("%s assessment incomplete\n", userId); + continue; + } + + try { + Map summary = JsonEncodedProvider.MAPPER.readValue(summaryString, Map.class); + + String result = (String) summary.get("result"); + if (result == null) { + System.out.printf("%s assessment incomplete\n", userId); + continue; + } + + String attributeName = "Assessment Complete - " + result; + + // Check if the user already has the attribute. + List attributes = f1.getAttribute(userId, attributeName); + + if (attributes.size() == 0) { + Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(new Date()); + attribute.setComment(summaryString); + + if (f1.addAttribute(userId, attribute)) { + System.out.printf("%s attribute added\n", userId); + } else { + System.out.printf("%s failed to add attribute\n", userId); + } + } else { + System.out.printf("%s already has attribute\n", userId); + } + } catch (Exception e) { + System.out.printf("%s exception: %s\n", userId, e.getMessage()); + } + } + } + + return offset; + } + + private static int training(String[] args, int offset) throws Exception { + final F1API f1 = getF1API(); + final DynamoDatabase db = getDatabase(); + + DynamoKey key = DynamoKey.newKey("training", null); + + while (key != null) { + Map> rows = db.getAll(key); + + key = null; + + for (Map.Entry> row : rows.entrySet()) { + key = row.getKey(); + + String userId = key.getHashKey(); + + String valueString = row.getValue().get("value"); + if (valueString == null || valueString.length() == 0) { + System.out.printf("%s empty training record\n", userId); + continue; + } + + try { + TrainingRecord record = + JsonEncodedProvider.MAPPER.readValue(valueString, TrainingRecord.class); + Playlist playlist = record.getPlaylist(); + +chapters: + for (Map.Entry entry : playlist.getChaptersMap().entrySet()) { + Chapter chapter = entry.getValue(); + + // Find completion date + Date complete = new Date(0); + for (VideoRecord vr : chapter.getVideos().values()) { + if (!vr.getComplete()) { + continue chapters; + } + + Date recordCompletion = vr.getCompletionDate(); + if (recordCompletion != null && complete.before(recordCompletion)) { + complete = vr.getCompletionDate(); + } + } + + String attributeName = "Training Complete - " + entry.getKey(); + + // Check if the user already has the attribute. + List attributes = f1.getAttribute(userId, attributeName); + + if (attributes.size() == 0) { + Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(complete); + + if (f1.addAttribute(userId, attribute)) { + System.out.printf("%s added %s\n", userId, attributeName); + } else { + System.out.printf("%s failed to add %s\n", userId, attributeName); + } + } else { + System.out.printf("%s already has %s\n", userId, attributeName); + } + } + + } catch (Exception e) { + System.out.printf("%s exception: %s\n", userId, e.getMessage()); + e.printStackTrace(); + } + } + } + + return offset; + } +} diff --git a/src/main/java/com/p4square/grow/tools/AttributeTool.java b/src/main/java/com/p4square/grow/tools/AttributeTool.java new file mode 100644 index 0000000..8e0540a --- /dev/null +++ b/src/main/java/com/p4square/grow/tools/AttributeTool.java @@ -0,0 +1,184 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.tools; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.restlet.Client; +import org.restlet.Context; +import org.restlet.data.Protocol; + +import com.p4square.grow.config.Config; +import com.p4square.f1oauth.Attribute; +import com.p4square.f1oauth.F1Access; +import com.p4square.f1oauth.F1API; +import com.p4square.f1oauth.F1Exception; +import com.p4square.restlet.oauth.OAuthUser; + +/** + * Tool for manipulating F1 Attributes. + * + * @author Jesse Morgan + */ +public class AttributeTool { + + private static Config mConfig; + private static F1API mF1API; + + public static void usage() { + System.out.println("java com.p4square.grow.tools.AttributeTool ...\n"); + System.out.println("Commands:"); + System.out.println("\t--domain Set config domain"); + System.out.println("\t--dev Set config domain to dev"); + System.out.println("\t--config Merge in config file"); + System.out.println("\t--list List all attributes"); + System.out.println("\t--assign Assign an attribute"); + System.out.println("\t--getall Get an attribute"); + System.out.println("\t--get Get an attribute"); + } + + public static void main(String... args) { + if (args.length == 0) { + usage(); + System.exit(1); + } + + mConfig = new Config(); + + try { + mConfig.updateConfig(AttributeTool.class.getResourceAsStream("/grow.properties")); + + int offset = 0; + while (offset < args.length) { + if ("--domain".equals(args[offset])) { + mConfig.setDomain(args[offset + 1]); + mF1API = null; + offset += 2; + + } else if ("--dev".equals(args[offset])) { + mConfig.setDomain("dev"); + mF1API = null; + offset += 1; + + } else if ("--config".equals(args[offset])) { + mConfig.updateConfig(args[offset + 1]); + mF1API = null; + offset += 2; + + } else if ("--list".equals(args[offset])) { + offset = list(args, ++offset); + + } else if ("--assign".equals(args[offset])) { + offset = assign(args, ++offset); + + } else if ("--getall".equals(args[offset])) { + offset = getall(args, ++offset); + + } else if ("--get".equals(args[offset])) { + offset = get(args, ++offset); + + } else { + throw new IllegalArgumentException("Unknown command " + args[offset]); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(2); + } + } + + private static F1API getF1API() throws Exception { + if (mF1API == null) { + Context context = new Context(); + Client client = new Client(context, Arrays.asList(Protocol.HTTP, Protocol.HTTPS)); + context.setClientDispatcher(client); + + F1Access f1Access = new F1Access(context, + mConfig.getString("f1ConsumerKey"), + mConfig.getString("f1ConsumerSecret"), + mConfig.getString("f1BaseUrl"), + mConfig.getString("f1ChurchCode"), + F1Access.UserType.WEBLINK); + + // Gather Username and Password + String username = System.console().readLine("F1 Username: "); + char[] password = System.console().readPassword("F1 Password: "); + + OAuthUser user = f1Access.getAccessToken(username, new String(password)); + Arrays.fill(password, ' '); // Lost cause, but I'll still try. + + mF1API = f1Access.getAuthenticatedApi(user); + } + + return mF1API; + } + + private static int list(String[] args, int offset) throws Exception { + final F1API f1 = getF1API(); + + final Map attributes = f1.getAttributeList(); + System.out.printf("%7s %s\n", "ID", "Name"); + for (Map.Entry entry : attributes.entrySet()) { + System.out.printf("%7s %s\n", entry.getValue(), entry.getKey()); + } + + return offset; + } + + private static int assign(String[] args, int offset) throws Exception { + final String userId = args[offset++]; + final String attributeName = args[offset++]; + final String comment = args[offset++]; + + final F1API f1 = getF1API(); + + Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(new Date()); + attribute.setComment(comment); + + if (f1.addAttribute(userId, attribute)) { + System.out.println("Added attribute " + attributeName + " for " + userId); + } else { + System.out.println("Failed to add attribute " + attributeName + " for " + userId); + } + + return offset; + } + + private static int getall(String[] args, int offset) throws Exception { + final String userId = args[offset++]; + + doGet(userId, null); + + return offset; + } + + private static int get(String[] args, int offset) throws Exception { + final String userId = args[offset++]; + final String attributeName = args[offset++]; + + doGet(userId, attributeName); + + return offset; + } + + private static void doGet(final String userId, final String attributeName) throws Exception { + final F1API f1 = getF1API(); + + List attributes = f1.getAttribute(userId, attributeName); + for (Attribute attribute : attributes) { + System.out.printf("%s %s %s %s %s\n%s\n\n", + userId, + attribute.getAttributeName(), + attribute.getId(), + attribute.getStartDate(), + attribute.getEndDate(), + attribute.getComment()); + } + } +} diff --git a/src/main/java/com/p4square/restlet/metrics/MetricRouter.java b/src/main/java/com/p4square/restlet/metrics/MetricRouter.java new file mode 100644 index 0000000..d4da270 --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricRouter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.routing.TemplateRoute; +import org.restlet.routing.Router; + +/** + * + * @author Jesse Morgan + */ +public class MetricRouter extends Router { + + private final MetricRegistry mMetricRegistry; + + public MetricRouter(Context context, MetricRegistry metrics) { + super(context); + mMetricRegistry = metrics; + } + + @Override + protected void doHandle(Restlet next, Request request, Response response) { + String baseName; + if (next instanceof TemplateRoute) { + TemplateRoute temp = (TemplateRoute) next; + baseName = MetricRegistry.name("MetricRouter", temp.getTemplate().getPattern()); + } else { + baseName = MetricRegistry.name("MetricRouter", "unknown"); + } + + final Timer.Context aggTimer = mMetricRegistry.timer("MetricRouter.time").time(); + final Timer.Context timer = mMetricRegistry.timer(baseName + ".time").time(); + + try { + super.doHandle(next, request, response); + } finally { + timer.stop(); + aggTimer.stop(); + + // Record status code + boolean success = !response.getStatus().isError(); + if (success) { + mMetricRegistry.counter("MetricRouter.success").inc(); + mMetricRegistry.counter(baseName + ".response.success").inc(); + } else { + mMetricRegistry.counter("MetricRouter.failure").inc(); + mMetricRegistry.counter(baseName + ".response.failure").inc(); + } + } + } +} diff --git a/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java b/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java new file mode 100644 index 0000000..6caf742 --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.json.MetricsModule; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.restlet.Application; +import org.restlet.Restlet; +import org.restlet.resource.Finder; + +/** + * + * @author Jesse Morgan + */ +public class MetricsApplication extends Application { + static final ObjectMapper MAPPER; + static { + MAPPER = new ObjectMapper(); + MAPPER.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, true)); + } + + private final MetricRegistry mMetricRegistry; + + public MetricsApplication(MetricRegistry metrics) { + mMetricRegistry = metrics; + } + + public MetricRegistry getMetricRegistry() { + return mMetricRegistry; + } + + @Override + public Restlet createInboundRoot() { + return new Finder(getContext(), MetricsResource.class); + } +} diff --git a/src/main/java/com/p4square/restlet/metrics/MetricsResource.java b/src/main/java/com/p4square/restlet/metrics/MetricsResource.java new file mode 100644 index 0000000..e2ab14d --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricsResource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import com.codahale.metrics.MetricRegistry; + +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.ServerResource; + +/** + * + * @author Jesse Morgan + */ +public class MetricsResource extends ServerResource { + + private MetricRegistry mMetricRegistry; + + @Override + public void doInit() { + mMetricRegistry = ((MetricsApplication) getApplication()).getMetricRegistry(); + } + + @Override + protected Representation get() { + JacksonRepresentation rep = new JacksonRepresentation<>(mMetricRegistry); + rep.setObjectMapper(MetricsApplication.MAPPER); + return rep; + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java new file mode 100644 index 0000000..c33bb5a --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.apache.log4j.Logger; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; +import org.restlet.security.User; + +/** + * Authenticator which makes an OAuth request to authenticate the user. + * + * If this Authenticator is made optional than no requests are made to the + * service provider. + * + * @author Jesse Morgan + */ +public class OAuthAuthenticator extends Authenticator { + private static Logger LOG = Logger.getLogger(OAuthAuthenticator.class); + + private static final String OAUTH_TOKEN = "oauth_token"; + private static final String COOKIE_NAME = "oauth_secret"; + + private final OAuthHelper mHelper; + + /** + * Create a new Authenticator. + * + * @param Context the current context. + * @param optional If true, unauthenticated users are allowed to continue. + * @param helper The OAuthHelper which will help with the requests. + */ + public OAuthAuthenticator(Context context, boolean optional, OAuthHelper helper) { + super(context, false, optional, null); + + mHelper = helper; + } + + protected boolean authenticate(Request request, Response response) { + /* + * The authentication workflow has three steps: + * 1. Get RequestToken + * 2. Authenticate the user + * 3. Get AccessToken + * + * The authentication workflow is broken into two stages. In the first, + * we generate the RequestToken (step 1) and redirect the user to the + * authentication page. When the user comes back, we will request the + * AccessToken (step 2). + * + * We determine which half we are in by the presence of the oauth_token + * parameter in the query string. + */ + + final String token = request.getResourceRef().getQueryAsForm().getFirstValue(OAUTH_TOKEN); + final String secret = request.getCookies().getFirstValue(COOKIE_NAME); + + try { + if (token == null) { + if (isOptional()) { + return false; + } + + // 1. Get RequestToken + Token requestToken = mHelper.getRequestToken(); + + if (requestToken == null) { + return false; + } + + // 2. Redirect user + // TODO Encrypt cookie + response.getCookieSettings().add(COOKIE_NAME, requestToken.getSecret()); + response.redirectSeeOther(mHelper.getLoginUrl(requestToken, request.getResourceRef().toString())); + return false; + + } else { + // 3. Get AccessToken + Token requestToken = new Token(token, secret); + User user = mHelper.getAccessToken(requestToken); + request.getClientInfo().setUser(user); + return true; + } + + } catch (OAuthException e) { + LOG.debug("Authentication failed: " + e); + return false; + } + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java new file mode 100644 index 0000000..76ff044 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import java.net.URLEncoder; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import java.util.Collections; +import java.util.Random; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.ChallengeRequest; +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; +import org.restlet.data.CharacterSet; +import org.restlet.data.Form; +import org.restlet.data.Method; +import org.restlet.data.Parameter; +import org.restlet.data.Reference; +import org.restlet.engine.header.ChallengeWriter; +import org.restlet.engine.header.Header; +import org.restlet.engine.security.AuthenticatorHelper; +import org.restlet.engine.util.Base64; +import org.restlet.util.Series; + +/** + * Authentication helper for signing OAuth Requests. + * + * This implementation is limited to one consumer token/secret per restlet + * engine. In practice this means you will only be able to interact with one + * service provider unless you loaded/unloaded the AuthenticationHelper for + * each request. + * + * @author Jesse Morgan + */ +public class OAuthAuthenticatorHelper extends AuthenticatorHelper { + private static final String SIGNATURE_METHOD = "HMAC-SHA1"; + private static final String JAVA_SIGNATURE_METHOD = "HmacSHA1"; + private static final String ENCODING = "UTF-8"; + + private final Random mRandom; + private final Token mConsumerToken; + + /** + * Package-private constructor. + * + * This class should only be instantiated by OAuthHelper. + */ + OAuthAuthenticatorHelper(Token consumerToken) { + super(ChallengeScheme.HTTP_OAUTH, true, false); + + mRandom = new Random(); + mConsumerToken = consumerToken; + } + + @Override + public void formatRequest(ChallengeWriter cw, ChallengeRequest cr, + Response response, Series
httpHeaders) throws IOException { + + throw new UnsupportedOperationException("OAuth Requests are not implemented"); + } + + @Override + public void formatResponse(ChallengeWriter cw, ChallengeResponse response, + Request request, Series
httpHeaders) { + + try { + Series authParams = new Series(Parameter.class); + + String nonce = String.valueOf(mRandom.nextInt()); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + authParams.add(new Parameter("oauth_consumer_key", mConsumerToken.getToken())); + authParams.add(new Parameter("oauth_nonce", nonce)); + authParams.add(new Parameter("oauth_signature_method", SIGNATURE_METHOD)); + authParams.add(new Parameter("oauth_timestamp", timestamp)); + authParams.add(new Parameter("oauth_version", "1.0")); + + String accessToken = response.getIdentifier(); + if (accessToken != null) { + authParams.add(new Parameter("oauth_token", accessToken)); + } + + // Generate Signature + String signature = generateSignature(response, request, authParams); + authParams.add(new Parameter("oauth_signature", signature)); + + // Write Header + for (Parameter p : authParams) { + cw.appendQuotedChallengeParameter(encode(p.getName()), encode(p.getValue())); + } + + } catch (IOException e) { + throw new RuntimeException(e); + + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Helper method to generate an OAuth Signature. + */ + private String generateSignature(ChallengeResponse response, Request request, + Series authParams) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + UnsupportedEncodingException { + + // HTTP Request Method + String httpMethod = request.getMethod().getName(); + + // Request Url + Reference url = request.getResourceRef(); + String requestUrl = encode(url.getScheme() + ":" + url.getHierarchicalPart()); + + // Normalized parameters + Series params = new Series(Parameter.class); + + // OAUTH Params + params.addAll(authParams); + + // Query Params + Form query = url.getQueryAsForm(); + params.addAll(query); + + // Sort it + Collections.sort(params); + + StringBuilder normalizedParamsBuilder = new StringBuilder(); + for (Parameter p : params) { + normalizedParamsBuilder.append('&'); + normalizedParamsBuilder.append(p.encode(CharacterSet.UTF_8)); + } + String normalizedParams = encode(normalizedParamsBuilder.substring(1)); // remove the first & + + // Generate signature base + String sigBase = httpMethod + "&" + requestUrl + "&" + normalizedParams.toString(); + + // Sign the signature base + Mac mac = Mac.getInstance(JAVA_SIGNATURE_METHOD); + + String accessTokenSecret = ""; + if (response.getIdentifier() != null) { + accessTokenSecret = new String(response.getSecret()); + } + + byte[] keyBytes = (encode(mConsumerToken.getSecret()) + "&" + encode(accessTokenSecret)).getBytes(ENCODING); + SecretKey key = new SecretKeySpec(keyBytes, JAVA_SIGNATURE_METHOD); + mac.init(key); + + byte[] signature = mac.doFinal(sigBase.getBytes(ENCODING)); + + return Base64.encode(signature, false).trim(); + } + + /** + * Helper method to URL Encode Strings. + */ + private String encode(String input) throws UnsupportedEncodingException { + return URLEncoder.encode(input, ENCODING); + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthException.java b/src/main/java/com/p4square/restlet/oauth/OAuthException.java new file mode 100644 index 0000000..dd326d3 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.Status; + +/** + * Exception throw when the service provider returns an error. + * + * @author Jesse Morgan + */ +public class OAuthException extends Exception { + private final Status mStatus; + + public OAuthException(Status status) { + super("Service provider failed request: " + status.getDescription()); + mStatus = status; + } + + public Status getStatus() { + return mStatus; + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthHelper.java b/src/main/java/com/p4square/restlet/oauth/OAuthHelper.java new file mode 100644 index 0000000..67dd238 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthHelper.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import java.net.URLEncoder; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +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.data.Status; +import org.restlet.engine.Engine; +import org.restlet.representation.Representation; + +/** + * Helper Class for OAuth 1.0 Authentication. + * + * @author Jesse Morgan + */ +public abstract class OAuthHelper { + private final Restlet mDispatcher; + private final Token mConsumerToken; + + /** + * Create a new OAuth Helper. + * As currently implemented, there can only be one OAuthHelper per Restlet + * Engine since this class registers its own provider for the OAuth + * authentication protocol. + * + * FIXME: This could be improved by making OAuthAuthenticationHelper and + * maybe Token aware of multiple service providers. + * + * @param context The restlet context which provides a ClientDispatcher. + * @param consumerKey The OAuth consumer key for this application. + * @param consumerSecret the OAuth consumer secret for this application. + */ + public OAuthHelper(Context context, String consumerKey, String consumerSecret) { + mDispatcher = context.getClientDispatcher(); + mConsumerToken = new Token(consumerKey, consumerSecret); + + Engine.getInstance().getRegisteredAuthenticators().add(new OAuthAuthenticatorHelper(mConsumerToken)); + } + + /** + * @return the URL for the initial RequestToken request. + */ + protected abstract String getRequestTokenUrl(); + + /** + * Request a RequestToken. + * + * @return a Token containing the RequestToken. + * @throws OAuthException if the request fails. + */ + public Token getRequestToken() throws OAuthException { + Request request = new Request(Method.GET, getRequestTokenUrl()); + request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); + + Response response = mDispatcher.handle(request); + + return processTokenRequest(response); + } + + /** + * @return the URL to redirect the user to for Authentication. + */ + public abstract String getLoginUrl(Token requestToken, String callback); + + /** + * @return the URL for the AccessToken request. + */ + protected abstract String getAccessTokenUrl(); + + /** + * Request an AccessToken for a previously authenticated RequestToken. + * + * @return an OAuthUser object containing the AccessToken. + * @throws OAuthException if the request fails. + */ + public OAuthUser getAccessToken(Token requestToken) throws OAuthException { + Request request = new Request(Method.GET, getAccessTokenUrl()); + request.setChallengeResponse(requestToken.getChallengeResponse()); + + return processAccessTokenRequest(request); + } + + /** + * Helper method to decode the token returned from an OAuth Request. + * + * @param response The Response object from the Request. + * @return the Token from the oauth_token and oauth_token_secret parameters. + * @throws OAuthException is the server reported an error. + */ + protected Token processTokenRequest(Response response) throws OAuthException { + Status status = response.getStatus(); + Representation entity = response.getEntity(); + + try { + if (status.isSuccess()) { + Form form = new Form(entity); + String token = form.getFirstValue("oauth_token"); + String secret = form.getFirstValue("oauth_token_secret"); + + return new Token(token, secret); + + } else { + throw new OAuthException(status); + } + } finally { + entity.release(); + } + } + + /** + * Helper method to create an OAuthUser from the AccessToken request. + * + * The User's identifier is set to the Content-Location header, if present. + * + * @param response The Response to the AccessToken Request. + * @return An OAuthUser object wrapping the AccessToken. + * @throws OAuthException if the request failed. + */ + public OAuthUser processAccessTokenRequest(Request request) throws OAuthException { + Response response = getResponse(request); + Token accessToken = processTokenRequest(response); + + Reference ref = response.getEntity().getLocationRef(); + if (ref != null) { + return new OAuthUser(ref.toString(), accessToken); + + } else { + return new OAuthUser(accessToken); + } + } + + /** + * Helper method to get a Response for a Request. + */ + public Response getResponse(Request request) { + return mDispatcher.handle(request); + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthUser.java b/src/main/java/com/p4square/restlet/oauth/OAuthUser.java new file mode 100644 index 0000000..11dbac1 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthUser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.ChallengeResponse; +import org.restlet.security.User; + +/** + * Simple User object which also contains an OAuth AccessToken. + * + * @author Jesse Morgan + */ +public class OAuthUser extends User { + private final Token mToken; + private final String mContentLocation; + + public OAuthUser(Token token) { + this(null, token); + } + + public OAuthUser(String location, Token token) { + super(); + mToken = token; + mContentLocation = location; + } + + /** + * @return the Location associated with the user. + */ + public String getLocation() { + return mContentLocation; + } + + /** + * @return The AccessToken. + */ + public Token getToken() { + return mToken; + } + + /** + * Convenience method for getToken().getChallengeResponse(). + * @return A ChallengeResponse based upon the access token. + */ + public ChallengeResponse getChallengeResponse() { + return mToken.getChallengeResponse(); + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/Token.java b/src/main/java/com/p4square/restlet/oauth/Token.java new file mode 100644 index 0000000..51a9087 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/Token.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; + +/** + * Token wraps the two Strings which make up an OAuth Token: the public + * component and the private component. + * + * @author Jesse Morgan + */ +public class Token { + private final String mToken; + private final String mSecret; + + public Token(String token, String secret) { + mToken = token; + mSecret = secret; + } + + /** + * @return the public component. + */ + public String getToken() { + return mToken; + } + + /** + * @return the secret component. + */ + public String getSecret() { + return mSecret; + } + + @Override + public String toString() { + return mToken + "&" + mSecret; + } + + /** + * Generate a ChallengeResponse based on this Token. + * + * @return a ChallengeResponse object using the OAUTH ChallengeScheme. + */ + public ChallengeResponse getChallengeResponse() { + return new ChallengeResponse(ChallengeScheme.HTTP_OAUTH, mToken, mSecret); + } +} diff --git a/src/main/java/com/p4square/session/Session.java b/src/main/java/com/p4square/session/Session.java new file mode 100644 index 0000000..1bb65f5 --- /dev/null +++ b/src/main/java/com/p4square/session/Session.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.restlet.security.User; + +/** + * + * @author Jesse Morgan + */ +public class Session { + static final long LIFETIME = 86400000; + + private final String mSessionId; + private final User mUser; + private final Map mData; + private long mExpires; + + Session(User user) { + mUser = user; + mSessionId = UUID.randomUUID().toString(); + mExpires = System.currentTimeMillis() + LIFETIME; + mData = new HashMap(); + } + + void touch() { + mExpires = System.currentTimeMillis() + LIFETIME; + } + + boolean isExpired() { + return System.currentTimeMillis() > mExpires; + } + + public String getId() { + return mSessionId; + } + + public Object get(String key) { + return mData.get(key); + } + + public void put(String key, String value) { + mData.put(key, value); + } + + public User getUser() { + return mUser; + } + + public Map getMap() { + return mData; + } +} diff --git a/src/main/java/com/p4square/session/SessionAuthenticator.java b/src/main/java/com/p4square/session/SessionAuthenticator.java new file mode 100644 index 0000000..794e1a8 --- /dev/null +++ b/src/main/java/com/p4square/session/SessionAuthenticator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; +import org.restlet.security.User; + +/** + * + * @author Jesse Morgan + */ +public class SessionAuthenticator /*extends Authenticator*/ { + /* + @Override + protected boolean authenticate(Request request, Response response) { + // Check for authentication cookie + final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); + if (cookie != null) { + cLog.debug("Got cookie: " + cookie); + // TODO Decrypt user info + User user = new User(cookie); + request.getClientInfo().setUser(user); + return true; + } + + // Challenge the user if not authenticated + response.redirectSeeOther(mLoginPage); + return false; + } + */ +} diff --git a/src/main/java/com/p4square/session/SessionCheckingAuthenticator.java b/src/main/java/com/p4square/session/SessionCheckingAuthenticator.java new file mode 100644 index 0000000..489d6a0 --- /dev/null +++ b/src/main/java/com/p4square/session/SessionCheckingAuthenticator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import org.apache.log4j.Logger; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; + +/** + * Authenticator which succeeds if a valid Session exists. + * + * @author Jesse Morgan + */ +public class SessionCheckingAuthenticator extends Authenticator { + private static final Logger LOG = Logger.getLogger(SessionCheckingAuthenticator.class); + + public SessionCheckingAuthenticator(Context context, boolean optional) { + super(context, optional); + } + + protected boolean authenticate(Request request, Response response) { + Session s = Sessions.getInstance().get(request); + + if (s != null) { + LOG.debug("Found session for user " + s.getUser()); + request.getClientInfo().setUser(s.getUser()); + return true; + + } else { + return false; + } + } + +} diff --git a/src/main/java/com/p4square/session/SessionCookieAuthenticator.java b/src/main/java/com/p4square/session/SessionCookieAuthenticator.java new file mode 100644 index 0000000..0074b77 --- /dev/null +++ b/src/main/java/com/p4square/session/SessionCookieAuthenticator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import org.apache.log4j.Logger; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; + +/** + * + * @author Jesse Morgan + */ +public class SessionCookieAuthenticator extends Authenticator { + private static final Logger LOG = Logger.getLogger(SessionCookieAuthenticator.class); + + private static final String COOKIE_NAME = "S"; + + private final Sessions mSessions; + + public SessionCookieAuthenticator(Context context, boolean optional, Sessions sessions) { + super(context, optional); + + mSessions = sessions; + } + + protected boolean authenticate(Request request, Response response) { + final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); + + if (request.getClientInfo().isAuthenticated()) { + // Request is already authenticated... create session if it doesn't exist. + if (cookie == null) { + Session s = mSessions.create(request.getClientInfo().getUser()); + response.getCookieSettings().add(COOKIE_NAME, s.getId()); + } + + return true; + + } else { + // Check for authentication cookie + if (cookie != null) { + LOG.debug("Got cookie: " + cookie); + + Session s = mSessions.get(cookie); + if (s != null) { + request.getClientInfo().setUser(s.getUser()); + return true; + } + } + + return false; + } + } + +} diff --git a/src/main/java/com/p4square/session/SessionCreatingAuthenticator.java b/src/main/java/com/p4square/session/SessionCreatingAuthenticator.java new file mode 100644 index 0000000..3ec14b4 --- /dev/null +++ b/src/main/java/com/p4square/session/SessionCreatingAuthenticator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import org.apache.log4j.Logger; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; +import org.restlet.security.User; + +/** + * Authenticator which creates a Session for the request and adds a cookie + * to the response. + * + * The Request MUST be Authenticated and MUST have a User object associated. + * + * @author Jesse Morgan + */ +public class SessionCreatingAuthenticator extends Authenticator { + private static final Logger LOG = Logger.getLogger(SessionCreatingAuthenticator.class); + + public SessionCreatingAuthenticator(Context context) { + super(context, true); + } + + protected boolean authenticate(Request request, Response response) { + if (Sessions.getInstance().get(request) != null) { + return true; + } + + User user = request.getClientInfo().getUser(); + + if (request.getClientInfo().isAuthenticated() && user != null) { + Sessions.getInstance().create(request, response); + LOG.debug(response); + return true; + } + + return false; + } + +} diff --git a/src/main/java/com/p4square/session/Sessions.java b/src/main/java/com/p4square/session/Sessions.java new file mode 100644 index 0000000..9f9dda0 --- /dev/null +++ b/src/main/java/com/p4square/session/Sessions.java @@ -0,0 +1,155 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.session; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import org.restlet.Response; +import org.restlet.Request; +import org.restlet.data.CookieSetting; +import org.restlet.security.User; + +/** + * Singleton Session Manager. + * + * @author Jesse Morgan + */ +public class Sessions { + private static final String COOKIE_NAME = "S"; + private static final int DELETE = 0; + + private static final Sessions THE = new Sessions(); + public static Sessions getInstance() { + return THE; + } + + private final Map mSessions; + private final Timer mCleanupTimer; + + private Sessions() { + mSessions = new ConcurrentHashMap(); + + mCleanupTimer = new Timer("sessionCleaner", true); + mCleanupTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + for (Session s : mSessions.values()) { + if (s.isExpired()) { + mSessions.remove(s.getId()); + } + } + } + }, Session.LIFETIME, Session.LIFETIME); + } + + /** + * Get a session by ID. + * + * @param sessionid + * The Session id + * @return The Session if found and not expired, null otherwise. + */ + public Session get(String sessionid) { + Session s = mSessions.get(sessionid); + + if (s != null && !s.isExpired()) { + s.touch(); + return s; + } + + return null; + } + + /** + * Get the Session associated with the Request. + * + * @param request + * The request to fetch a session for. + * @return A session or null if no session is found. + */ + public Session get(Request request) { + final String cookie = request.getCookies().getFirstValue(COOKIE_NAME); + + if (cookie != null) { + return get(cookie); + } + + return null; + } + + /** + * Create a new Session for the given User object. + * + * @param user + * The User to associate with the Session. + * @return The new Session object. + */ + public Session create(User user) { + if (user == null) { + throw new IllegalArgumentException("Can not create session for null user."); + } + + Session s = new Session(user); + mSessions.put(s.getId(), s); + + return s; + } + + /** + * Delete a Session. + * + * @param sessionid + * The id of the Session to remove. + */ + public void delete(String sessionid) { + mSessions.remove(sessionid); + } + + /** + * Create a new Session and add the Session cookie to the response. + * + * @param request + * The request to create the Session for. + * @param response + * The response to add the session cookie to. + * @return The new Session. + */ + public Session create(Request request, Response response) { + Session s = create(request.getClientInfo().getUser()); + + CookieSetting cookie = new CookieSetting(COOKIE_NAME, s.getId()); + cookie.setPath("/"); + + request.getCookies().add(cookie); + response.getCookieSettings().add(cookie); + + return s; + } + + /** + * Remove a Session and delete the cookies. + * + * @param request + * The request with the session cookie to remove + * @param response + * The response to remove the session cookie from. + */ + public void delete(Request request, Response response) { + final String sessionid = request.getCookies().getFirstValue(COOKIE_NAME); + + delete(sessionid); + + CookieSetting cookie = new CookieSetting(COOKIE_NAME, ""); + cookie.setPath("/"); + cookie.setMaxAge(DELETE); + + request.getCookies().add(cookie); + response.getCookieSettings().add(cookie); + } + +} diff --git a/src/main/resources/com/p4square/grow/backend/apiinfo.html b/src/main/resources/com/p4square/grow/backend/apiinfo.html new file mode 100644 index 0000000..a3637c9 --- /dev/null +++ b/src/main/resources/com/p4square/grow/backend/apiinfo.html @@ -0,0 +1,41 @@ + + +API Info + + +
+
/backend/accounts/{userId}
+
GET information about userId or PUT new information.
+ +
/backend/assessment/question/{questionId}
+
GET information about questionId. Special questionIds: first identifies first question. count returns total number of questions.
+ +
/backend/accounts/{userId}/assessment
+
GET the assessment summary for userId or DELETE userId's assessment.
+ +
/backend/accounts/{userId}/assessment/answers/{questionId}
+
GET userId's answer to questionId, PUT a new answer, or DELETE an answer.
+ +
/backend/training/{level}
+
GET all video information for level.
+ +
/backend/training/{level}/videos/{videoId}
+
GET video information for videoId in level.
+ +
/backend/accounts/{userId}/training
+
GET training record summary for userId.
+ +
/backend/accounts/{userId}/training/videos/{videoId}
+
GET training record for userId and videoId or PUT a new record.
+ +
/backend/banner
+
GET the info banner or PUT new banner info.
+ +
/backend/feed/{topic}
+
Get all threads for forum topic.
+ +
/backend/feed/{topic}/{thread}
+
Get all responses to question thread on forum topic.
+
+ + diff --git a/src/main/resources/grow.properties b/src/main/resources/grow.properties new file mode 100644 index 0000000..53075fe --- /dev/null +++ b/src/main/resources/grow.properties @@ -0,0 +1,15 @@ +# Frontend Settings +*.backendUri = riap://component/backend +*.staticRoot = +*.dynamicRoot = + +prod.postAccountCreationPage = http://foursquaregrow.com/login.html + +# Backend Settings +prod.clusterName = Prod Cluster +dev.clusterName = Dev Cluster + +*.awsRegion = us-west-2 +prod.dynamoTablePrefix = grow-prod- +serverprod.dynamoTablePrefix = grow-prod- +dev.dynamoTablePrefix = grow-dev- diff --git a/src/main/resources/jetty-logging.properties b/src/main/resources/jetty-logging.properties new file mode 100644 index 0000000..c291cb8 --- /dev/null +++ b/src/main/resources/jetty-logging.properties @@ -0,0 +1 @@ +org.eclipse.jetty.LEVEL=INFO diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..c1e2ecb --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,15 @@ +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=DEBUG, stdout, logfile + +log4j.loggger.org.eclipse = WARN + +# stdout appender +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n + +# service.log appender +log4j.appender.logfile=org.apache.log4j.FileAppender +log4j.appender.logfile.File=service.log +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +log4j.appender.logfile.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/src/main/resources/templates/macros/common-page.ftl b/src/main/resources/templates/macros/common-page.ftl new file mode 100644 index 0000000..1780036 --- /dev/null +++ b/src/main/resources/templates/macros/common-page.ftl @@ -0,0 +1,29 @@ +<#macro commonpage> + <#compress> + + + + Grow Process + + + + + + + + +
+ <#include "/templates/banner.ftl"> + <#include "/templates/header.ftl"> + + <#nested> + +
+
+ + <#include "/templates/footer.ftl"> + + + + + diff --git a/src/main/resources/templates/macros/common.ftl b/src/main/resources/templates/macros/common.ftl new file mode 100644 index 0000000..d388a4e --- /dev/null +++ b/src/main/resources/templates/macros/common.ftl @@ -0,0 +1,4 @@ +<#include "content.ftl"> +<#include "noticebox.ftl"> +<#assign dynamicRoot = ""> +<#assign staticRoot = ""> diff --git a/src/main/resources/templates/macros/content.ftl b/src/main/resources/templates/macros/content.ftl new file mode 100644 index 0000000..eaf0b17 --- /dev/null +++ b/src/main/resources/templates/macros/content.ftl @@ -0,0 +1,7 @@ +<#macro content class=""> +
+
+ <#nested> +
+
+ diff --git a/src/main/resources/templates/macros/hms.ftl b/src/main/resources/templates/macros/hms.ftl new file mode 100644 index 0000000..339b8a9 --- /dev/null +++ b/src/main/resources/templates/macros/hms.ftl @@ -0,0 +1,25 @@ +<#macro hms seconds> + <#assign h = (seconds / 3600)?int /> + <#assign m = (seconds % 3600 / 60)?int /> + <#assign s = (seconds % 3600 % 60)?int /> + + <#if (h < 10)> + <#assign h = "0${h}" /> + + + <#if (m < 10)> + <#assign m = "0${m}" /> + + + <#if (s < 10)> + <#assign s = "0${s}" /> + + + <#if (seconds >= 3600)> + ${h}:${m}:${s} + <#elseif (seconds >= 60)> + ${m}:${s} + <#else> + ${s} seconds + + diff --git a/src/main/resources/templates/macros/noticebox.ftl b/src/main/resources/templates/macros/noticebox.ftl new file mode 100644 index 0000000..34ef994 --- /dev/null +++ b/src/main/resources/templates/macros/noticebox.ftl @@ -0,0 +1,10 @@ +<#macro noticebox class=""> +
+
+

+ + <#nested> +

+
+
+ diff --git a/src/main/resources/templates/pages/assessment.html.ftl b/src/main/resources/templates/pages/assessment.html.ftl new file mode 100644 index 0000000..3737dc6 --- /dev/null +++ b/src/main/resources/templates/pages/assessment.html.ftl @@ -0,0 +1,43 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

Assessment

+ +

+ Welcome to the start of GROW and the GROW personal assessment. The + purpose of this 18-question assessment will identify where you are + in the spiritual development process: +

+ +

+ Growth Process +

+ +

+ Upon completion of the assessment, you will be invited to join the + process at the appropriate stage. For example if the assessment + returns a “believer” result, you will be invited to start the + Believer level of this process. +

+ +

+ Once you have completed this level, you will be invited to begin the next stage of GROW. +

+ +

+ Let's get with your personal GROW assessment now, it will only take a few minutes. +

+ + + + + + + diff --git a/src/main/resources/templates/pages/contact.html.ftl b/src/main/resources/templates/pages/contact.html.ftl new file mode 100644 index 0000000..c84e047 --- /dev/null +++ b/src/main/resources/templates/pages/contact.html.ftl @@ -0,0 +1,18 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

Contact Us

+ +

+ If you have any questions about GROW, please email us at connect@myfoursquarechurch.com or call us at 253-848-9111. +

+ + + + + diff --git a/src/main/resources/templates/pages/deeper/believer.html.ftl b/src/main/resources/templates/pages/deeper/believer.html.ftl new file mode 100644 index 0000000..2bf9150 --- /dev/null +++ b/src/main/resources/templates/pages/deeper/believer.html.ftl @@ -0,0 +1,51 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+
+ <#include "/templates/deeperheader.ftl"> + +

Assessments

+
    +
  • + Spiritual Gifts Assessment +

    + This is a spiritual gifts assessment. We all have + gifts from God to be understood and utilized and used + for Him. This link is a tool to help you identify your + spiritual gifts so that you can better understand your + fit in the body of Christ. +

    +
  • +
+

Reading List

+ +

Other

+ +
+
+ diff --git a/src/main/resources/templates/pages/deeper/disciple.html.ftl b/src/main/resources/templates/pages/deeper/disciple.html.ftl new file mode 100644 index 0000000..dd7fc13 --- /dev/null +++ b/src/main/resources/templates/pages/deeper/disciple.html.ftl @@ -0,0 +1,49 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+
+ <#include "/templates/deeperheader.ftl"> + +

Assessments

+
    +
  • + Transformational Disciple Assessment +

    + The Transformational Discipleship Assessment (TDA) + is an advanced tool to dive more deeply into + opportunities for your customized discipleship + journey. This is an excellent tool to help you + take your spiritual formation to the next level. +

    +
  • +
+

Reading List

+ +

Other

+ +
+
+ diff --git a/src/main/resources/templates/pages/deeper/leader.html.ftl b/src/main/resources/templates/pages/deeper/leader.html.ftl new file mode 100644 index 0000000..371c367 --- /dev/null +++ b/src/main/resources/templates/pages/deeper/leader.html.ftl @@ -0,0 +1,23 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+ +
+ diff --git a/src/main/resources/templates/pages/deeper/seeker.html.ftl b/src/main/resources/templates/pages/deeper/seeker.html.ftl new file mode 100644 index 0000000..ed0b21a --- /dev/null +++ b/src/main/resources/templates/pages/deeper/seeker.html.ftl @@ -0,0 +1,30 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+ +
+ + diff --git a/src/main/resources/templates/pages/deeper/teacher.html.ftl b/src/main/resources/templates/pages/deeper/teacher.html.ftl new file mode 100644 index 0000000..f0046f2 --- /dev/null +++ b/src/main/resources/templates/pages/deeper/teacher.html.ftl @@ -0,0 +1,42 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+
+ <#include "/templates/deeperheader.ftl"> + +

Assessments

+
    +
  • + Transformational Disciple Assessment +

    + The Transformational Discipleship Assessment (TDA) + is an advanced tool to dive more deeply into + opportunities for your customized discipleship + journey. This is an excellent tool to help you + take your spiritual formation to the next level. +

    +
  • +
+

Reading List

+ +

Other

+ +
+
+ diff --git a/src/main/resources/templates/pages/index.html.ftl b/src/main/resources/templates/pages/index.html.ftl new file mode 100644 index 0000000..59a5e38 --- /dev/null +++ b/src/main/resources/templates/pages/index.html.ftl @@ -0,0 +1,50 @@ +<#include "/macros/common.ftl"> + + + + Grow Process + + + + + + + + +
+ <#include "/templates/banner.ftl"> +
+

+ + <#if config.getDomain() != "prod"> + ${config.getDomain()} + +

+ + <#include "/templates/nav.ftl"> +
+ + <#include "/templates/index-hero.ftl"> + + <@content> +

Welcome to GROW

+

+ GROW is an on-line web based spiritual formation + process created by Foursquare Puyallup church with you in mind, to + assist you in your journey to discover God, and to be an effective + follower of Jesus Christ. +

+ + + + + +
+
+ +<#include "/templates/footer.ftl"> + + + diff --git a/src/main/resources/templates/pages/learnmore.html.ftl b/src/main/resources/templates/pages/learnmore.html.ftl new file mode 100644 index 0000000..f69b075 --- /dev/null +++ b/src/main/resources/templates/pages/learnmore.html.ftl @@ -0,0 +1,107 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

Learn More

+ +

What is GROW?

+

+ GROW is a comprehensive spiritual formation and discipleship tool + that is specifically designed to help you grow and mature as a + follower of Jesus Christ. +

+

+ GROW is an innovative, web based process that incorporates multiple + media elements for learning including assessments, videos, written + materials, interactive tools, community forums, etc. +

+

+ GROW is designed to be experienced in a variety of ways to meet all + the possible scenarios and environments for learning: +

+
    +
  • In a community group environment
  • +
  • In a small discipleship group of a couple of people
  • +
  • Direct one-on-one discipleship
  • +
  • Friends with friends
  • +
  • Husband and wife
  • +
  • At the coffee shop
  • +
  • In your kitchen or living room
  • +
  • On vacation
  • +
+ +

Why GROW?

+

+ Disciple means "learner" and our goal is to help you be an + effective learner and grow in the knowledge of Jesus Christ, the + Word of God, and the essentials of your faith. This will cause the + transformation God promises you will experience, and that + transformation will in turn change your life! +

+

+ Every individual is in one of four stages of spiritual formation: +

+

Seeker, Believer, Disciple, Teacher

+

+ Our goal is to help you identify where you are, and to help you get to + the next stage of development and spiritual maturity. +

+

+ We believe that Jesus last command in Matthew 28:19 should be our + highest priority: +

+
+ "Therefore go and make disciples of all nations, baptizing them in + the name of the Father and of the Son and of the Holy Spirit..." +
+

+ At Foursquare Puyallup, we + are passionate about people discovering God, + entering into a personal relationship with Jesus Christ, and then + realizing their full potential in Christ as a follower of His. +

+ +

It is in our mission statement:

+ +
+ We present the Gospel message in such a way that Seekers become + Believers, Believers become Disciples, Disciples become Teachers and + Teachers lead this process. +
+ +

And it's our #5 Core value as a church:

+ +
+ We value leadership development. We help Seekers to become + Believers, Believers to become Disciples and Disciples to become + Teachers. We want to encourage and cheer the development process. +
+ +

+ We have created the GROW process for that very purpose, and in + fulfillment of God's call on the church. +

+ +

How does GROW work?

+

Here is how it works:

+
    +
  • You begin by taking a short assessment that will help you identify where you are in the stages of development
  • +
  • Upon completion of the survey, you will be invited to begin the process by watching a series of teaching videos, with accompanying printable participant outlines
  • +
  • Each stage has a forum to communicate with others who are in the process with you
  • +
  • Each stage has a list of the actions and attributes that identify someone at that particular stage of development
  • +
  • Each stage has a "digger deeper" section for more learning tools and resources to continue to develop and grow
  • +
  • Upon completion of each stage you will be invited to move to the next stage in the GROW process
  • +
+ +

Now it's your turn to act!

+

Get started now in your journey by taking the GROW assessment.

+ + + + <#include "/templates/getstarted-button.ftl"> + + diff --git a/src/main/resources/templates/pages/login.html.ftl b/src/main/resources/templates/pages/login.html.ftl new file mode 100644 index 0000000..b88ea74 --- /dev/null +++ b/src/main/resources/templates/pages/login.html.ftl @@ -0,0 +1,41 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + +
+
+
Puyallup Foursquare
+
+
+
+

Login using your Foursquare Community Groups / Online Giving login.

+

+

+
+

+

+

+
+

+

+

Don't have an account? Sign up!

+ +
+
+
Acts 2:42
+
Leadership Development
+
+
+
+
+ + diff --git a/src/main/resources/templates/pages/newaccount.html.ftl b/src/main/resources/templates/pages/newaccount.html.ftl new file mode 100644 index 0000000..edfd196 --- /dev/null +++ b/src/main/resources/templates/pages/newaccount.html.ftl @@ -0,0 +1,27 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <#if errorMessage??> + <@noticebox class="visible"> + ${errorMessage?html} + + <#else> + <@noticebox> + + + + <@content> +

+ Fill out the form below to create a new Puyallup Foursquare InFellowship account. +

+
+

+

+

+

+ + + + + diff --git a/src/main/resources/templates/pages/verification.html.ftl b/src/main/resources/templates/pages/verification.html.ftl new file mode 100644 index 0000000..e81b005 --- /dev/null +++ b/src/main/resources/templates/pages/verification.html.ftl @@ -0,0 +1,17 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content> +

+ We have sent you a verification email. + You will be taken back to the login page after you activate your account. +

+ + + + + diff --git a/src/main/resources/templates/pages/version.ftl b/src/main/resources/templates/pages/version.ftl new file mode 100644 index 0000000..d8a38a2 --- /dev/null +++ b/src/main/resources/templates/pages/version.ftl @@ -0,0 +1 @@ +Current version <#include "/templates/gitversion.ftl"> diff --git a/src/main/resources/templates/templates/assessment-results.ftl b/src/main/resources/templates/templates/assessment-results.ftl new file mode 100644 index 0000000..12c45b7 --- /dev/null +++ b/src/main/resources/templates/templates/assessment-results.ftl @@ -0,0 +1,34 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

Congratulations!

+ +

Congratulations for completing your GROW assessment!

+ +

Based on your responses you have been identified as a ${stage?cap_first}.

+ +

+ So what's next? Now you begin the process of GROWing. The button + below will take you to the ${stage?cap_first} page. +

+ +

Here you will find everything you need to begin the GROW process and start your journey.

+ +

+ We are genuinely excited for you. Each phase of the GROW process + will produce positive quantifiable and quality results in your life, as + you learn, and then apply this learning in your life. +

+ + + + + + diff --git a/src/main/resources/templates/templates/banner.ftl b/src/main/resources/templates/templates/banner.ftl new file mode 100644 index 0000000..3a2d23b --- /dev/null +++ b/src/main/resources/templates/templates/banner.ftl @@ -0,0 +1,6 @@ +<#assign bannerResult = get("bannerData", "riap://component/backend/banner")> +<#if bannerResult.succeeded == true> + <#if (bannerData.html!"") != ""> + + + diff --git a/src/main/resources/templates/templates/communityfeed.ftl b/src/main/resources/templates/templates/communityfeed.ftl new file mode 100644 index 0000000..71b33bc --- /dev/null +++ b/src/main/resources/templates/templates/communityfeed.ftl @@ -0,0 +1,41 @@ +<#escape x as x?html> +
+

Discussion Forum

+ + <#assign max_threads = 5> + <#assign threads = feeddata.getThreads(chapter, max_threads)> + <#list threads as thread> + <#assign messages = feeddata.getMessages(chapter, thread.id)> +
+
+

Q: ${thread.message.message!""}

+
By ${thread.message.author.firstName}
+ +
+ + <#list messages as msg> +
+

A: ${msg.message!""}

+
By ${thread.message.author.firstName}
+ <#if msg_has_next && msg_index == 0> + + +
+ +
+ +
+
+
+ + + +
+
+
+ diff --git a/src/main/resources/templates/templates/deeperheader.ftl b/src/main/resources/templates/templates/deeperheader.ftl new file mode 100644 index 0000000..32fa9bf --- /dev/null +++ b/src/main/resources/templates/templates/deeperheader.ftl @@ -0,0 +1,7 @@ +

Going Deeper

+

+ This section is a list of resources provided to help you to go + deeper in your faith. It includes reading material, links to + helpful resources, etc. +

+ diff --git a/src/main/resources/templates/templates/error.ftl b/src/main/resources/templates/templates/error.ftl new file mode 100644 index 0000000..4f46839 --- /dev/null +++ b/src/main/resources/templates/templates/error.ftl @@ -0,0 +1,17 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content> +

An Error has Occurred

+ +

An error has occurred. If you continue to see this message, please contact us.

+

Error: ${errorMessage}

+ + + + + diff --git a/src/main/resources/templates/templates/footer.ftl b/src/main/resources/templates/templates/footer.ftl new file mode 100644 index 0000000..e6bae13 --- /dev/null +++ b/src/main/resources/templates/templates/footer.ftl @@ -0,0 +1,16 @@ + + diff --git a/src/main/resources/templates/templates/getstarted-button.ftl b/src/main/resources/templates/templates/getstarted-button.ftl new file mode 100644 index 0000000..b0baaee --- /dev/null +++ b/src/main/resources/templates/templates/getstarted-button.ftl @@ -0,0 +1,7 @@ +
+ <#if user??> + Get Started! ➙ + <#else> + Get Started! ➙ + +
diff --git a/src/main/resources/templates/templates/gitversion.ftl b/src/main/resources/templates/templates/gitversion.ftl new file mode 100644 index 0000000..08e5207 --- /dev/null +++ b/src/main/resources/templates/templates/gitversion.ftl @@ -0,0 +1 @@ +${describe} diff --git a/src/main/resources/templates/templates/header.ftl b/src/main/resources/templates/templates/header.ftl new file mode 100644 index 0000000..0911bbc --- /dev/null +++ b/src/main/resources/templates/templates/header.ftl @@ -0,0 +1,15 @@ +
+

+ <#if user??> + Grow Process + <#else> + Grow Process + + <#if config.getDomain() != "prod"> + ${config.getDomain()} + + Foursqaure Church +

+ + <#include "/templates/nav.ftl"> +
diff --git a/src/main/resources/templates/templates/index-hero.ftl b/src/main/resources/templates/templates/index-hero.ftl new file mode 100644 index 0000000..023475a --- /dev/null +++ b/src/main/resources/templates/templates/index-hero.ftl @@ -0,0 +1,8 @@ +
+ +
+ diff --git a/src/main/resources/templates/templates/nav.ftl b/src/main/resources/templates/templates/nav.ftl new file mode 100644 index 0000000..c1f22ae --- /dev/null +++ b/src/main/resources/templates/templates/nav.ftl @@ -0,0 +1,21 @@ +<#macro navLink href> +
  • + class="current" + + href="${href}"><#nested>
  • + + + diff --git a/src/main/resources/templates/templates/newbeliever.ftl b/src/main/resources/templates/templates/newbeliever.ftl new file mode 100644 index 0000000..226b054 --- /dev/null +++ b/src/main/resources/templates/templates/newbeliever.ftl @@ -0,0 +1,41 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

    New Believers

    +

    + CONGRATULATIONS on your new relationship with Jesus Christ. + It is vital to grow in your understanding of God and of Jesus + Christ and the Holy Spirit as you begin your new journey of + faith in Jesus. +

    +

    + You may not feel a sense of urgency to begin, to get the full + understanding of your “new birth” as the Bible + calls it. But trust us when we tell you that to delay in your + growing in the knowledge of Jesus and His life for you, will + only hinder you as a follower of Jesus. +

    +

    + The SEEKER series of four videos you are about to watch + provides you with very important foundational elements of + God's love for you and his gift of salvation through Jesus. +

    +

    + Watch them in their entirety! The last video will lead in a + prayer of salvation that you have already done at church. You + are free to pray it again, but it is not necessary, you are + SAVED! +

    +

    + Now click GET STARTED and create your LOGIN and start GROWing! +

    + + + <#include "/templates/getstarted-button.ftl"> + + diff --git a/src/main/resources/templates/templates/question-circle.ftl b/src/main/resources/templates/templates/question-circle.ftl new file mode 100644 index 0000000..fbb2e61 --- /dev/null +++ b/src/main/resources/templates/templates/question-circle.ftl @@ -0,0 +1,23 @@ +

    ${question.question}

    +<#if question.description??> +

    + ${question.description} +

    + + +

    Move the white dot to answer the question.

    + +
    +
    + ${question.topLeft} + ${question.topRight} +
    +
    +
    +
    +
    + ${question.bottomLeft} + ${question.bottomRight} +
    +
    + diff --git a/src/main/resources/templates/templates/question-image.ftl b/src/main/resources/templates/templates/question-image.ftl new file mode 100644 index 0000000..280c4aa --- /dev/null +++ b/src/main/resources/templates/templates/question-image.ftl @@ -0,0 +1,17 @@ +

    ${question.question}

    +<#if question.description??> +

    + ${question.description} +

    + + +
    + <#list question.answers?keys as answerid> + <#if selectedAnswerId?? && answerid == selectedAnswerId> + + <#else> + + + +
    + diff --git a/src/main/resources/templates/templates/question-quad.ftl b/src/main/resources/templates/templates/question-quad.ftl new file mode 100644 index 0000000..f84dcff --- /dev/null +++ b/src/main/resources/templates/templates/question-quad.ftl @@ -0,0 +1,19 @@ +

    ${question.question}

    +<#if question.description??> +

    + ${question.description} +

    + + +

    Move the white dot to answer the question.

    + +
    +
    ${question.top}
    +
    +
    ${question.left}
    +
    +
    ${question.right}
    +
    +
    ${question.bottom}
    +
    + diff --git a/src/main/resources/templates/templates/question-slider.ftl b/src/main/resources/templates/templates/question-slider.ftl new file mode 100644 index 0000000..08b6bd0 --- /dev/null +++ b/src/main/resources/templates/templates/question-slider.ftl @@ -0,0 +1,18 @@ +

    ${question.question}

    +<#if question.description??> +

    + ${question.description} +

    + + +

    Slide the slider to answer the question.

    + +
    +
    +
    + <#list question.answers?keys as answerid> +
    ${question.answers[answerid].text}
    + + +
    +
    diff --git a/src/main/resources/templates/templates/question-text.ftl b/src/main/resources/templates/templates/question-text.ftl new file mode 100644 index 0000000..ac5b846 --- /dev/null +++ b/src/main/resources/templates/templates/question-text.ftl @@ -0,0 +1,17 @@ +

    ${question.question}

    +<#if question.description??> +

    + ${question.description} +

    + + +
    + <#list question.answers?keys as answerid> + <#if selectedAnswerId?? && answerid == selectedAnswerId> +
    ${question.answers[answerid].text}
    + <#else> +
    ${question.answers[answerid].text}
    + + +
    + diff --git a/src/main/resources/templates/templates/stage-complete.ftl b/src/main/resources/templates/templates/stage-complete.ftl new file mode 100644 index 0000000..72dc780 --- /dev/null +++ b/src/main/resources/templates/templates/stage-complete.ftl @@ -0,0 +1,58 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

    Congratulations!

    + +

    + Congratulations on the completion of the ${stage?cap_first} level of the GROW process. +

    + +

    + We strongly encourage you move to the next stage of GROW. However, + before you do that, let’s review a key component to ensure you + experience the transformation and benefits of what you have already + learned: Implementation. +

    + +

    + Learning is important, we should all be continuous learners, + otherwise we stagnate. +

    + +

    + However to learn new information and then not implement or employ + that learning in our life, will make us smarter but not necessarily + wiser or better. +

    + +

    + For many there is a “knowing, doing gap” in their lives. They know + a lot, but they don’t do anything with what they know, and they remain + “stuck”. +

    + +

    + We know that for your new knowledge and learning to have an impact + on your life you must implement it. +

    + +

    + You were given some tips for how to act on the new knowledge you + have. We encourage you to be proactive and intentional in closing the + gap between what you know and what you do, to experience all that God + has for you. +

    + + + + <#if nextstage??> + + + diff --git a/src/main/resources/templates/templates/stage-teacher-forward.ftl b/src/main/resources/templates/templates/stage-teacher-forward.ftl new file mode 100644 index 0000000..e8f186d --- /dev/null +++ b/src/main/resources/templates/templates/stage-teacher-forward.ftl @@ -0,0 +1,47 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + + + <@content class="text"> +

    Welcome

    + +

    + Welcome to the "Leader" portion of GROW. This portion of the GROW + process is intended for those who either: +

    + +
      +
    • Want to lead in the church, or,
    • +
    • Just want to continue to grow personally and be prepared if + called by God to lead
    • +
    + +

    + This section of GROW contains two elements: Teacher and Group Leader +

    + +
      +
    1. Teacher: If you are considering leading in the church in any + capacity; on a team in ministry, or as a Community Group leader you must + complete Teacher
    2. + +
    3. Group Leader: If you are considering leading a Community Group, + you must complete BOTH Teacher and Group Leader
    4. +
    + +

    + We trust you will enjoy these teachings, learn and grow and be better + equipped to serve God regardless of how or where you serve or lead. +

    + + + + <#if nextstage??> + + + diff --git a/src/main/resources/templates/templates/survey.ftl b/src/main/resources/templates/templates/survey.ftl new file mode 100644 index 0000000..d770461 --- /dev/null +++ b/src/main/resources/templates/templates/survey.ftl @@ -0,0 +1,53 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> + +<@commonpage> + <@noticebox> + The Grow Process focuses on the topic that you want to learn + about. Our 'Assessment' test will give you the right courses + fit for your level. + + +
    +
    +
    + +
    +
    + + + + +
    + <#switch question.type> + <#case "text"> + <#include "/templates/question-text.ftl"> + <#break> + <#case "image"> + <#include "/templates/question-image.ftl"> + <#break> + <#case "slider"> + <#include "/templates/question-slider.ftl"> + <#break> + <#case "quad"> + <#include "/templates/question-quad.ftl"> + <#break> + <#case "circle"> + <#include "/templates/question-circle.ftl"> + <#break> + +
    + +
    + + diff --git a/src/main/resources/templates/templates/training.ftl b/src/main/resources/templates/templates/training.ftl new file mode 100644 index 0000000..c5d1b32 --- /dev/null +++ b/src/main/resources/templates/templates/training.ftl @@ -0,0 +1,87 @@ +<#include "/macros/common.ftl"> +<#include "/macros/common-page.ftl"> +<#include "/macros/hms.ftl"> + +<@commonpage> + <@noticebox> + The Grow Process focuses on the topic that you want to learn + about. Our 'Assessment' test will give you the right courses + fit for your level. + + +
    +
    +
    + +
    + + +
    + "${chapter?capitalize} Chapter Progress:" +
    +
    ${chapterProgress}%
    +
    + + <#assign sidebar=showfeed || deeperinclude?has_content> + +
    style="width: 70%"> + <#assign allowed = true> + <#list videos as video> +
    style="margin-right: 30px"> +
    ${video.title}
    +

    <#if video.number != "0">${video.number}. ${video.title}

    + <@hms seconds=video.length /> + <#if (video.pdf!"") != ""> + Outline + + <#if !allowUserToSkip && allowed && !video.completed> + <#assign allowed = false> + +
    + +
    + + <#assign hasDeeper = ["seeker", "believer", "disciple", "teacher"]> + <#if hasDeeper?seq_contains(chapter)> +
    +

    Going Deeper

    +

    + Click here + for additional and important resources specifically for ${chapter}s. +

    +

    + This page contains a list of resources provided to help you to go + deeper in your faith. It includes reading material, links to + helpful resources, etc. +

    +
    + + + <#if showfeed!false> + <#include "/templates/communityfeed.ftl"> + +
    + +
    +
    Close Video
    +
    + +
    +
    + + + + diff --git a/src/main/resources/templates/utils/dump.ftl b/src/main/resources/templates/utils/dump.ftl new file mode 100644 index 0000000..6491a25 --- /dev/null +++ b/src/main/resources/templates/utils/dump.ftl @@ -0,0 +1,98 @@ +<#-- dump.ftl + -- + -- Generates tree representations of data model items. + -- + -- Usage: + -- <#import "dump.ftl" as dumper> + -- + -- <#assign foo = something.in["your"].data[0].model /> + -- + -- <@dumper.dump foo /> + -- + -- When used within html pages you've to use
    -tags to get the wanted
    +  -- result:
    +  -- 
    +  -- <@dumper.dump foo />
    +  -- 
    +  -->
    +
    +<#-- The black_list contains bad hash keys. Any hash key which matches a 
    +  -- black_list entry is prevented from being displayed.
    +  -->
    +<#assign black_list = ["class"] />
    +
    +
    +<#-- 
    +  -- The main macro.
    +  -->
    +  
    +<#macro dump data>
    +(root)
    +<#if data?is_enumerable>
    +<@printList data,[] />
    +<#elseif data?is_hash_ex>
    +<@printHashEx data,[] />
    +
    +
    +
    +<#-- private helper macros. it's not recommended to use these macros from 
    +  -- outside the macro library.
    +  -->
    +
    +<#macro printList list has_next_array>
    +<#local counter=0 />
    +<#list list as item>
    +<#list has_next_array+[true] as has_next><#if !has_next>    <#else>  | 
    +<#list has_next_array as has_next><#if !has_next>    <#else>  | <#t>
    +<#t><@printItem item?if_exists,has_next_array+[item_has_next], counter />
    +<#local counter = counter + 1/>
    +
    +
    +
    +<#macro printHashEx hash has_next_array>
    +<#list hash?keys as key>
    +<#list has_next_array+[true] as has_next><#if !has_next>    <#else>  | 
    +<#list has_next_array as has_next><#if !has_next>    <#else>  | <#t>
    +<#t><@printItem hash[key]?if_exists,has_next_array+[key_has_next], key />
    +
    +
    +
    +<#macro printItem item has_next_array key>
    +<#if item?is_method>
    +  +- ${key} = ?? (method)
    +<#elseif item?is_enumerable>
    +  +- ${key}
    +  <@printList item, has_next_array /><#t>
    +<#elseif item?is_hash_ex && omit(key?string)><#-- omit bean-wrapped java.lang.Class objects -->
    +  +- ${key} (omitted)
    +<#elseif item?is_hash_ex>
    +  +- ${key}
    +  <@printHashEx item, has_next_array /><#t>
    +<#elseif item?is_number>
    +  +- ${key} = ${item}
    +<#elseif item?is_string>
    +  +- ${key} = "${item}"
    +<#elseif item?is_boolean>
    +  +- ${key} = ${item?string}
    +<#elseif item?is_date>
    +  +- ${key} = ${item?string("yyyy-MM-dd HH:mm:ss zzzz")}
    +<#elseif item?is_transform>
    +  +- ${key} = ?? (transform)
    +<#elseif item?is_macro>
    +  +- ${key} = ?? (macro)
    +<#elseif item?is_hash>
    +  +- ${key} = ?? (hash)
    +<#elseif item?is_node>
    +  +- ${key} = ?? (node)
    +
    +
    +
    +<#function omit key>
    +    <#local what = key?lower_case>
    +    <#list black_list as item>
    +        <#if what?index_of(item) gte 0>
    +            <#return true>
    +        
    +    
    +    <#return false>
    +
    \ No newline at end of file
    diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
    new file mode 100644
    index 0000000..4b85bde
    --- /dev/null
    +++ b/src/main/webapp/WEB-INF/web.xml
    @@ -0,0 +1,44 @@
    +
    +
    +
    +
    +    grow-frontend
    +
    +    
    +    
    +        RestletServlet
    +        
    +            org.restlet.ext.servlet.ServerServlet
    +        
    +        
    +            org.restlet.component
    +            com.p4square.grow.GrowProcessComponent
    +        
    +    
    +
    +    
    +    
    +        default
    +        /style.css
    +    
    +    
    +        default
    +        /favicon.ico
    +    
    +    
    +        default
    +        /images/*
    +    
    +    
    +        default
    +        /scripts/*
    +    
    +    
    +        RestletServlet
    +        /*
    +    
    +
    diff --git a/src/main/webapp/error.html b/src/main/webapp/error.html
    new file mode 100644
    index 0000000..b01128b
    --- /dev/null
    +++ b/src/main/webapp/error.html
    @@ -0,0 +1,63 @@
    +
    +
    +
    +    An Error Has Occurred - Grow Process
    +
    +    
    +    
    +    
    +    
    +    
    +
    +
    +
    +
    +

    + Grow Process + Foursqaure Church +

    + + +
    + +
    +
    +

    +

    +
    +
    + +
    +
    +

    An Unexpected Error has Occurred

    + +

    An error has occurred. If you continue to see this message, please contact us.

    +
    +
    + +
    +
    + + + + + + diff --git a/src/main/webapp/favicon.ico b/src/main/webapp/favicon.ico new file mode 100644 index 0000000..200f311 Binary files /dev/null and b/src/main/webapp/favicon.ico differ diff --git a/src/main/webapp/images/02-a1-hover.jpg b/src/main/webapp/images/02-a1-hover.jpg new file mode 100644 index 0000000..8101265 Binary files /dev/null and b/src/main/webapp/images/02-a1-hover.jpg differ diff --git a/src/main/webapp/images/02-a1.jpg b/src/main/webapp/images/02-a1.jpg new file mode 100644 index 0000000..d87ea14 Binary files /dev/null and b/src/main/webapp/images/02-a1.jpg differ diff --git a/src/main/webapp/images/02-a2-hover.jpg b/src/main/webapp/images/02-a2-hover.jpg new file mode 100644 index 0000000..40c1bd7 Binary files /dev/null and b/src/main/webapp/images/02-a2-hover.jpg differ diff --git a/src/main/webapp/images/02-a2.jpg b/src/main/webapp/images/02-a2.jpg new file mode 100644 index 0000000..e03669c Binary files /dev/null and b/src/main/webapp/images/02-a2.jpg differ diff --git a/src/main/webapp/images/02-a3-hover.jpg b/src/main/webapp/images/02-a3-hover.jpg new file mode 100644 index 0000000..fc2bb44 Binary files /dev/null and b/src/main/webapp/images/02-a3-hover.jpg differ diff --git a/src/main/webapp/images/02-a3.jpg b/src/main/webapp/images/02-a3.jpg new file mode 100644 index 0000000..1991fb1 Binary files /dev/null and b/src/main/webapp/images/02-a3.jpg differ diff --git a/src/main/webapp/images/02-a4-hover.jpg b/src/main/webapp/images/02-a4-hover.jpg new file mode 100644 index 0000000..9f08263 Binary files /dev/null and b/src/main/webapp/images/02-a4-hover.jpg differ diff --git a/src/main/webapp/images/02-a4.jpg b/src/main/webapp/images/02-a4.jpg new file mode 100644 index 0000000..742f60e Binary files /dev/null and b/src/main/webapp/images/02-a4.jpg differ diff --git a/src/main/webapp/images/02-a5-hover.jpg b/src/main/webapp/images/02-a5-hover.jpg new file mode 100644 index 0000000..8cd1f9d Binary files /dev/null and b/src/main/webapp/images/02-a5-hover.jpg differ diff --git a/src/main/webapp/images/02-a5.jpg b/src/main/webapp/images/02-a5.jpg new file mode 100644 index 0000000..465c199 Binary files /dev/null and b/src/main/webapp/images/02-a5.jpg differ diff --git a/src/main/webapp/images/02-a6-hover.jpg b/src/main/webapp/images/02-a6-hover.jpg new file mode 100644 index 0000000..1fe87bd Binary files /dev/null and b/src/main/webapp/images/02-a6-hover.jpg differ diff --git a/src/main/webapp/images/02-a6.jpg b/src/main/webapp/images/02-a6.jpg new file mode 100644 index 0000000..df26868 Binary files /dev/null and b/src/main/webapp/images/02-a6.jpg differ diff --git a/src/main/webapp/images/08-a1-hover.jpg b/src/main/webapp/images/08-a1-hover.jpg new file mode 100644 index 0000000..cfa7077 Binary files /dev/null and b/src/main/webapp/images/08-a1-hover.jpg differ diff --git a/src/main/webapp/images/08-a1.jpg b/src/main/webapp/images/08-a1.jpg new file mode 100644 index 0000000..e249a3c Binary files /dev/null and b/src/main/webapp/images/08-a1.jpg differ diff --git a/src/main/webapp/images/08-a2-hover.jpg b/src/main/webapp/images/08-a2-hover.jpg new file mode 100644 index 0000000..77af9cc Binary files /dev/null and b/src/main/webapp/images/08-a2-hover.jpg differ diff --git a/src/main/webapp/images/08-a2.jpg b/src/main/webapp/images/08-a2.jpg new file mode 100644 index 0000000..287353d Binary files /dev/null and b/src/main/webapp/images/08-a2.jpg differ diff --git a/src/main/webapp/images/08-a3-hover.jpg b/src/main/webapp/images/08-a3-hover.jpg new file mode 100644 index 0000000..6d3b50c Binary files /dev/null and b/src/main/webapp/images/08-a3-hover.jpg differ diff --git a/src/main/webapp/images/08-a3.jpg b/src/main/webapp/images/08-a3.jpg new file mode 100644 index 0000000..eab7d7a Binary files /dev/null and b/src/main/webapp/images/08-a3.jpg differ diff --git a/src/main/webapp/images/about-grow.png b/src/main/webapp/images/about-grow.png new file mode 100644 index 0000000..c95a018 Binary files /dev/null and b/src/main/webapp/images/about-grow.png differ diff --git a/src/main/webapp/images/acts242.png b/src/main/webapp/images/acts242.png new file mode 100644 index 0000000..358f3f7 Binary files /dev/null and b/src/main/webapp/images/acts242.png differ diff --git a/src/main/webapp/images/close.png b/src/main/webapp/images/close.png new file mode 100644 index 0000000..8854d89 Binary files /dev/null and b/src/main/webapp/images/close.png differ diff --git a/src/main/webapp/images/complete.png b/src/main/webapp/images/complete.png new file mode 100644 index 0000000..780be45 Binary files /dev/null and b/src/main/webapp/images/complete.png differ diff --git a/src/main/webapp/images/faux_right_column.png b/src/main/webapp/images/faux_right_column.png new file mode 100644 index 0000000..d87e901 Binary files /dev/null and b/src/main/webapp/images/faux_right_column.png differ diff --git a/src/main/webapp/images/foursquarechurchlogin.png b/src/main/webapp/images/foursquarechurchlogin.png new file mode 100644 index 0000000..0d5f861 Binary files /dev/null and b/src/main/webapp/images/foursquarechurchlogin.png differ diff --git a/src/main/webapp/images/foursquarelg.png b/src/main/webapp/images/foursquarelg.png new file mode 100644 index 0000000..bb01365 Binary files /dev/null and b/src/main/webapp/images/foursquarelg.png differ diff --git a/src/main/webapp/images/foursquaresm.png b/src/main/webapp/images/foursquaresm.png new file mode 100644 index 0000000..263e0d9 Binary files /dev/null and b/src/main/webapp/images/foursquaresm.png differ diff --git a/src/main/webapp/images/grow-poster.png b/src/main/webapp/images/grow-poster.png new file mode 100644 index 0000000..35e6e45 Binary files /dev/null and b/src/main/webapp/images/grow-poster.png differ diff --git a/src/main/webapp/images/hero.png b/src/main/webapp/images/hero.png new file mode 100644 index 0000000..6077e96 Binary files /dev/null and b/src/main/webapp/images/hero.png differ diff --git a/src/main/webapp/images/leadershipdev.png b/src/main/webapp/images/leadershipdev.png new file mode 100644 index 0000000..d08a29a Binary files /dev/null and b/src/main/webapp/images/leadershipdev.png differ diff --git a/src/main/webapp/images/loginbg.png b/src/main/webapp/images/loginbg.png new file mode 100644 index 0000000..79a791f Binary files /dev/null and b/src/main/webapp/images/loginbg.png differ diff --git a/src/main/webapp/images/logo.png b/src/main/webapp/images/logo.png new file mode 100644 index 0000000..b18dd7d Binary files /dev/null and b/src/main/webapp/images/logo.png differ diff --git a/src/main/webapp/images/next.png b/src/main/webapp/images/next.png new file mode 100644 index 0000000..4449a8d Binary files /dev/null and b/src/main/webapp/images/next.png differ diff --git a/src/main/webapp/images/noticeicon.png b/src/main/webapp/images/noticeicon.png new file mode 100644 index 0000000..578fb5e Binary files /dev/null and b/src/main/webapp/images/noticeicon.png differ diff --git a/src/main/webapp/images/play.png b/src/main/webapp/images/play.png new file mode 100644 index 0000000..bd17dfc Binary files /dev/null and b/src/main/webapp/images/play.png differ diff --git a/src/main/webapp/images/previous.png b/src/main/webapp/images/previous.png new file mode 100644 index 0000000..7e5eef6 Binary files /dev/null and b/src/main/webapp/images/previous.png differ diff --git a/src/main/webapp/images/quad.png b/src/main/webapp/images/quad.png new file mode 100644 index 0000000..9cd3e74 Binary files /dev/null and b/src/main/webapp/images/quad.png differ diff --git a/src/main/webapp/images/quadselector.png b/src/main/webapp/images/quadselector.png new file mode 100644 index 0000000..a730adb Binary files /dev/null and b/src/main/webapp/images/quadselector.png differ diff --git a/src/main/webapp/images/reply.png b/src/main/webapp/images/reply.png new file mode 100644 index 0000000..ffcebe8 Binary files /dev/null and b/src/main/webapp/images/reply.png differ diff --git a/src/main/webapp/images/slider.png b/src/main/webapp/images/slider.png new file mode 100644 index 0000000..acc98dc Binary files /dev/null and b/src/main/webapp/images/slider.png differ diff --git a/src/main/webapp/images/videoimage.jpg b/src/main/webapp/images/videoimage.jpg new file mode 100644 index 0000000..23e8053 Binary files /dev/null and b/src/main/webapp/images/videoimage.jpg differ diff --git a/src/main/webapp/notfound.html b/src/main/webapp/notfound.html new file mode 100644 index 0000000..29e6493 --- /dev/null +++ b/src/main/webapp/notfound.html @@ -0,0 +1,63 @@ + + + + File Not Found - Grow Process + + + + + + + + +
    +
    +

    + Grow Process + Foursqaure Church +

    + + +
    + +
    +
    +

    +

    +
    +
    + +
    +
    +

    File Not Found

    + +

    The requested URL was not found. If you believe this to be in error, please contact us.

    +
    +
    + +
    +
    + + + + + + diff --git a/src/main/webapp/scripts/growth.js b/src/main/webapp/scripts/growth.js new file mode 100644 index 0000000..9ae6786 --- /dev/null +++ b/src/main/webapp/scripts/growth.js @@ -0,0 +1,315 @@ +$(document).ready(function() +{ + $('.slider').draggable({ + axis:"x", + containment:"parent", + cursor:"pointer", + stop: function (event, ui) { + var range = $(ui.helper).parent().width() - 46; + var value = (ui.position.left + range / 2) / range; + $("#answerField").val(value); + } + }); + $('.sliderbar').mousedown(function(e) { + var left = $(this).offset().left; + var width = $(this).width(); + proposed = Math.max(left, Math.min(e.pageX - 23, left + width - 46)); + $(this).children('.slider').offset({left: proposed}); + + var range = width - 46; + var value = (proposed - left) / range; + $("#answerField").val(value); + }); + + $('.quad .selector').draggable({ + containment:"parent", + cursor:"pointer", + stop: function (event, ui) { + updateQuadAnswer(ui.position.left, ui.position.top); + }, + drag: function (event, ui) { + var x = ui.position.left - 125; + var y = -(ui.position.top - 125); + var signY = y<0?-1:1; + var signX = x<0?-1:1; + ui.position.left = signX * Math.min(signX*x, 125) + 125; + x = ui.position.left - 125; + ui.position.top = -(signY * Math.min(Math.abs(y), Math.abs(Math.sqrt(125*125 - x*x))) - 125); + } + }); + $('.quad').mousedown(function(e) { + var x = e.offsetX - 135; + var y = -(e.offsetY - 135); + if (Math.sqrt(x*x+y*y) < 135) { + $(this).children('.selector').offset({left: e.pageX - 10, top: e.pageY - 10}); + updateQuadAnswer(e.offsetX - 10, e.offsetY - 10); + } + }); + + var previousAnswer = $("#answerField").val(); + if (previousAnswer != undefined) { + if (!isNaN(previousAnswer)) { + var range = $('.sliderQuestion .sliderbar').width() - 46; + var left = previousAnswer * range - range / 2; + $('.sliderQuestion .slider').css('left', left); + } else { + var index = previousAnswer.indexOf(',') + if (index != -1) { + var x = previousAnswer.substr(0, index); + var y = previousAnswer.substr(index + 1); + if (!isNaN(x) && !isNaN(y)) { + var posX = x * 125 + 125; + var posY = -y * 125 + 125; + $('.quadQuestion .selector').css({left:posX,top:posY}); + } + + } + } + } + + $('.imageQuestion img.answer').hover(function (e) { + // Enter + if (!$(e.target).hasClass('selected')) { + var url = e.target.src; + e.target.src = url.replace('.jpg', '-hover.jpg'); + } + }, + function (e) { + // Exit + if (!$(e.target).hasClass('selected')) { + var url = e.target.src; + e.target.src = url.replace('-hover.jpg', '.jpg'); + } + }); + + var video = document.getElementById("herovideo"); + if (video != null) { + video.removeAttribute("controls"); + } + + $('textarea').bind({ + focus: function () { + var self = $(this); + + if (self.val() == self.attr('defaultValue')) { + self.val('').removeClass('default'); + }; + }, + blur: function () { + var self = $(this), + val = jQuery.trim(self.val()); + + if (val == "" || val == self.attr('defaultValue')) { + self.val(self.attr('defaultValue')).addClass('default'); + }; + } + }).trigger('blur'); + + $("#thefeed article .answer:nth-child(3)").delay(300).slideDown(); + + $("#banner").slideDown(); +}); + +function notice(msg) +{ + $('#noticebox p span').html(msg) + $('#noticebox').slideDown(); +} + +function updateQuadAnswer(offsetX, offsetY) +{ + var x = (offsetX - 125) / 125; + var y = -(offsetY - 125) / 125; + var value = x + "," + y; + $("#answerField").val(value); +} + +function selectAnswer(element) +{ + // Cleanup image selections + $(element).parent('.imageQuestion').children('.selected').each(function(i, e) { + var url = e.src; + e.src = url.replace('-hover.jpg', '.jpg'); + }); + + $(element).siblings('.selected').removeClass('selected'); + $(element).addClass('selected'); + $("#answerField").val(element.id); +} + +function previousQuestion() +{ + $("#direction").val("previous"); + sendAnswer(false); +} + +function nextQuestion() +{ + $("#direction").val("next"); + sendAnswer(true); +} + +function sendAnswer(required) +{ + var selectedAnswer = $("#answerField").val(); + + if (required && selectedAnswer == '') { + notice('Please select an answer before moving to the next question'); + return; + } + + $("#questionForm").submit(); +} + +function playVideo(videoId) +{ + if (!$('#' + videoId).hasClass('allowed')) { + notice("You must watch the videos in order."); + return; + } + + $.ajax({ + type: "GET", + url: location.href + "/videos/" + videoId + ".json", + dataType: "json" + }).done(function(data) { + if (data == null) { + notice("Unable to load the video at this time. Please check your internet connection and try again later. If the problem persists, please contact us."); + return; + } + + var player = $('#videoplayer video')[0]; + if (typeof player.canPlayType !== 'function') { + notice("Your browser does not support html5 videos. Please try another browser or contact us."); + } + + for (var i in data.urls) { + var video = data.urls[i]; + if (player.canPlayType(video.type) != '') { + player.src = video.src; + player.load(); + player.play(); + player.addEventListener('ended', function(){ reportVideoComplete(data); }); + displayPlayer(); + return; + } + } + + notice("We could not find a video format that will work with your browser. Please try another browser or contact us."); + }).error(function(jqXHR, error) { + notice('Could not load video due to ' + error + '. If the problem persists, please contact us.'); + }); +} + +function displayPlayer() +{ + // Display Player + $("#content").fadeOut(100); + $("body").animate({backgroundColor: '#181818'}, 500, 'linear', function(){ + $("#videoplayer").fadeIn(200); + }); + + $(document).keyup('displayPlayer.exit', function(e) { + if (e.keyCode == 27) { + closeVideo(); + } + }); +} + +function reportVideoComplete(data) +{ + notice("You finished \u201C" + data.title + ".\u201D"); + var completedBefore = $('#videos article .completed').length; + + $('#' + data.id).addClass('completed'); + $('#' + data.id).parent().next().children('div.image').addClass('allowed'); + + var completed = $('#videos article .completed').length; + var total = $('#videos article').length; + var percent = Math.floor(completed * 100 / total) + "%"; + $('#chapterprogress .progress').width(percent); + $('#chapterprogress .progresslabel').css('left', percent); + $('#chapterprogress .progresslabel').html(percent); + + closeVideo(); + + $.ajax({ + type: "POST", + url: location.href + "/videos/" + data.id + ".json", + dataType: "json", + data: {'completed':'true'} + }).error(function(jqXHR, error) { + notice('Could not record video completiton due to ' + error + '. If the problem persists, please contact us.'); + }).always(function() { + if (completed == total && completedBefore != completed) { + chapterComplete(); + } + }); +} + +function closeVideo() +{ + var player = $('#videoplayer video')[0]; + if (typeof player.pause === 'function') { + player.pause(); + } + + $("#videoplayer").fadeOut(100); + $("body").animate({backgroundColor: '#FFFFFF'}, 500, 'linear', function(){ + $("#content").fadeIn(200); + }); + + $(document).unbind('displayPlayer.exit'); +} + +function chapterComplete() +{ + notice("You've completed this chapter!"); + location.href += "/completed"; +} + +function submitClassForm() +{ + notice("Submitting Class Form..."); + + var firstname = $("#firstname").val(); + var lastname = $("#lastname").val(); + var phone = $("#phone").val(); + var email = $("#email").val(); + + if (firstname == "") { + alert("First name is required."); + return false; + } else if (lastname == "") { + alert("Last name is required."); + return false; + } else if (phone == "") { + alert("Phone is required."); + return false; + } else if (email == "") { + alert("Email is required."); + return false; + } + + $.ajax({ + type: "POST", + url: "http://www.myfoursquarechurch.com/grow-classes/#response", + data: $("#classform").serialize(), + }).always(function() { + location.href="/account/training/introduction"; + }); + + return false; +} + +function answerQuestion(id) +{ + $("#answer-" + id).slideToggle(); +} + +function showAnswers(obj) +{ + $(obj).hide(); + $(obj).parents(".answer").siblings(".slider").slideDown(); +} + diff --git a/src/main/webapp/scripts/jquery-ui.js b/src/main/webapp/scripts/jquery-ui.js new file mode 100644 index 0000000..df25d36 --- /dev/null +++ b/src/main/webapp/scripts/jquery-ui.js @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.3 - 2013-06-10 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.sortable.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.menu.js, jquery.ui.progressbar.js, jquery.ui.slider.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js, jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function(e,t){function i(t,i){var a,n,r,o=t.nodeName.toLowerCase();return"area"===o?(a=t.parentNode,n=a.name,t.href&&n&&"map"===a.nodeName.toLowerCase()?(r=e("img[usemap=#"+n+"]")[0],!!r&&s(r)):!1):(/input|select|textarea|button|object/.test(o)?!t.disabled:"a"===o?t.href||i:i)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var a=0,n=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.3",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var s,a,n=e(this[0]);n.length&&n[0]!==document;){if(s=n.css("position"),("absolute"===s||"relative"===s||"fixed"===s)&&(a=parseInt(n.css("zIndex"),10),!isNaN(a)&&0!==a))return a;n=n.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++a)})},removeUniqueId:function(){return this.each(function(){n.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var s=e.attr(t,"tabindex"),a=isNaN(s);return(a||s>=0)&&i(t,!a)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,s){function a(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===s?["Left","Right"]:["Top","Bottom"],r=s.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+s]=function(i){return i===t?o["inner"+s].call(this):this.each(function(){e(this).css(r,a(this,i)+"px")})},e.fn["outer"+s]=function(t,i){return"number"!=typeof t?o["outer"+s].call(this,t):this.each(function(){e(this).css(r,a(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,s){var a,n=e.ui[t].prototype;for(a in s)n.plugins[a]=n.plugins[a]||[],n.plugins[a].push([i,s[a]])},call:function(e,t,i){var s,a=e.plugins[t];if(a&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(s=0;a.length>s;s++)e.options[a[s][0]]&&a[s][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",a=!1;return t[s]>0?!0:(t[s]=1,a=t[s]>0,t[s]=0,a)}})})(jQuery);(function(e,t){var i=0,s=Array.prototype.slice,n=e.cleanData;e.cleanData=function(t){for(var i,s=0;null!=(i=t[s]);s++)try{e(i).triggerHandler("remove")}catch(a){}n(t)},e.widget=function(i,s,n){var a,r,o,h,l={},u=i.split(".")[0];i=i.split(".")[1],a=u+"-"+i,n||(n=s,s=e.Widget),e.expr[":"][a.toLowerCase()]=function(t){return!!e.data(t,a)},e[u]=e[u]||{},r=e[u][i],o=e[u][i]=function(e,i){return this._createWidget?(arguments.length&&this._createWidget(e,i),t):new o(e,i)},e.extend(o,r,{version:n.version,_proto:e.extend({},n),_childConstructors:[]}),h=new s,h.options=e.widget.extend({},h.options),e.each(n,function(i,n){return e.isFunction(n)?(l[i]=function(){var e=function(){return s.prototype[i].apply(this,arguments)},t=function(e){return s.prototype[i].apply(this,e)};return function(){var i,s=this._super,a=this._superApply;return this._super=e,this._superApply=t,i=n.apply(this,arguments),this._super=s,this._superApply=a,i}}(),t):(l[i]=n,t)}),o.prototype=e.widget.extend(h,{widgetEventPrefix:r?h.widgetEventPrefix:i},l,{constructor:o,namespace:u,widgetName:i,widgetFullName:a}),r?(e.each(r._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete r._childConstructors):s._childConstructors.push(o),e.widget.bridge(i,o)},e.widget.extend=function(i){for(var n,a,r=s.call(arguments,1),o=0,h=r.length;h>o;o++)for(n in r[o])a=r[o][n],r[o].hasOwnProperty(n)&&a!==t&&(i[n]=e.isPlainObject(a)?e.isPlainObject(i[n])?e.widget.extend({},i[n],a):e.widget.extend({},a):a);return i},e.widget.bridge=function(i,n){var a=n.prototype.widgetFullName||i;e.fn[i]=function(r){var o="string"==typeof r,h=s.call(arguments,1),l=this;return r=!o&&h.length?e.widget.extend.apply(null,[r].concat(h)):r,o?this.each(function(){var s,n=e.data(this,a);return n?e.isFunction(n[r])&&"_"!==r.charAt(0)?(s=n[r].apply(n,h),s!==n&&s!==t?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):t):e.error("no such method '"+r+"' for "+i+" widget instance"):e.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+r+"'")}):this.each(function(){var t=e.data(this,a);t?t.option(r||{})._init():e.data(this,a,new n(r,this))}),l}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
    ",options:{disabled:!1,create:null},_createWidget:function(t,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(i,s){var n,a,r,o=i;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof i)if(o={},n=i.split("."),i=n.shift(),n.length){for(a=o[i]=e.widget.extend({},this.options[i]),r=0;n.length-1>r;r++)a[n[r]]=a[n[r]]||{},a=a[n[r]];if(i=n.pop(),s===t)return a[i]===t?null:a[i];a[i]=s}else{if(s===t)return this.options[i]===t?null:this.options[i];o[i]=s}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var a,r=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=a=e(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,a=this.widget()),e.each(n,function(n,o){function h(){return i||r.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?r[o]:o).apply(r,arguments):t}"string"!=typeof o&&(h.guid=o.guid=o.guid||h.guid||e.guid++);var l=n.match(/^(\w+)\s*(.*)$/),u=l[1]+r.eventNamespace,c=l[2];c?a.delegate(c,u,h):s.bind(u,h)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,r=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(r)&&r.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var r,o=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),r=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),r&&e.effects&&e.effects.effect[o]?s[t](n):o!==t&&s[o]?s[o](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}})})(jQuery);(function(e){var t=!1;e(document).mouseup(function(){t=!1}),e.widget("ui.mouse",{version:"1.10.3",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(i){return!0===e.data(i.target,t.widgetName+".preventClickEvent")?(e.removeData(i.target,t.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!t){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?e(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===e.data(i.target,this.widgetName+".preventClickEvent")&&e.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return s._mouseMove(e)},this._mouseUpDelegate=function(e){return s._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),t=!0,!0)):!0}},_mouseMove:function(t){return e.ui.ie&&(!document.documentMode||9>document.documentMode)&&!t.button?this._mouseUp(t):this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,h=Math.round,l=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("
    "),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow?"":e.element.css("overflow-x"),s=e.isWindow?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widths?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(h.horizontal="center"),d>m&&m>r(n+a)&&(h.vertical="middle"),h.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,h)}),c.offset(t.extend(C,{using:l}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-o-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-o-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,m=-2*e.offset[1];0>c?(s=t.top+p+f+m+e.collisionHeight-o-a,t.top+p+f+m>c&&(0>s||r(c)>s)&&(t.top+=p+f+m)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+m-h,t.top+p+f+m>u&&(i>0||u>r(i))&&(t.top+=p+f+m))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){e.widget("ui.draggable",e.ui.mouse,{version:"1.10.3",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(t){var i=this.options;return this.helper||i.disabled||e(t.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(t),this.handle?(e(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){e("
    ").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(e(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(t){var i=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!i.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_mouseDrag:function(t,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(t),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",t,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var i=this,s=!1;return e.ui.ddmanager&&!this.options.dropBehaviour&&(s=e.ui.ddmanager.drop(this,t)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||e.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",t)!==!1&&i._clear()}):this._trigger("stop",t)!==!1&&this._clear(),!1):!1},_mouseUp:function(t){return e("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){return this.options.handle?!!e(t.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(t){var i=this.options,s=e.isFunction(i.helper)?e(i.helper.apply(this.element[0],[t])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(t){"string"==typeof t&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){var t=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&e.ui.ie)&&(t={top:0,left:0}),{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var e=this.element.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,e(window).scrollLeft()+e(window).width()-this.helperProportions.width-this.margins.left,e(window).scrollTop()+(e(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,e(document).width()-this.helperProportions.width-this.margins.left,(e(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=e(n.containment),s=i[0],s&&(t="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(t?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(t?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(t,i){i||(i=this.position);var s="absolute"===t?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(t){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=t.pageX,l=t.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.lefti[2]&&(h=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(l=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((l-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,l=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((h-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,h=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:l-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:h-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(t,i,s){return s=s||this._uiHash(),e.ui.plugin.call(this,t,[i,s]),"drag"===t&&(this.positionAbs=this._convertPositionTo("absolute")),e.Widget.prototype._trigger.call(this,t,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,i){var s=e(this).data("ui-draggable"),n=s.options,a=e.extend({},i,{item:s.element});s.sortables=[],e(n.connectToSortable).each(function(){var i=e.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",t,a))})},stop:function(t,i){var s=e(this).data("ui-draggable"),n=e.extend({},i,{item:s.element});e.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(t),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",t,n))})},drag:function(t,i){var s=e(this).data("ui-draggable"),n=this;e.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,e.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&e.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=e(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},t.target=this.instance.currentItem[0],this.instance._mouseCapture(t,!0),this.instance._mouseStart(t,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",t),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(t)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",t,this.instance._uiHash(this.instance)),this.instance._mouseStop(t,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",t),s.dropped=!1)})}}),e.ui.plugin.add("draggable","cursor",{start:function(){var t=e("body"),i=e(this).data("ui-draggable").options;t.css("cursor")&&(i._cursor=t.css("cursor")),t.css("cursor",i.cursor)},stop:function(){var t=e(this).data("ui-draggable").options;t._cursor&&e("body").css("cursor",t._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,i){var s=e(i.helper),n=e(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(t,i){var s=e(this).data("ui-draggable").options;s._opacity&&e(i.helper).css("opacity",s._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(){var t=e(this).data("ui-draggable");t.scrollParent[0]!==document&&"HTML"!==t.scrollParent[0].tagName&&(t.overflowOffset=t.scrollParent.offset())},drag:function(t){var i=e(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-t.pageY=0;c--)r=p.snapElements[c].left,h=r+p.snapElements[c].width,l=p.snapElements[c].top,u=l+p.snapElements[c].height,r-m>v||g>h+m||l-m>y||b>u+m||!e.contains(p.snapElements[c].item.ownerDocument,p.snapElements[c].item)?(p.snapElements[c].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=!1):("inner"!==f.snapMode&&(s=m>=Math.abs(l-y),n=m>=Math.abs(u-b),a=m>=Math.abs(r-v),o=m>=Math.abs(h-g),s&&(i.position.top=p._convertPositionTo("relative",{top:l-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:u,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:h}).left-p.margins.left)),d=s||n||a||o,"outer"!==f.snapMode&&(s=m>=Math.abs(l-b),n=m>=Math.abs(u-y),a=m>=Math.abs(r-g),o=m>=Math.abs(h-v),s&&(i.position.top=p._convertPositionTo("relative",{top:l,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:u-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:h-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[c].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=s||n||a||o||d)}}),e.ui.plugin.add("draggable","stack",{start:function(){var t,i=this.data("ui-draggable").options,s=e.makeArray(e(i.stack)).sort(function(t,i){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(i).css("zIndex"),10)||0)});s.length&&(t=parseInt(e(s[0]).css("zIndex"),10)||0,e(s).each(function(i){e(this).css("zIndex",t+i)}),this.css("zIndex",t+s.length))}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,i){var s=e(i.helper),n=e(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(t,i){var s=e(this).data("ui-draggable").options;s._zIndex&&e(i.helper).css("zIndex",s._zIndex)}})})(jQuery);(function(e){function t(e,t,i){return e>t&&t+i>e}e.widget("ui.droppable",{version:"1.10.3",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var t=this.options,i=t.accept;this.isover=!1,this.isout=!0,this.accept=e.isFunction(i)?i:function(e){return e.is(i)},this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight},e.ui.ddmanager.droppables[t.scope]=e.ui.ddmanager.droppables[t.scope]||[],e.ui.ddmanager.droppables[t.scope].push(this),t.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){for(var t=0,i=e.ui.ddmanager.droppables[this.options.scope];i.length>t;t++)i[t]===this&&i.splice(t,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(t,i){"accept"===t&&(this.accept=e.isFunction(i)?i:function(e){return e.is(i)}),e.Widget.prototype._setOption.apply(this,arguments)},_activate:function(t){var i=e.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",t,this.ui(i))},_deactivate:function(t){var i=e.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",t,this.ui(i))},_over:function(t){var i=e.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",t,this.ui(i)))},_out:function(t){var i=e.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",t,this.ui(i)))},_drop:function(t,i){var s=i||e.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var t=e.data(this,"ui-droppable");return t.options.greedy&&!t.options.disabled&&t.options.scope===s.options.scope&&t.accept.call(t.element[0],s.currentItem||s.element)&&e.ui.intersect(s,e.extend(t,{offset:t.element.offset()}),t.options.tolerance)?(n=!0,!1):undefined}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",t,this.ui(s)),this.element):!1):!1},ui:function(e){return{draggable:e.currentItem||e.element,helper:e.helper,position:e.position,offset:e.positionAbs}}}),e.ui.intersect=function(e,i,s){if(!i.offset)return!1;var n,a,o=(e.positionAbs||e.position.absolute).left,r=o+e.helperProportions.width,h=(e.positionAbs||e.position.absolute).top,l=h+e.helperProportions.height,u=i.offset.left,c=u+i.proportions.width,d=i.offset.top,p=d+i.proportions.height;switch(s){case"fit":return o>=u&&c>=r&&h>=d&&p>=l;case"intersect":return o+e.helperProportions.width/2>u&&c>r-e.helperProportions.width/2&&h+e.helperProportions.height/2>d&&p>l-e.helperProportions.height/2;case"pointer":return n=(e.positionAbs||e.position.absolute).left+(e.clickOffset||e.offset.click).left,a=(e.positionAbs||e.position.absolute).top+(e.clickOffset||e.offset.click).top,t(a,d,i.proportions.height)&&t(n,u,i.proportions.width);case"touch":return(h>=d&&p>=h||l>=d&&p>=l||d>h&&l>p)&&(o>=u&&c>=o||r>=u&&c>=r||u>o&&r>c);default:return!1}},e.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(t,i){var s,n,a=e.ui.ddmanager.droppables[t.options.scope]||[],o=i?i.type:null,r=(t.currentItem||t.element).find(":data(ui-droppable)").addBack();e:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||t&&!a[s].accept.call(a[s].element[0],t.currentItem||t.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions.height=0;continue e}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions={width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight})}},drop:function(t,i){var s=!1;return e.each((e.ui.ddmanager.droppables[t.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&e.ui.intersect(t,this,this.options.tolerance)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],t.currentItem||t.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(t,i){t.element.parentsUntil("body").bind("scroll.droppable",function(){t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,i)})},drag:function(t,i){t.options.refreshPositions&&e.ui.ddmanager.prepareOffsets(t,i),e.each(e.ui.ddmanager.droppables[t.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=e.ui.intersect(t,this,this.options.tolerance),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return e.data(this,"ui-droppable").options.scope===n}),a.length&&(s=e.data(a[0],"ui-droppable"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(t,i){t.element.parentsUntil("body").unbind("scroll.droppable"),t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,i)}}})(jQuery);(function(e){function t(e){return parseInt(e,10)||0}function i(e){return!isNaN(parseInt(e,10))}e.widget("ui.resizable",e.ui.mouse,{version:"1.10.3",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_create:function(){var t,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),e.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(e("
    ").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.data("ui-resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(e(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),t=this.handles.split(","),this.handles={},i=0;t.length>i;i++)s=e.trim(t[i]),a="ui-resizable-"+s,n=e("
    "),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(t){var i,s,n,a;t=t||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=e(this.handles[i],this.element).show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=e(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),t.css(n,a),this._proportionallyResize()),e(this.handles[i]).length},this._renderAxis(this.element),this._handles=e(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),e(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(e(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(e(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var t,i=function(t){e(t).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),t=this.element,this.originalElement.css({position:t.css("position"),width:t.outerWidth(),height:t.outerHeight(),top:t.css("top"),left:t.css("left")}).insertAfter(t),t.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(t){var i,s,n=!1;for(i in this.handles)s=e(this.handles[i])[0],(s===t.target||e.contains(s,t.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(i){var s,n,a,o=this.options,r=this.element.position(),h=this.element;return this.resizing=!0,/absolute/.test(h.css("position"))?h.css({position:"absolute",top:h.css("top"),left:h.css("left")}):h.is(".ui-draggable")&&h.css({position:"absolute",top:r.top,left:r.left}),this._renderProxy(),s=t(this.helper.css("left")),n=t(this.helper.css("top")),o.containment&&(s+=e(o.containment).scrollLeft()||0,n+=e(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:s,top:n},this.size=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalSize=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalPosition={left:s,top:n},this.sizeDiff={width:h.outerWidth()-h.width(),height:h.outerHeight()-h.height()},this.originalMousePosition={left:i.pageX,top:i.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,a=e(".ui-resizable-"+this.axis).css("cursor"),e("body").css("cursor","auto"===a?this.axis+"-resize":a),h.addClass("ui-resizable-resizing"),this._propagate("start",i),!0},_mouseDrag:function(t){var i,s=this.helper,n={},a=this.originalMousePosition,o=this.axis,r=this.position.top,h=this.position.left,l=this.size.width,u=this.size.height,c=t.pageX-a.left||0,d=t.pageY-a.top||0,p=this._change[o];return p?(i=p.apply(this,[t,c,d]),this._updateVirtualBoundaries(t.shiftKey),(this._aspectRatio||t.shiftKey)&&(i=this._updateRatio(i,t)),i=this._respectSize(i,t),this._updateCache(i),this._propagate("resize",t),this.position.top!==r&&(n.top=this.position.top+"px"),this.position.left!==h&&(n.left=this.position.left+"px"),this.size.width!==l&&(n.width=this.size.width+"px"),this.size.height!==u&&(n.height=this.size.height+"px"),s.css(n),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),e.isEmptyObject(n)||this._trigger("resize",t,this.ui()),!1):!1},_mouseStop:function(t){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,u=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&e.ui.hasScroll(i[0],"left")?0:u.sizeDiff.height,a=s?0:u.sizeDiff.width,o={width:u.helper.width()-a,height:u.helper.height()-n},r=parseInt(u.element.css("left"),10)+(u.position.left-u.originalPosition.left)||null,h=parseInt(u.element.css("top"),10)+(u.position.top-u.originalPosition.top)||null,l.animate||this.element.css(e.extend(o,{top:h,left:r})),u.helper.height(u.size.height),u.helper.width(u.size.width),this._helper&&!l.animate&&this._proportionallyResize()),e("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(e){var t,s,n,a,o,r=this.options;o={minWidth:i(r.minWidth)?r.minWidth:0,maxWidth:i(r.maxWidth)?r.maxWidth:1/0,minHeight:i(r.minHeight)?r.minHeight:0,maxHeight:i(r.maxHeight)?r.maxHeight:1/0},(this._aspectRatio||e)&&(t=o.minHeight*this.aspectRatio,n=o.minWidth/this.aspectRatio,s=o.maxHeight*this.aspectRatio,a=o.maxWidth/this.aspectRatio,t>o.minWidth&&(o.minWidth=t),n>o.minHeight&&(o.minHeight=n),o.maxWidth>s&&(o.maxWidth=s),o.maxHeight>a&&(o.maxHeight=a)),this._vBoundaries=o},_updateCache:function(e){this.offset=this.helper.offset(),i(e.left)&&(this.position.left=e.left),i(e.top)&&(this.position.top=e.top),i(e.height)&&(this.size.height=e.height),i(e.width)&&(this.size.width=e.width)},_updateRatio:function(e){var t=this.position,s=this.size,n=this.axis;return i(e.height)?e.width=e.height*this.aspectRatio:i(e.width)&&(e.height=e.width/this.aspectRatio),"sw"===n&&(e.left=t.left+(s.width-e.width),e.top=null),"nw"===n&&(e.top=t.top+(s.height-e.height),e.left=t.left+(s.width-e.width)),e},_respectSize:function(e){var t=this._vBoundaries,s=this.axis,n=i(e.width)&&t.maxWidth&&t.maxWidthe.width,r=i(e.height)&&t.minHeight&&t.minHeight>e.height,h=this.originalPosition.left+this.originalSize.width,l=this.position.top+this.size.height,u=/sw|nw|w/.test(s),c=/nw|ne|n/.test(s);return o&&(e.width=t.minWidth),r&&(e.height=t.minHeight),n&&(e.width=t.maxWidth),a&&(e.height=t.maxHeight),o&&u&&(e.left=h-t.minWidth),n&&u&&(e.left=h-t.maxWidth),r&&c&&(e.top=l-t.minHeight),a&&c&&(e.top=l-t.maxHeight),e.width||e.height||e.left||!e.top?e.width||e.height||e.top||!e.left||(e.left=null):e.top=null,e},_proportionallyResize:function(){if(this._proportionallyResizeElements.length){var e,t,i,s,n,a=this.helper||this.element;for(e=0;this._proportionallyResizeElements.length>e;e++){if(n=this._proportionallyResizeElements[e],!this.borderDif)for(this.borderDif=[],i=[n.css("borderTopWidth"),n.css("borderRightWidth"),n.css("borderBottomWidth"),n.css("borderLeftWidth")],s=[n.css("paddingTop"),n.css("paddingRight"),n.css("paddingBottom"),n.css("paddingLeft")],t=0;i.length>t;t++)this.borderDif[t]=(parseInt(i[t],10)||0)+(parseInt(s[t],10)||0);n.css({height:a.height()-this.borderDif[0]-this.borderDif[2]||0,width:a.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var t=this.element,i=this.options;this.elementOffset=t.offset(),this._helper?(this.helper=this.helper||e("
    "),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(e,t){return{width:this.originalSize.width+t}},w:function(e,t){var i=this.originalSize,s=this.originalPosition;return{left:s.left+t,width:i.width-t}},n:function(e,t,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(e,t,i){return{height:this.originalSize.height+i}},se:function(t,i,s){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,i,s]))},sw:function(t,i,s){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,i,s]))},ne:function(t,i,s){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,i,s]))},nw:function(t,i,s){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,i,s]))}},_propagate:function(t,i){e.ui.plugin.call(this,t,[i,this.ui()]),"resize"!==t&&this._trigger(t,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),e.ui.plugin.add("resizable","animate",{stop:function(t){var i=e(this).data("ui-resizable"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&e.ui.hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,u=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(e.extend(h,u&&l?{top:u,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&e(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",t)}})}}),e.ui.plugin.add("resizable","containment",{start:function(){var i,s,n,a,o,r,h,l=e(this).data("ui-resizable"),u=l.options,c=l.element,d=u.containment,p=d instanceof e?d.get(0):/parent/.test(d)?c.parent().get(0):d;p&&(l.containerElement=e(p),/document/.test(d)||d===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}):(i=e(p),s=[],e(["Top","Right","Left","Bottom"]).each(function(e,n){s[e]=t(i.css("padding"+n))}),l.containerOffset=i.offset(),l.containerPosition=i.position(),l.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},n=l.containerOffset,a=l.containerSize.height,o=l.containerSize.width,r=e.ui.hasScroll(p,"left")?p.scrollWidth:o,h=e.ui.hasScroll(p)?p.scrollHeight:a,l.parentData={element:p,left:n.left,top:n.top,width:r,height:h}))},resize:function(t){var i,s,n,a,o=e(this).data("ui-resizable"),r=o.options,h=o.containerOffset,l=o.position,u=o._aspectRatio||t.shiftKey,c={top:0,left:0},d=o.containerElement;d[0]!==document&&/static/.test(d.css("position"))&&(c=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-c.left),u&&(o.size.height=o.size.width/o.aspectRatio),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),u&&(o.size.width=o.size.height*o.aspectRatio),o.position.top=o._helper?h.top:0),o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top,i=Math.abs((o._helper?o.offset.left-c.left:o.offset.left-c.left)+o.sizeDiff.width),s=Math.abs((o._helper?o.offset.top-c.top:o.offset.top-h.top)+o.sizeDiff.height),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a&&(i-=o.parentData.left),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,u&&(o.size.height=o.size.width/o.aspectRatio)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,u&&(o.size.width=o.size.height*o.aspectRatio))},stop:function(){var t=e(this).data("ui-resizable"),i=t.options,s=t.containerOffset,n=t.containerPosition,a=t.containerElement,o=e(t.helper),r=o.offset(),h=o.outerWidth()-t.sizeDiff.width,l=o.outerHeight()-t.sizeDiff.height;t._helper&&!i.animate&&/relative/.test(a.css("position"))&&e(this).css({left:r.left-n.left-s.left,width:h,height:l}),t._helper&&!i.animate&&/static/.test(a.css("position"))&&e(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),e.ui.plugin.add("resizable","alsoResize",{start:function(){var t=e(this).data("ui-resizable"),i=t.options,s=function(t){e(t).each(function(){var t=e(this);t.data("ui-resizable-alsoresize",{width:parseInt(t.width(),10),height:parseInt(t.height(),10),left:parseInt(t.css("left"),10),top:parseInt(t.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):e.each(i.alsoResize,function(e){s(e)})},resize:function(t,i){var s=e(this).data("ui-resizable"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(t,s){e(t).each(function(){var t=e(this),n=e(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:t.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(o,function(e,t){var i=(n[t]||0)+(r[t]||0);i&&i>=0&&(a[t]=i||null)}),t.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):e.each(n.alsoResize,function(e,t){h(e,t)})},stop:function(){e(this).removeData("resizable-alsoresize")}}),e.ui.plugin.add("resizable","ghost",{start:function(){var t=e(this).data("ui-resizable"),i=t.options,s=t.size;t.ghost=t.originalElement.clone(),t.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),t.ghost.appendTo(t.helper)},resize:function(){var t=e(this).data("ui-resizable");t.ghost&&t.ghost.css({position:"relative",height:t.size.height,width:t.size.width})},stop:function(){var t=e(this).data("ui-resizable");t.ghost&&t.helper&&t.helper.get(0).removeChild(t.ghost.get(0))}}),e.ui.plugin.add("resizable","grid",{resize:function(){var t=e(this).data("ui-resizable"),i=t.options,s=t.size,n=t.originalSize,a=t.originalPosition,o=t.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,h=r[0]||1,l=r[1]||1,u=Math.round((s.width-n.width)/h)*h,c=Math.round((s.height-n.height)/l)*l,d=n.width+u,p=n.height+c,f=i.maxWidth&&d>i.maxWidth,m=i.maxHeight&&p>i.maxHeight,g=i.minWidth&&i.minWidth>d,v=i.minHeight&&i.minHeight>p;i.grid=r,g&&(d+=h),v&&(p+=l),f&&(d-=h),m&&(p-=l),/^(se|s|e)$/.test(o)?(t.size.width=d,t.size.height=p):/^(ne)$/.test(o)?(t.size.width=d,t.size.height=p,t.position.top=a.top-c):/^(sw)$/.test(o)?(t.size.width=d,t.size.height=p,t.position.left=a.left-u):(t.size.width=d,t.size.height=p,t.position.top=a.top-c,t.position.left=a.left-u)}})})(jQuery);(function(e){e.widget("ui.selectable",e.ui.mouse,{version:"1.10.3",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var t,i=this;this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){t=e(i.options.filter,i.element[0]),t.addClass("ui-selectee"),t.each(function(){var t=e(this),i=t.offset();e.data(this,"selectable-item",{element:this,$element:t,left:i.left,top:i.top,right:i.left+t.outerWidth(),bottom:i.top+t.outerHeight(),startselected:!1,selected:t.hasClass("ui-selected"),selecting:t.hasClass("ui-selecting"),unselecting:t.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=t.addClass("ui-selectee"),this._mouseInit(),this.helper=e("
    ")},_destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled"),this._mouseDestroy()},_mouseStart:function(t){var i=this,s=this.options;this.opos=[t.pageX,t.pageY],this.options.disabled||(this.selectees=e(s.filter,this.element[0]),this._trigger("start",t),e(s.appendTo).append(this.helper),this.helper.css({left:t.pageX,top:t.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=e.data(this,"selectable-item");s.startselected=!0,t.metaKey||t.ctrlKey||(s.$element.removeClass("ui-selected"),s.selected=!1,s.$element.addClass("ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",t,{unselecting:s.element}))}),e(t.target).parents().addBack().each(function(){var s,n=e.data(this,"selectable-item");return n?(s=!t.metaKey&&!t.ctrlKey||!n.$element.hasClass("ui-selected"),n.$element.removeClass(s?"ui-unselecting":"ui-selected").addClass(s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",t,{selecting:n.element}):i._trigger("unselecting",t,{unselecting:n.element}),!1):undefined}))},_mouseDrag:function(t){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,a=this.opos[0],o=this.opos[1],r=t.pageX,h=t.pageY;return a>r&&(i=r,r=a,a=i),o>h&&(i=h,h=o,o=i),this.helper.css({left:a,top:o,width:r-a,height:h-o}),this.selectees.each(function(){var i=e.data(this,"selectable-item"),l=!1;i&&i.element!==s.element[0]&&("touch"===n.tolerance?l=!(i.left>r||a>i.right||i.top>h||o>i.bottom):"fit"===n.tolerance&&(l=i.left>a&&r>i.right&&i.top>o&&h>i.bottom),l?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,s._trigger("selecting",t,{selecting:i.element}))):(i.selecting&&((t.metaKey||t.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",t,{unselecting:i.element}))),i.selected&&(t.metaKey||t.ctrlKey||i.startselected||(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",t,{unselecting:i.element})))))}),!1}},_mouseStop:function(t){var i=this;return this.dragged=!1,e(".ui-unselecting",this.element[0]).each(function(){var s=e.data(this,"selectable-item");s.$element.removeClass("ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",t,{unselected:s.element})}),e(".ui-selecting",this.element[0]).each(function(){var s=e.data(this,"selectable-item");s.$element.removeClass("ui-selecting").addClass("ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",t,{selected:s.element})}),this._trigger("stop",t),this.helper.remove(),!1}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.3",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,a=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,a.widgetName+"-item")===a?(s=t(this),!1):undefined}),t.data(e.target,a.widgetName+"-item")===a&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,a,o=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,o.cursorAt&&this._adjustOffsetFromHelper(o.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),o.containment&&this._setContainment(),o.cursor&&"auto"!==o.cursor&&(a=this.document.find("body"),this.storedCursor=a.css("cursor"),a.css("cursor",o.cursor),this.storedStylesheet=t("").appendTo(a)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!o.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,a,o=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],a=this._intersectsWithPointer(s),a&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===a?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===a?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),a=this.options.axis,o={};a&&"x"!==a||(o.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),a&&"y"!==a||(o.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(o,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,a=t.left,o=a+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>a&&o>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>a&&o>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,a=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return n?this.floating?o&&"right"===o||"down"===a?2:1:a&&("down"===a?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return this.floating&&a?"right"===a&&s||"left"===a&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){var i,s,n,a,o=[],r=[],h=this._connectWith();if(h&&e)for(i=h.length-1;i>=0;i--)for(n=t(h[i]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&r.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(r.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),i=r.length-1;i>=0;i--)r[i][0].each(function(){o.push(this)});return t(o)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,a,o,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&(u.push([t.isFunction(a.options.items)?a.options.items.call(a.element[0],e,{item:this.currentItem}):t(a.options.items,a.element),a]),this.containers.push(a));for(i=u.length-1;i>=0;i--)for(o=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",o),c.push({item:h,instance:o,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,a;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),a=n.offset(),s.left=a.left,s.top=a.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)a=this.containers[i].element.offset(),this.containers[i].containerCache.left=a.left,this.containers[i].containerCache.top=a.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t("
    ",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,a,o,r,h,l,c,u,d,p,f=null,m=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],m=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[m].containerCache.over||(this.containers[m]._trigger("over",s,this._uiHash(this)),this.containers[m].containerCache.over=1);else{for(o=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],a=this.items.length-1;a>=0;a--)t.contains(this.containers[m].element[0],this.items[a].item[0])&&this.items[a].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[a].top,this.items[a].height))&&(u=this.items[a].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[a][l]-c)&&(d=!0,u+=this.items[a][l]),o>Math.abs(u-c)&&(o=Math.abs(u-c),r=this.items[a],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[m])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[m].element,!0),this._trigger("change",s,this._uiHash()),this.containers[m]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[m],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[m]._trigger("over",s,this._uiHash(this)),this.containers[m].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():a?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():a?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,a=e.pageX,o=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(a=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(o=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1],o=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((a-this.originalPageX)/n.grid[0])*n.grid[0],a=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:a-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){this.reverting=!1;var i,s=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(i in this._storedCSS)("auto"===this._storedCSS[i]||"static"===this._storedCSS[i])&&(this._storedCSS[i]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&s.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||s.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(s.push(function(t){this._trigger("remove",t,this._uiHash())}),s.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),s.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),i=this.containers.length-1;i>=0;i--)e||s.push(function(t){return function(e){t._trigger("deactivate",e,this._uiHash(this))}}.call(this,this.containers[i])),this.containers[i].containerCache.over&&(s.push(function(t){return function(e){t._trigger("out",e,this._uiHash(this))}}.call(this,this.containers[i])),this.containers[i].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),i=0;s.length>i;i++)s[i].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(i=0;s.length>i;i++)s[i].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(t){var e=0,i={},s={};i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="hide",s.height=s.paddingTop=s.paddingBottom=s.borderTopWidth=s.borderBottomWidth="show",t.widget("ui.accordion",{version:"1.10.3",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),0>e.active&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t(),content:this.active.length?this.active.next():t()}},_createIcons:function(){var e=this.options.icons;e&&(t("").addClass("ui-accordion-header-icon ui-icon "+e.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(e.header).addClass(e.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?(this._activate(e),undefined):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),"disabled"===t&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!e),undefined)},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var i=t.ui.keyCode,s=this.headers.length,n=this.headers.index(e.target),a=!1;switch(e.keyCode){case i.RIGHT:case i.DOWN:a=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:a=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(e);break;case i.HOME:a=this.headers[0];break;case i.END:a=this.headers[s-1]}a&&(t(e.target).attr("tabIndex",-1),t(a).attr("tabIndex",0),a.focus(),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().focus()},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide()},_refresh:function(){var i,s=this.options,n=s.heightStyle,a=this.element.parent(),o=this.accordionId="ui-accordion-"+(this.element.attr("id")||++e);this.active=this._findActive(s.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(e){var i=t(this),s=i.attr("id"),n=i.next(),a=n.attr("id");s||(s=o+"-header-"+e,i.attr("id",s)),a||(a=o+"-panel-"+e,n.attr("id",a)),i.attr("aria-controls",a),n.attr("aria-labelledby",s)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false",tabIndex:-1}).next().attr({"aria-expanded":"false","aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true",tabIndex:0}).next().attr({"aria-expanded":"true","aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(s.event),"fill"===n?(i=a.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.headers.each(function(){i-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===n&&(i=0,this.headers.next().each(function(){i=Math.max(i,t(this).css("height","").height())}).height(i))},_activate:function(e){var i=this._findActive(e)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var i={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),a=n[0]===s[0],o=a&&i.collapsible,r=o?t():n.next(),h=s.next(),l={oldHeader:s,oldPanel:h,newHeader:o?t():n,newPanel:r};e.preventDefault(),a&&!i.collapsible||this._trigger("beforeActivate",e,l)===!1||(i.active=o?!1:this.headers.index(n),this.active=a?t():n,this._toggle(l),s.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),a||(n.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&n.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),n.next().addClass("ui-accordion-content-active")))},_toggle:function(e){var i=e.newPanel,s=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,e):(s.hide(),i.show(),this._toggleComplete(e)),s.attr({"aria-expanded":"false","aria-hidden":"true"}),s.prev().attr("aria-selected","false"),i.length&&s.length?s.prev().attr("tabIndex",-1):i.length&&this.headers.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr({"aria-expanded":"true","aria-hidden":"false"}).prev().attr({"aria-selected":"true",tabIndex:0})},_animate:function(t,e,n){var a,o,r,h=this,l=0,c=t.length&&(!e.length||t.index()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,undefined;e=!1,s=!1,i=!1;var a=t.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:e=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case a.UP:e=!0,this._keyEvent("previous",n);break;case a.DOWN:e=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),undefined):(this._searchTimeout(t),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(t),this._change(t),undefined)}}),this._initSource(),this.menu=t("
     
    "+"",S=u?"":"",x=0;7>x;x++)N=(x+c)%7,S+="=5?" class='ui-datepicker-week-end'":"")+">"+""+p[N]+"";for(M+=S+"",I=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,I)),P=(this._getFirstDayOfMonth(te,Z)-c+7)%7,A=Math.ceil((P+I)/7),z=q?this.maxRows>A?this.maxRows:A:A,this.maxRows=z,H=this._daylightSavingAdjust(new Date(te,Z,1-P)),E=0;z>E;E++){for(M+="",F=u?"":"",x=0;7>x;x++)O=g?g.apply(t.input?t.input[0]:null,[H]):[!0,""],W=H.getMonth()!==Z,j=W&&!_||!O[0]||G&&G>H||$&&H>$,F+="",H.setDate(H.getDate()+1),H=this._daylightSavingAdjust(H);M+=F+""}Z++,Z>11&&(Z=0,te++),M+="
    "+this._get(t,"weekHeader")+"
    "+this._get(t,"calculateWeek")(H)+""+(W&&!v?" ":j?""+H.getDate()+"":""+H.getDate()+"")+"
    "+(q?""+(Q[0]>0&&D===Q[1]-1?"
    ":""):""),w+=M}y+=w}return y+=l,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,a,r,o){var h,l,c,u,d,p,f,m,g=this._get(t,"changeMonth"),v=this._get(t,"changeYear"),_=this._get(t,"showMonthAfterYear"),b="
    ",y="";if(a||!g)y+=""+r[e]+"";else{for(h=s&&s.getFullYear()===i,l=n&&n.getFullYear()===i,y+=""}if(_||(b+=y+(!a&&g&&v?"":" ")),!t.yearshtml)if(t.yearshtml="",a||!v)b+=""+i+"";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10); +return isNaN(e)?d:e},f=p(u[0]),m=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,m=n?Math.min(m,n.getFullYear()):m,t.yearshtml+="",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),_&&(b+=(!a&&g&&v?"":" ")+y),b+="
    "},_adjustInstDate:function(t,e,i){var s=t.drawYear+("Y"===i?e:0),n=t.drawMonth+("M"===i?e:0),a=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),r=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,a)));t.selectedDay=r.getDate(),t.drawMonth=t.selectedMonth=r.getMonth(),t.drawYear=t.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),a=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&a.setDate(this._getDaysInMonth(a.getFullYear(),a.getMonth())),this._isInRange(t,a)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),a=this._getMinMaxDate(t,"max"),r=null,o=null,h=this._get(t,"yearRange");return h&&(i=h.split(":"),s=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=s),i[1].match(/[+\-].*/)&&(o+=s)),(!n||e.getTime()>=n.getTime())&&(!a||e.getTime()<=a.getTime())&&(!r||e.getFullYear()>=r)&&(!o||o>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).mousedown(t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new i,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.10.3"})(jQuery);(function(t){var e={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};t.widget("ui.dialog",{version:"1.10.3",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(e){var i=t(this).css(e).offset().top;0>i&&t(this).css("top",e.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&t.fn.draggable&&this._makeDraggable(),this.options.resizable&&t.fn.resizable&&this._makeResizable(),this._isOpen=!1},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var e=this.options.appendTo;return e&&(e.jquery||e.nodeType)?t(e):this.document.find(e||"body").eq(0)},_destroy:function(){var t,e=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),t=e.parent.children().eq(e.index),t.length&&t[0]!==this.element[0]?t.before(this.element):e.parent.append(this.element)},widget:function(){return this.uiDialog},disable:t.noop,enable:t.noop,close:function(e){var i=this;this._isOpen&&this._trigger("beforeClose",e)!==!1&&(this._isOpen=!1,this._destroyOverlay(),this.opener.filter(":focusable").focus().length||t(this.document[0].activeElement).blur(),this._hide(this.uiDialog,this.options.hide,function(){i._trigger("close",e)}))},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(t,e){var i=!!this.uiDialog.nextAll(":visible").insertBefore(this.uiDialog).length;return i&&!e&&this._trigger("focus",t),i},open:function(){var e=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),undefined):(this._isOpen=!0,this.opener=t(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this._show(this.uiDialog,this.options.show,function(){e._focusTabbable(),e._trigger("focus")}),this._trigger("open"),undefined)},_focusTabbable:function(){var t=this.element.find("[autofocus]");t.length||(t=this.element.find(":tabbable")),t.length||(t=this.uiDialogButtonPane.find(":tabbable")),t.length||(t=this.uiDialogTitlebarClose.filter(":tabbable")),t.length||(t=this.uiDialog),t.eq(0).focus()},_keepFocus:function(e){function i(){var e=this.document[0].activeElement,i=this.uiDialog[0]===e||t.contains(this.uiDialog[0],e);i||this._focusTabbable()}e.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=t("
    ").addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front "+this.options.dialogClass).hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._on(this.uiDialog,{keydown:function(e){if(this.options.closeOnEscape&&!e.isDefaultPrevented()&&e.keyCode&&e.keyCode===t.ui.keyCode.ESCAPE)return e.preventDefault(),this.close(e),undefined;if(e.keyCode===t.ui.keyCode.TAB){var i=this.uiDialog.find(":tabbable"),s=i.filter(":first"),n=i.filter(":last");e.target!==n[0]&&e.target!==this.uiDialog[0]||e.shiftKey?e.target!==s[0]&&e.target!==this.uiDialog[0]||!e.shiftKey||(n.focus(1),e.preventDefault()):(s.focus(1),e.preventDefault())}},mousedown:function(t){this._moveToTop(t)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var e;this.uiDialogTitlebar=t("
    ").addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(this.uiDialog),this._on(this.uiDialogTitlebar,{mousedown:function(e){t(e.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.focus()}}),this.uiDialogTitlebarClose=t("").button({label:this.options.closeText,icons:{primary:"ui-icon-closethick"},text:!1}).addClass("ui-dialog-titlebar-close").appendTo(this.uiDialogTitlebar),this._on(this.uiDialogTitlebarClose,{click:function(t){t.preventDefault(),this.close(t)}}),e=t("").uniqueId().addClass("ui-dialog-title").prependTo(this.uiDialogTitlebar),this._title(e),this.uiDialog.attr({"aria-labelledby":e.attr("id")})},_title:function(t){this.options.title||t.html(" "),t.text(this.options.title)},_createButtonPane:function(){this.uiDialogButtonPane=t("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),this.uiButtonSet=t("
    ").addClass("ui-dialog-buttonset").appendTo(this.uiDialogButtonPane),this._createButtons()},_createButtons:function(){var e=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),t.isEmptyObject(i)||t.isArray(i)&&!i.length?(this.uiDialog.removeClass("ui-dialog-buttons"),undefined):(t.each(i,function(i,s){var n,a;s=t.isFunction(s)?{click:s,text:i}:s,s=t.extend({type:"button"},s),n=s.click,s.click=function(){n.apply(e.element[0],arguments)},a={icons:s.icons,text:s.showText},delete s.icons,delete s.showText,t("",s).button(a).appendTo(e.uiButtonSet)}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),undefined)},_makeDraggable:function(){function e(t){return{position:t.position,offset:t.offset}}var i=this,s=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(s,n){t(this).addClass("ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",s,e(n))},drag:function(t,s){i._trigger("drag",t,e(s))},stop:function(n,a){s.position=[a.position.left-i.document.scrollLeft(),a.position.top-i.document.scrollTop()],t(this).removeClass("ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",n,e(a))}})},_makeResizable:function(){function e(t){return{originalPosition:t.originalPosition,originalSize:t.originalSize,position:t.position,size:t.size}}var i=this,s=this.options,n=s.resizable,a=this.uiDialog.css("position"),o="string"==typeof n?n:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:s.maxWidth,maxHeight:s.maxHeight,minWidth:s.minWidth,minHeight:this._minHeight(),handles:o,start:function(s,n){t(this).addClass("ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",s,e(n))},resize:function(t,s){i._trigger("resize",t,e(s))},stop:function(n,a){s.height=t(this).height(),s.width=t(this).width(),t(this).removeClass("ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",n,e(a))}}).css("position",a)},_minHeight:function(){var t=this.options;return"auto"===t.height?t.minHeight:Math.min(t.minHeight,t.height)},_position:function(){var t=this.uiDialog.is(":visible");t||this.uiDialog.show(),this.uiDialog.position(this.options.position),t||this.uiDialog.hide()},_setOptions:function(s){var n=this,a=!1,o={};t.each(s,function(t,s){n._setOption(t,s),t in e&&(a=!0),t in i&&(o[t]=s)}),a&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",o)},_setOption:function(t,e){var i,s,n=this.uiDialog;"dialogClass"===t&&n.removeClass(this.options.dialogClass).addClass(e),"disabled"!==t&&(this._super(t,e),"appendTo"===t&&this.uiDialog.appendTo(this._appendTo()),"buttons"===t&&this._createButtons(),"closeText"===t&&this.uiDialogTitlebarClose.button({label:""+e}),"draggable"===t&&(i=n.is(":data(ui-draggable)"),i&&!e&&n.draggable("destroy"),!i&&e&&this._makeDraggable()),"position"===t&&this._position(),"resizable"===t&&(s=n.is(":data(ui-resizable)"),s&&!e&&n.resizable("destroy"),s&&"string"==typeof e&&n.resizable("option","handles",e),s||e===!1||this._makeResizable()),"title"===t&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var t,e,i,s=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),s.minWidth>s.width&&(s.width=s.minWidth),t=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),e=Math.max(0,s.minHeight-t),i="number"==typeof s.maxHeight?Math.max(0,s.maxHeight-t):"none","auto"===s.height?this.element.css({minHeight:e,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,s.height-t)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var e=t(this);return t("
    ").css({position:"absolute",width:e.outerWidth(),height:e.outerHeight()}).appendTo(e.parent()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(e){return t(e.target).closest(".ui-dialog").length?!0:!!t(e.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var e=this,i=this.widgetFullName;t.ui.dialog.overlayInstances||this._delay(function(){t.ui.dialog.overlayInstances&&this.document.bind("focusin.dialog",function(s){e._allowInteraction(s)||(s.preventDefault(),t(".ui-dialog:visible:last .ui-dialog-content").data(i)._focusTabbable())})}),this.overlay=t("
    ").addClass("ui-widget-overlay ui-front").appendTo(this._appendTo()),this._on(this.overlay,{mousedown:"_keepFocus"}),t.ui.dialog.overlayInstances++}},_destroyOverlay:function(){this.options.modal&&this.overlay&&(t.ui.dialog.overlayInstances--,t.ui.dialog.overlayInstances||this.document.unbind("focusin.dialog"),this.overlay.remove(),this.overlay=null)}}),t.ui.dialog.overlayInstances=0,t.uiBackCompat!==!1&&t.widget("ui.dialog",t.ui.dialog,{_position:function(){var e,i=this.options.position,s=[],n=[0,0];i?(("string"==typeof i||"object"==typeof i&&"0"in i)&&(s=i.split?i.split(" "):[i[0],i[1]],1===s.length&&(s[1]=s[0]),t.each(["left","top"],function(t,e){+s[t]===s[t]&&(n[t]=s[t],s[t]=e)}),i={my:s[0]+(0>n[0]?n[0]:"+"+n[0])+" "+s[1]+(0>n[1]?n[1]:"+"+n[1]),at:s.join(" ")}),i=t.extend({},t.ui.dialog.prototype.options.position,i)):i=t.ui.dialog.prototype.options.position,e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.position(i),e||this.uiDialog.hide()}})})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.3",defaultElement:"
      ",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.mouseHandled=!0,this.select(e),i.has(".ui-menu").length?this.expand(e):this.element.is(":focus")||(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,h=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:h=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}h&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()
    ").appendTo(this.element),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(t){return t===e?this.options.value:(this.options.value=this._constrainedValue(t),this._refreshValue(),e)},_constrainedValue:function(t){return t===e&&(t=this.options.value),this.indeterminate=t===!1,"number"!=typeof t&&(t=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var e=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(i.toFixed(0)+"%"),this.element.toggleClass("ui-progressbar-indeterminate",this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=t("
    ").appendTo(this.valueDiv))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":e}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),e===this.options.max&&this._trigger("complete")}})})(jQuery);(function(t){var e=5;t.widget("ui.slider",t.ui.mouse,{version:"1.10.3",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null,change:null,slide:null,start:null,stop:null},_create:function(){this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"),this._refresh(),this._setOption("disabled",this.options.disabled),this._animateOff=!1},_refresh:function(){this._createRange(),this._createHandles(),this._setupEvents(),this._refreshValue()},_createHandles:function(){var e,i,s=this.options,n=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),a="",o=[];for(i=s.values&&s.values.length||1,n.length>i&&(n.slice(i).remove(),n=n.slice(0,i)),e=n.length;i>e;e++)o.push(a);this.handles=n.add(t(o.join("")).appendTo(this.element)),this.handle=this.handles.eq(0),this.handles.each(function(e){t(this).data("ui-slider-handle-index",e)})},_createRange:function(){var e=this.options,i="";e.range?(e.range===!0&&(e.values?e.values.length&&2!==e.values.length?e.values=[e.values[0],e.values[0]]:t.isArray(e.values)&&(e.values=e.values.slice(0)):e.values=[this._valueMin(),this._valueMin()]),this.range&&this.range.length?this.range.removeClass("ui-slider-range-min ui-slider-range-max").css({left:"",bottom:""}):(this.range=t("
    ").appendTo(this.element),i="ui-slider-range ui-widget-header ui-corner-all"),this.range.addClass(i+("min"===e.range||"max"===e.range?" ui-slider-range-"+e.range:""))):this.range=t([])},_setupEvents:function(){var t=this.handles.add(this.range).filter("a");this._off(t),this._on(t,this._handleEvents),this._hoverable(t),this._focusable(t)},_destroy:function(){this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-widget ui-widget-content ui-corner-all"),this._mouseDestroy()},_mouseCapture:function(e){var i,s,n,a,o,r,h,l,u=this,c=this.options;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),i={x:e.pageX,y:e.pageY},s=this._normValueFromMouse(i),n=this._valueMax()-this._valueMin()+1,this.handles.each(function(e){var i=Math.abs(s-u.values(e));(n>i||n===i&&(e===u._lastChangedValue||u.values(e)===c.min))&&(n=i,a=t(this),o=e)}),r=this._start(e,o),r===!1?!1:(this._mouseSliding=!0,this._handleIndex=o,a.addClass("ui-state-active").focus(),h=a.offset(),l=!t(e.target).parents().addBack().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:e.pageX-h.left-a.width()/2,top:e.pageY-h.top-a.height()/2-(parseInt(a.css("borderTopWidth"),10)||0)-(parseInt(a.css("borderBottomWidth"),10)||0)+(parseInt(a.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(e,o,s),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(t){var e={x:t.pageX,y:t.pageY},i=this._normValueFromMouse(e);return this._slide(t,this._handleIndex,i),!1},_mouseStop:function(t){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(t,this._handleIndex),this._change(t,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation="vertical"===this.options.orientation?"vertical":"horizontal"},_normValueFromMouse:function(t){var e,i,s,n,a;return"horizontal"===this.orientation?(e=this.elementSize.width,i=t.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(e=this.elementSize.height,i=t.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),s=i/e,s>1&&(s=1),0>s&&(s=0),"vertical"===this.orientation&&(s=1-s),n=this._valueMax()-this._valueMin(),a=this._valueMin()+s*n,this._trimAlignValue(a)},_start:function(t,e){var i={handle:this.handles[e],value:this.value()};return this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("start",t,i)},_slide:function(t,e,i){var s,n,a;this.options.values&&this.options.values.length?(s=this.values(e?0:1),2===this.options.values.length&&this.options.range===!0&&(0===e&&i>s||1===e&&s>i)&&(i=s),i!==this.values(e)&&(n=this.values(),n[e]=i,a=this._trigger("slide",t,{handle:this.handles[e],value:i,values:n}),s=this.values(e?0:1),a!==!1&&this.values(e,i,!0))):i!==this.value()&&(a=this._trigger("slide",t,{handle:this.handles[e],value:i}),a!==!1&&this.value(i))},_stop:function(t,e){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("stop",t,i)},_change:function(t,e){if(!this._keySliding&&!this._mouseSliding){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._lastChangedValue=e,this._trigger("change",t,i)}},value:function(t){return arguments.length?(this.options.value=this._trimAlignValue(t),this._refreshValue(),this._change(null,0),undefined):this._value()},values:function(e,i){var s,n,a;if(arguments.length>1)return this.options.values[e]=this._trimAlignValue(i),this._refreshValue(),this._change(null,e),undefined;if(!arguments.length)return this._values();if(!t.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(e):this.value();for(s=this.options.values,n=arguments[0],a=0;s.length>a;a+=1)s[a]=this._trimAlignValue(n[a]),this._change(null,a);this._refreshValue()},_setOption:function(e,i){var s,n=0;switch("range"===e&&this.options.range===!0&&("min"===i?(this.options.value=this._values(0),this.options.values=null):"max"===i&&(this.options.value=this._values(this.options.values.length-1),this.options.values=null)),t.isArray(this.options.values)&&(n=this.options.values.length),t.Widget.prototype._setOption.apply(this,arguments),e){case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":for(this._animateOff=!0,this._refreshValue(),s=0;n>s;s+=1)this._change(null,s);this._animateOff=!1;break;case"min":case"max":this._animateOff=!0,this._refreshValue(),this._animateOff=!1;break;case"range":this._animateOff=!0,this._refresh(),this._animateOff=!1}},_value:function(){var t=this.options.value;return t=this._trimAlignValue(t)},_values:function(t){var e,i,s;if(arguments.length)return e=this.options.values[t],e=this._trimAlignValue(e);if(this.options.values&&this.options.values.length){for(i=this.options.values.slice(),s=0;i.length>s;s+=1)i[s]=this._trimAlignValue(i[s]);return i}return[]},_trimAlignValue:function(t){if(this._valueMin()>=t)return this._valueMin();if(t>=this._valueMax())return this._valueMax();var e=this.options.step>0?this.options.step:1,i=(t-this._valueMin())%e,s=t-i;return 2*Math.abs(i)>=e&&(s+=i>0?e:-e),parseFloat(s.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var e,i,s,n,a,o=this.options.range,r=this.options,h=this,l=this._animateOff?!1:r.animate,u={};this.options.values&&this.options.values.length?this.handles.each(function(s){i=100*((h.values(s)-h._valueMin())/(h._valueMax()-h._valueMin())),u["horizontal"===h.orientation?"left":"bottom"]=i+"%",t(this).stop(1,1)[l?"animate":"css"](u,r.animate),h.options.range===!0&&("horizontal"===h.orientation?(0===s&&h.range.stop(1,1)[l?"animate":"css"]({left:i+"%"},r.animate),1===s&&h.range[l?"animate":"css"]({width:i-e+"%"},{queue:!1,duration:r.animate})):(0===s&&h.range.stop(1,1)[l?"animate":"css"]({bottom:i+"%"},r.animate),1===s&&h.range[l?"animate":"css"]({height:i-e+"%"},{queue:!1,duration:r.animate}))),e=i}):(s=this.value(),n=this._valueMin(),a=this._valueMax(),i=a!==n?100*((s-n)/(a-n)):0,u["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[l?"animate":"css"](u,r.animate),"min"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({width:i+"%"},r.animate),"max"===o&&"horizontal"===this.orientation&&this.range[l?"animate":"css"]({width:100-i+"%"},{queue:!1,duration:r.animate}),"min"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({height:i+"%"},r.animate),"max"===o&&"vertical"===this.orientation&&this.range[l?"animate":"css"]({height:100-i+"%"},{queue:!1,duration:r.animate}))},_handleEvents:{keydown:function(i){var s,n,a,o,r=t(i.target).data("ui-slider-handle-index");switch(i.keyCode){case t.ui.keyCode.HOME:case t.ui.keyCode.END:case t.ui.keyCode.PAGE_UP:case t.ui.keyCode.PAGE_DOWN:case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(i.preventDefault(),!this._keySliding&&(this._keySliding=!0,t(i.target).addClass("ui-state-active"),s=this._start(i,r),s===!1))return}switch(o=this.options.step,n=a=this.options.values&&this.options.values.length?this.values(r):this.value(),i.keyCode){case t.ui.keyCode.HOME:a=this._valueMin();break;case t.ui.keyCode.END:a=this._valueMax();break;case t.ui.keyCode.PAGE_UP:a=this._trimAlignValue(n+(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.PAGE_DOWN:a=this._trimAlignValue(n-(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:if(n===this._valueMax())return;a=this._trimAlignValue(n+o);break;case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(n===this._valueMin())return;a=this._trimAlignValue(n-o)}this._slide(i,r,a)},click:function(t){t.preventDefault()},keyup:function(e){var i=t(e.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(e,i),this._change(e,i),t(e.target).removeClass("ui-state-active"))}}})})(jQuery);(function(t){function e(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.widget("ui.spinner",{version:"1.10.3",defaultElement:"",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e={},i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);void 0!==n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var t=this.element[0]===this.document[0].activeElement;t||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var t=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=t.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*t.height())&&t.height()>0&&t.height(t.height()),this.options.disabled&&this.disable()},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""+""+""+""+""},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){if("culture"===t||"numberFormat"===t){var i=this._parse(this.element.val());return this.options[t]=e,this.element.val(this._format(i)),void 0}("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(this.buttons.first().find(".ui-icon").removeClass(this.options.icons.up).addClass(e.up),this.buttons.last().find(".ui-icon").removeClass(this.options.icons.down).addClass(e.down)),this._super(t,e),"disabled"===t&&(e?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:e(function(t){this._super(t),this._value(this.element.val())}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:e(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:e(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:e(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:e(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(e(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}})})(jQuery);(function(t,e){function i(){return++n}function s(t){return t.hash.length>1&&decodeURIComponent(t.href.replace(a,""))===decodeURIComponent(location.href.replace(a,""))}var n=0,a=/#.*$/;t.widget("ui.tabs",{version:"1.10.3",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var e=this,i=this.options;this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",i.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var i=this.options.active,s=this.options.collapsible,n=location.hash.substring(1);return null===i&&(n&&this.tabs.each(function(s,a){return t(a).attr("aria-controls")===n?(i=s,!1):e}),null===i&&(i=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===i||-1===i)&&(i=this.tabs.length?0:!1)),i!==!1&&(i=this.tabs.index(this.tabs.eq(i)),-1===i&&(i=s?!1:0)),!s&&i===!1&&this.anchors.length&&(i=0),i},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(i){var s=t(this.document[0].activeElement).closest("li"),n=this.tabs.index(s),a=!0;if(!this._handlePageNav(i)){switch(i.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:n++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:a=!1,n--;break;case t.ui.keyCode.END:n=this.anchors.length-1;break;case t.ui.keyCode.HOME:n=0;break;case t.ui.keyCode.SPACE:return i.preventDefault(),clearTimeout(this.activating),this._activate(n),e;case t.ui.keyCode.ENTER:return i.preventDefault(),clearTimeout(this.activating),this._activate(n===this.options.active?!1:n),e;default:return}i.preventDefault(),clearTimeout(this.activating),n=this._focusNextTab(n,a),i.ctrlKey||(s.attr("aria-selected","false"),this.tabs.eq(n).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",n)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.focus())},_handlePageNav:function(i){return i.altKey&&i.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):i.altKey&&i.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):e},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).focus(),t},_setOption:function(t,i){return"active"===t?(this._activate(i),e):"disabled"===t?(this._setupDisabled(i),e):(this._super(t,i),"collapsible"===t&&(this.element.toggleClass("ui-tabs-collapsible",i),i||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(i),"heightStyle"===t&&this._setupHeightStyle(i),e)},_tabId:function(t){return t.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=t(),this.anchors.each(function(i,n){var a,o,r,h=t(n).uniqueId().attr("id"),l=t(n).closest("li"),u=l.attr("aria-controls");s(n)?(a=n.hash,o=e.element.find(e._sanitizeSelector(a))):(r=e._tabId(l),a="#"+r,o=e.element.find(a),o.length||(o=e._createPanel(r),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),u&&l.data("ui-tabs-aria-controls",u),l.attr({"aria-controls":a.substring(1),"aria-labelledby":h}),o.attr("aria-labelledby",h)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.element.find("ol,ul").eq(0)},_createPanel:function(e){return t("
    ").attr("id",e).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(e){t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1);for(var i,s=0;i=this.tabs[s];s++)e===!0||-1!==t.inArray(s,e)?t(i).addClass("ui-state-disabled").attr("aria-disabled","true"):t(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=e},_setupEvents:function(e){var i={click:function(t){t.preventDefault()}};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),a=n.closest("li"),o=a[0]===s[0],r=o&&i.collapsible,h=r?t():this._getPanelForTab(a),l=s.length?this._getPanelForTab(s):t(),u={oldTab:s,oldPanel:l,newTab:r?t():a,newPanel:h};e.preventDefault(),a.hasClass("ui-state-disabled")||a.hasClass("ui-tabs-loading")||this.running||o&&!i.collapsible||this._trigger("beforeActivate",e,u)===!1||(i.active=r?!1:this.tabs.index(a),this.active=o?t():a,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(a),e),this._toggle(e,u))},_toggle:function(e,i){function s(){a.running=!1,a._trigger("activate",e,i)}function n(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),o.length&&a.options.show?a._show(o,a.options.show,s):(o.show(),s())}var a=this,o=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),n()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),r.hide(),n()),r.attr({"aria-expanded":"false","aria-hidden":"true"}),i.oldTab.attr("aria-selected","false"),o.length&&r.length?i.oldTab.attr("tabIndex",-1):o.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),o.attr({"aria-expanded":"true","aria-hidden":"false"}),i.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(t){return"string"==typeof t&&(t=this.anchors.index(this.anchors.filter("[href$='"+t+"']"))),t},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var s=this.options.disabled;s!==!1&&(i===e?s=!1:(i=this._getIndex(i),s=t.isArray(s)?t.map(s,function(t){return t!==i?t:null}):t.map(this.tabs,function(t,e){return e!==i?e:null})),this._setupDisabled(s))},disable:function(i){var s=this.options.disabled;if(s!==!0){if(i===e)s=!0;else{if(i=this._getIndex(i),-1!==t.inArray(i,s))return;s=t.isArray(s)?t.merge([i],s).sort():[i]}this._setupDisabled(s)}},load:function(e,i){e=this._getIndex(e);var n=this,a=this.tabs.eq(e),o=a.find(".ui-tabs-anchor"),r=this._getPanelForTab(a),h={tab:a,panel:r};s(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,h)),this.xhr&&"canceled"!==this.xhr.statusText&&(a.addClass("ui-tabs-loading"),r.attr("aria-busy","true"),this.xhr.success(function(t){setTimeout(function(){r.html(t),n._trigger("load",i,h)},1)}).complete(function(t,e){setTimeout(function(){"abort"===e&&n.panels.stop(!1,!0),a.removeClass("ui-tabs-loading"),r.removeAttr("aria-busy"),t===n.xhr&&delete n.xhr},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href"),beforeSend:function(e,a){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:a},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}})})(jQuery);(function(t){function e(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))}function i(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")}var s=0;t.widget("ui.tooltip",{version:"1.10.3",options:{content:function(){var e=t(this).attr("title")||"";return t("").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(e,i){var s=this;return"disabled"===e?(this[i?"_disable":"_enable"](),this.options[e]=i,void 0):(this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e)}),void 0)},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0)}),this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.is("[title]")&&e.data("ui-tooltip-title",e.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))})},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,a=e?e.type:null;return"string"==typeof s?this._open(e,t,s):(i=s.call(t[0],function(i){t.data("ui-tooltip-open")&&n._delay(function(){e&&(e.type=a),this._open(e,t,i)})}),i&&this._open(e,t,i),void 0)},_open:function(i,s,n){function a(t){l.of=t,o.is(":hidden")||o.position(l)}var o,r,h,l=t.extend({},this.options.position);if(n){if(o=this._find(s),o.length)return o.find(".ui-tooltip-content").html(n),void 0;s.is("[title]")&&(i&&"mouseover"===i.type?s.attr("title",""):s.removeAttr("title")),o=this._tooltip(s),e(s,o.attr("id")),o.find(".ui-tooltip-content").html(n),this.options.track&&i&&/^mouse/.test(i.type)?(this._on(this.document,{mousemove:a}),a(i)):o.position(t.extend({of:s},this.options.position)),o.hide(),this._show(o,this.options.show),this.options.show&&this.options.show.delay&&(h=this.delayedShow=setInterval(function(){o.is(":visible")&&(a(l.of),clearInterval(h))},t.fx.interval)),this._trigger("open",i,{tooltip:o}),r={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var i=t.Event(e);i.currentTarget=s[0],this.close(i,!0)}},remove:function(){this._removeTooltip(o)}},i&&"mouseover"!==i.type||(r.mouseleave="close"),i&&"focusin"!==i.type||(r.focusout="close"),this._on(!0,s,r)}},close:function(e){var s=this,n=t(e?e.currentTarget:this.element),a=this._find(n);this.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&n.attr("title",n.data("ui-tooltip-title")),i(n),a.stop(!0),this._hide(a,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),this.closing=!0,this._trigger("close",e,{tooltip:a}),this.closing=!1)},_tooltip:function(e){var i="ui-tooltip-"+s++,n=t("
    ").attr({id:i,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return t("
    ").addClass("ui-tooltip-content").appendTo(n),n.appendTo(this.document[0].body),this.tooltips[i]=e,n},_find:function(e){var i=e.data("ui-tooltip-id");return i?t("#"+i):t()},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0),t("#"+i).remove(),s.data("ui-tooltip-title")&&(s.attr("title",s.data("ui-tooltip-title")),s.removeData("ui-tooltip-title"))})}})})(jQuery);(function(t,e){var i="ui-effects-";t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,a){var o,r=a.re.exec(i),h=r&&a.parse(r),l=a.space||"rgba";return h?(o=s[l](h),s[c[l].cache]=o[c[l].cache],n=s._rgba=o._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,a.transparent),s):a[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

    ")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,o,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=e);var u=this,d=t.type(n),p=this._rgba=[];return o!==e&&(n=[n,o,r,h],d="array"),"string"===d?this.parse(s(n)||a._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var a=s.cache;f(s.props,function(t,e){if(!u[a]&&s.to){if("alpha"===t||null==n[t])return;u[a]=s.to(u._rgba)}u[a][e.idx]=i(n[t],e,!0)}),u[a]&&0>t.inArray(null,u[a].slice(0,3))&&(u[a][3]=1,s.from&&(u._rgba=s.from(u[a])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),a=c[n],o=0===this.alpha()?l("transparent"):this,r=o[a.cache]||a.to(o._rgba),h=r.slice();return s=s[a.cache],f(a.props,function(t,n){var a=n.idx,o=r[a],l=s[a],c=u[n.type]||{};null!==l&&(null===o?h[a]=l:(c.mod&&(l-o>c.mod/2?o+=c.mod:o-l>c.mod/2&&(o-=c.mod)),h[a]=i((l-o)*e+o,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,a=t[2]/255,o=t[3],r=Math.max(s,n,a),h=Math.min(s,n,a),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-a)/l+360:n===r?60*(a-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==o?1:o]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],a=t[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,e+1/3)),Math.round(255*n(r,o,e)),Math.round(255*n(r,o,e-1/3)),a]},f(c,function(s,n){var a=n.props,o=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[o]&&(this[o]=h(this._rgba)),s===e)return this[o].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[o].slice();return f(a,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[o]=d,n):l(d)},f(a,function(e,i){l.fn[e]||(l.fn[e]=function(n){var a,o=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===o?c:("function"===o&&(n=n.call(this,c),o=t.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=c+parseFloat(a[2])*("+"===a[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var a,o,r="";if("transparent"!==n&&("string"!==t.type(n)||(a=s(n)))){if(n=l(a||n),!d.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&o&&o.style;)try{r=t.css(o,"backgroundColor"),o=o.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(o),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},a=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function s(e,i){var s,n,o={};for(s in i)n=i[s],e[s]!==n&&(a[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(o[s]=n));return o}var n=["add","remove","toggle"],a={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(jQuery.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(e,a,o,r){var h=t.speed(a,o,r);return this.queue(function(){var a,o=t(this),r=o.attr("class")||"",l=h.children?o.find("*").addBack():o;l=l.map(function(){var e=t(this);return{el:e,start:i(this)}}),a=function(){t.each(n,function(t,i){e[i]&&o[i+"Class"](e[i])})},a(),l=l.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),o.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){a(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(o[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,a){return s?t.effects.animateClass.call(this,{add:i},s,n,a):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,a){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,a):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(i){return function(s,n,a,o,r){return"boolean"==typeof n||n===e?a?t.effects.animateClass.call(this,n?{add:s}:{remove:s},a,o,r):i.apply(this,arguments):t.effects.animateClass.call(this,{toggle:s},n,a,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,a){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,a)}})}(),function(){function s(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function n(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}t.extend(t.effects,{version:"1.10.3",save:function(t,e){for(var s=0;e.length>s;s++)null!==e[s]&&t.data(i+e[s],t[0].style[e[s]])},restore:function(t,s){var n,a;for(a=0;s.length>a;a++)null!==s[a]&&(n=t.data(i+s[a]),n===e&&(n=""),t.css(s[a],n))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return e.wrap(s),(e[0]===a||t.contains(e[0],a))&&t(a).focus(),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).focus()),e},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var a=e.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),t.fn.extend({effect:function(){function e(e){function s(){t.isFunction(a)&&a.call(n[0]),t.isFunction(e)&&e()}var n=t(this),a=i.complete,r=i.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),s()):o.call(n[0],i,s)}var i=s.apply(this,arguments),n=i.mode,a=i.queue,o=t.effects.effect[i.effect];return t.fx.off||!o?n?this[n](i.duration,i.complete):this.each(function(){i.complete&&i.complete.call(this)}):a===!1?this.each(e):this.queue(a||"fx",e)},show:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(n(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s}})}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}()})(jQuery);(function(t){var e=/up|down|vertical/,i=/up|left|vertical|horizontal/;t.effects.effect.blind=function(s,n){var a,o,r,h=t(this),l=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(h,s.mode||"hide"),u=s.direction||"up",d=e.test(u),p=d?"height":"width",f=d?"top":"left",m=i.test(u),g={},v="show"===c;h.parent().is(".ui-effects-wrapper")?t.effects.save(h.parent(),l):t.effects.save(h,l),h.show(),a=t.effects.createWrapper(h).css({overflow:"hidden"}),o=a[p](),r=parseFloat(a.css(f))||0,g[p]=v?o:0,m||(h.css(d?"bottom":"right",0).css(d?"top":"left","auto").css({position:"absolute"}),g[f]=v?r:o+r),v&&(a.css(p,0),m||a.css(f,r+o)),a.animate(g,{duration:s.duration,easing:s.easing,queue:!1,complete:function(){"hide"===c&&h.hide(),t.effects.restore(h,l),t.effects.removeWrapper(h),n()}})}})(jQuery);(function(t){t.effects.effect.bounce=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],h=t.effects.setMode(o,e.mode||"effect"),l="hide"===h,c="show"===h,u=e.direction||"up",d=e.distance,p=e.times||5,f=2*p+(c||l?1:0),m=e.duration/f,g=e.easing,v="up"===u||"down"===u?"top":"left",_="up"===u||"left"===u,b=o.queue(),y=b.length;for((c||l)&&r.push("opacity"),t.effects.save(o,r),o.show(),t.effects.createWrapper(o),d||(d=o["top"===v?"outerHeight":"outerWidth"]()/3),c&&(a={opacity:1},a[v]=0,o.css("opacity",0).css(v,_?2*-d:2*d).animate(a,m,g)),l&&(d/=Math.pow(2,p-1)),a={},a[v]=0,s=0;p>s;s++)n={},n[v]=(_?"-=":"+=")+d,o.animate(n,m,g).animate(a,m,g),d=l?2*d:d/2;l&&(n={opacity:0},n[v]=(_?"-=":"+=")+d,o.animate(n,m,g)),o.queue(function(){l&&o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}),y>1&&b.splice.apply(b,[1,0].concat(b.splice(y,f+1))),o.dequeue()}})(jQuery);(function(t){t.effects.effect.clip=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],h=t.effects.setMode(o,e.mode||"hide"),l="show"===h,c=e.direction||"vertical",u="vertical"===c,d=u?"height":"width",p=u?"top":"left",f={};t.effects.save(o,r),o.show(),s=t.effects.createWrapper(o).css({overflow:"hidden"}),n="IMG"===o[0].tagName?s:o,a=n[d](),l&&(n.css(d,0),n.css(p,a/2)),f[d]=l?a:0,f[p]=l?0:a/2,n.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){l||o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.drop=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","opacity","height","width"],o=t.effects.setMode(n,e.mode||"hide"),r="show"===o,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h?"pos":"neg",u={opacity:r?1:0};t.effects.save(n,a),n.show(),t.effects.createWrapper(n),s=e.distance||n["top"===l?"outerHeight":"outerWidth"](!0)/2,r&&n.css("opacity",0).css(l,"pos"===c?-s:s),u[l]=(r?"pos"===c?"+=":"-=":"pos"===c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.explode=function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),m||p.hide(),i()}var a,o,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),m="show"===f,g=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/d),_=Math.ceil(p.outerHeight()/u),b=[];for(a=0;u>a;a++)for(h=g.top+a*_,c=a-(u-1)/2,o=0;d>o;o++)r=g.left+o*v,l=o-(d-1)/2,p.clone().appendTo("body").wrap("
    ").css({position:"absolute",visibility:"visible",left:-o*v,top:-a*_}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:_,left:r+(m?l*v:0),top:h+(m?c*_:0),opacity:m?0:1}).animate({left:r+(m?0:l*v),top:h+(m?0:c*_),opacity:m?1:0},e.duration||500,e.easing,s)}})(jQuery);(function(t){t.effects.effect.fade=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"toggle");s.animate({opacity:n},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}})(jQuery);(function(t){t.effects.effect.fold=function(e,i){var s,n,a=t(this),o=["position","top","bottom","left","right","height","width"],r=t.effects.setMode(a,e.mode||"hide"),h="show"===r,l="hide"===r,c=e.size||15,u=/([0-9]+)%/.exec(c),d=!!e.horizFirst,p=h!==d,f=p?["width","height"]:["height","width"],m=e.duration/2,g={},v={};t.effects.save(a,o),a.show(),s=t.effects.createWrapper(a).css({overflow:"hidden"}),n=p?[s.width(),s.height()]:[s.height(),s.width()],u&&(c=parseInt(u[1],10)/100*n[l?0:1]),h&&s.css(d?{height:0,width:c}:{height:c,width:0}),g[f[0]]=h?n[0]:c,v[f[1]]=h?n[1]:0,s.animate(g,m,e.easing).animate(v,m,e.easing,function(){l&&a.hide(),t.effects.restore(a,o),t.effects.removeWrapper(a),i()})}})(jQuery);(function(t){t.effects.effect.highlight=function(e,i){var s=t(this),n=["backgroundImage","backgroundColor","opacity"],a=t.effects.setMode(s,e.mode||"show"),o={backgroundColor:s.css("backgroundColor")};"hide"===a&&(o.opacity=0),t.effects.save(s,n),s.show().css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(o,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===a&&s.hide(),t.effects.restore(s,n),i()}})}})(jQuery);(function(t){t.effects.effect.pulsate=function(e,i){var s,n=t(this),a=t.effects.setMode(n,e.mode||"show"),o="show"===a,r="hide"===a,h=o||"hide"===a,l=2*(e.times||5)+(h?1:0),c=e.duration/l,u=0,d=n.queue(),p=d.length;for((o||!n.is(":visible"))&&(n.css("opacity",0).show(),u=1),s=1;l>s;s++)n.animate({opacity:u},c,e.easing),u=1-u;n.animate({opacity:u},c,e.easing),n.queue(function(){r&&n.hide(),i()}),p>1&&d.splice.apply(d,[1,0].concat(d.splice(p,l+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.puff=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"hide"),a="hide"===n,o=parseInt(e.percent,10)||150,r=o/100,h={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()};t.extend(e,{effect:"scale",queue:!1,fade:!0,mode:n,complete:i,percent:a?o:100,from:a?h:{height:h.height*r,width:h.width*r,outerHeight:h.outerHeight*r,outerWidth:h.outerWidth*r}}),s.effect(e)},t.effects.effect.scale=function(e,i){var s=t(this),n=t.extend(!0,{},e),a=t.effects.setMode(s,e.mode||"effect"),o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"hide"===a?0:100),r=e.direction||"both",h=e.origin,l={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()},c={y:"horizontal"!==r?o/100:1,x:"vertical"!==r?o/100:1};n.effect="size",n.queue=!1,n.complete=i,"effect"!==a&&(n.origin=h||["middle","center"],n.restore=!0),n.from=e.from||("show"===a?{height:0,width:0,outerHeight:0,outerWidth:0}:l),n.to={height:l.height*c.y,width:l.width*c.x,outerHeight:l.outerHeight*c.y,outerWidth:l.outerWidth*c.x},n.fade&&("show"===a&&(n.from.opacity=0,n.to.opacity=1),"hide"===a&&(n.from.opacity=1,n.to.opacity=0)),s.effect(n)},t.effects.effect.size=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","width","height","overflow","opacity"],h=["position","top","bottom","left","right","overflow","opacity"],l=["width","height","overflow"],c=["fontSize"],u=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],d=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=t.effects.setMode(o,e.mode||"effect"),f=e.restore||"effect"!==p,m=e.scale||"both",g=e.origin||["middle","center"],v=o.css("position"),_=f?r:h,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&o.show(),s={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},"toggle"===e.mode&&"show"===p?(o.from=e.to||b,o.to=e.from||s):(o.from=e.from||("show"===p?b:s),o.to=e.to||("hide"===p?b:s)),a={from:{y:o.from.height/s.height,x:o.from.width/s.width},to:{y:o.to.height/s.height,x:o.to.width/s.width}},("box"===m||"both"===m)&&(a.from.y!==a.to.y&&(_=_.concat(u),o.from=t.effects.setTransition(o,u,a.from.y,o.from),o.to=t.effects.setTransition(o,u,a.to.y,o.to)),a.from.x!==a.to.x&&(_=_.concat(d),o.from=t.effects.setTransition(o,d,a.from.x,o.from),o.to=t.effects.setTransition(o,d,a.to.x,o.to))),("content"===m||"both"===m)&&a.from.y!==a.to.y&&(_=_.concat(c).concat(l),o.from=t.effects.setTransition(o,c,a.from.y,o.from),o.to=t.effects.setTransition(o,c,a.to.y,o.to)),t.effects.save(o,_),o.show(),t.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),g&&(n=t.effects.getBaseline(g,s),o.from.top=(s.outerHeight-o.outerHeight())*n.y,o.from.left=(s.outerWidth-o.outerWidth())*n.x,o.to.top=(s.outerHeight-o.to.outerHeight)*n.y,o.to.left=(s.outerWidth-o.to.outerWidth)*n.x),o.css(o.from),("content"===m||"both"===m)&&(u=u.concat(["marginTop","marginBottom"]).concat(c),d=d.concat(["marginLeft","marginRight"]),l=r.concat(u).concat(d),o.find("*[width]").each(function(){var i=t(this),s={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()};f&&t.effects.save(i,l),i.from={height:s.height*a.from.y,width:s.width*a.from.x,outerHeight:s.outerHeight*a.from.y,outerWidth:s.outerWidth*a.from.x},i.to={height:s.height*a.to.y,width:s.width*a.to.x,outerHeight:s.height*a.to.y,outerWidth:s.width*a.to.x},a.from.y!==a.to.y&&(i.from=t.effects.setTransition(i,u,a.from.y,i.from),i.to=t.effects.setTransition(i,u,a.to.y,i.to)),a.from.x!==a.to.x&&(i.from=t.effects.setTransition(i,d,a.from.x,i.from),i.to=t.effects.setTransition(i,d,a.to.x,i.to)),i.css(i.from),i.animate(i.to,e.duration,e.easing,function(){f&&t.effects.restore(i,l)})})),o.animate(o.to,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){0===o.to.opacity&&o.css("opacity",o.from.opacity),"hide"===p&&o.hide(),t.effects.restore(o,_),f||("static"===v?o.css({position:"relative",top:o.to.top,left:o.to.left}):t.each(["top","left"],function(t,e){o.css(e,function(e,i){var s=parseInt(i,10),n=t?o.to.left:o.to.top;return"auto"===i?n+"px":s+n+"px"})})),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.shake=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","height","width"],o=t.effects.setMode(n,e.mode||"effect"),r=e.direction||"left",h=e.distance||20,l=e.times||3,c=2*l+1,u=Math.round(e.duration/c),d="up"===r||"down"===r?"top":"left",p="up"===r||"left"===r,f={},m={},g={},v=n.queue(),_=v.length;for(t.effects.save(n,a),n.show(),t.effects.createWrapper(n),f[d]=(p?"-=":"+=")+h,m[d]=(p?"+=":"-=")+2*h,g[d]=(p?"-=":"+=")+2*h,n.animate(f,u,e.easing),s=1;l>s;s++)n.animate(m,u,e.easing).animate(g,u,e.easing);n.animate(m,u,e.easing).animate(f,u/2,e.easing).queue(function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}),_>1&&v.splice.apply(v,[1,0].concat(v.splice(_,c+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.slide=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","width","height"],o=t.effects.setMode(n,e.mode||"show"),r="show"===o,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u={};t.effects.save(n,a),n.show(),s=e.distance||n["top"===l?"outerHeight":"outerWidth"](!0),t.effects.createWrapper(n).css({overflow:"hidden"}),r&&n.css(l,c?isNaN(s)?"-"+s:-s:s),u[l]=(r?c?"+=":"-=":c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.transfer=function(e,i){var s=t(this),n=t(e.to),a="fixed"===n.css("position"),o=t("body"),r=a?o.scrollTop():0,h=a?o.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
    ").appendTo(document.body).addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:a?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),i()})}})(jQuery); \ No newline at end of file diff --git a/src/main/webapp/scripts/jquery.min.js b/src/main/webapp/scripts/jquery.min.js new file mode 100644 index 0000000..006e953 --- /dev/null +++ b/src/main/webapp/scripts/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.9.1 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery.min.map +*/(function(e,t){var n,r,i=typeof t,o=e.document,a=e.location,s=e.jQuery,u=e.$,l={},c=[],p="1.9.1",f=c.concat,d=c.push,h=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,b=function(e,t){return new b.fn.init(e,t,r)},x=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^[\],:{}\s]*$/,E=/(?:^|:|,)(?:\s*\[)+/g,S=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,A=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,j=/^-ms-/,D=/-([\da-z])/gi,L=function(e,t){return t.toUpperCase()},H=function(e){(o.addEventListener||"load"===e.type||"complete"===o.readyState)&&(q(),b.ready())},q=function(){o.addEventListener?(o.removeEventListener("DOMContentLoaded",H,!1),e.removeEventListener("load",H,!1)):(o.detachEvent("onreadystatechange",H),e.detachEvent("onload",H))};b.fn=b.prototype={jquery:p,constructor:b,init:function(e,n,r){var i,a;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof b?n[0]:n,b.merge(this,b.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:o,!0)),C.test(i[1])&&b.isPlainObject(n))for(i in n)b.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(a=o.getElementById(i[2]),a&&a.parentNode){if(a.id!==i[2])return r.find(e);this.length=1,this[0]=a}return this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):b.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),b.makeArray(e,this))},selector:"",length:0,size:function(){return this.length},toArray:function(){return h.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=b.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return b.each(this,e,t)},ready:function(e){return b.ready.promise().done(e),this},slice:function(){return this.pushStack(h.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(b.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:d,sort:[].sort,splice:[].splice},b.fn.init.prototype=b.fn,b.extend=b.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},u=1,l=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},u=2),"object"==typeof s||b.isFunction(s)||(s={}),l===u&&(s=this,--u);l>u;u++)if(null!=(o=arguments[u]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(b.isPlainObject(r)||(n=b.isArray(r)))?(n?(n=!1,a=e&&b.isArray(e)?e:[]):a=e&&b.isPlainObject(e)?e:{},s[i]=b.extend(c,a,r)):r!==t&&(s[i]=r));return s},b.extend({noConflict:function(t){return e.$===b&&(e.$=u),t&&e.jQuery===b&&(e.jQuery=s),b},isReady:!1,readyWait:1,holdReady:function(e){e?b.readyWait++:b.ready(!0)},ready:function(e){if(e===!0?!--b.readyWait:!b.isReady){if(!o.body)return setTimeout(b.ready);b.isReady=!0,e!==!0&&--b.readyWait>0||(n.resolveWith(o,[b]),b.fn.trigger&&b(o).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===b.type(e)},isArray:Array.isArray||function(e){return"array"===b.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if(!e||"object"!==b.type(e)||e.nodeType||b.isWindow(e))return!1;try{if(e.constructor&&!y.call(e,"constructor")&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||y.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=b.buildFragment([e],t,i),i&&b(i).remove(),b.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=b.trim(n),n&&k.test(n.replace(S,"@").replace(A,"]").replace(E,"")))?Function("return "+n)():(b.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||b.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&b.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(j,"ms-").replace(D,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:v&&!v.call("\ufeff\u00a0")?function(e){return null==e?"":v.call(e)}:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?b.merge(n,"string"==typeof e?[e]:e):d.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(g)return g.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return f.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),b.isFunction(e)?(r=h.call(arguments,2),i=function(){return e.apply(n||this,r.concat(h.call(arguments)))},i.guid=e.guid=e.guid||b.guid++,i):t},access:function(e,n,r,i,o,a,s){var u=0,l=e.length,c=null==r;if("object"===b.type(r)){o=!0;for(u in r)b.access(e,n,u,r[u],!0,a,s)}else if(i!==t&&(o=!0,b.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(b(e),n)})),n))for(;l>u;u++)n(e[u],r,s?i:i.call(e[u],u,n(e[u],r)));return o?e:c?n.call(e):l?n(e[0],r):a},now:function(){return(new Date).getTime()}}),b.ready.promise=function(t){if(!n)if(n=b.Deferred(),"complete"===o.readyState)setTimeout(b.ready);else if(o.addEventListener)o.addEventListener("DOMContentLoaded",H,!1),e.addEventListener("load",H,!1);else{o.attachEvent("onreadystatechange",H),e.attachEvent("onload",H);var r=!1;try{r=null==e.frameElement&&o.documentElement}catch(i){}r&&r.doScroll&&function a(){if(!b.isReady){try{r.doScroll("left")}catch(e){return setTimeout(a,50)}q(),b.ready()}}()}return n.promise(t)},b.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=b.type(e);return b.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=b(o);var _={};function F(e){var t=_[e]={};return b.each(e.match(w)||[],function(e,n){t[n]=!0}),t}b.Callbacks=function(e){e="string"==typeof e?_[e]||F(e):b.extend({},e);var n,r,i,o,a,s,u=[],l=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=u.length,n=!0;u&&o>a;a++)if(u[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,u&&(l?l.length&&c(l.shift()):r?u=[]:p.disable())},p={add:function(){if(u){var t=u.length;(function i(t){b.each(t,function(t,n){var r=b.type(n);"function"===r?e.unique&&p.has(n)||u.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=u.length:r&&(s=t,c(r))}return this},remove:function(){return u&&b.each(arguments,function(e,t){var r;while((r=b.inArray(t,u,r))>-1)u.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?b.inArray(e,u)>-1:!(!u||!u.length)},empty:function(){return u=[],this},disable:function(){return u=l=r=t,this},disabled:function(){return!u},lock:function(){return l=t,r||p.disable(),this},locked:function(){return!l},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!u||i&&!l||(n?l.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},b.extend({Deferred:function(e){var t=[["resolve","done",b.Callbacks("once memory"),"resolved"],["reject","fail",b.Callbacks("once memory"),"rejected"],["notify","progress",b.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return b.Deferred(function(n){b.each(t,function(t,o){var a=o[0],s=b.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&b.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?b.extend(e,r):r}},i={};return r.pipe=r.then,b.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=h.call(arguments),r=n.length,i=1!==r||e&&b.isFunction(e.promise)?r:0,o=1===i?e:b.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?h.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,u,l;if(r>1)for(s=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&b.isFunction(n[t].promise)?n[t].promise().done(a(t,l,n)).fail(o.reject).progress(a(t,u,s)):--i;return i||o.resolveWith(l,n),o.promise()}}),b.support=function(){var t,n,r,a,s,u,l,c,p,f,d=o.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
    a",n=d.getElementsByTagName("*"),r=d.getElementsByTagName("a")[0],!n||!r||!n.length)return{};s=o.createElement("select"),l=s.appendChild(o.createElement("option")),a=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={getSetAttribute:"t"!==d.className,leadingWhitespace:3===d.firstChild.nodeType,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:"/a"===r.getAttribute("href"),opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:!!a.value,optSelected:l.selected,enctype:!!o.createElement("form").enctype,html5Clone:"<:nav>"!==o.createElement("nav").cloneNode(!0).outerHTML,boxModel:"CSS1Compat"===o.compatMode,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},a.checked=!0,t.noCloneChecked=a.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!l.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}a=o.createElement("input"),a.setAttribute("value",""),t.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),t.radioValue="t"===a.value,a.setAttribute("checked","t"),a.setAttribute("name","t"),u=o.createDocumentFragment(),u.appendChild(a),t.appendChecked=a.checked,t.checkClone=u.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;return d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip,b(function(){var n,r,a,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",u=o.getElementsByTagName("body")[0];u&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",u.appendChild(n).appendChild(d),d.innerHTML="
    t
    ",a=d.getElementsByTagName("td"),a[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===a[0].offsetHeight,a[0].style.display="",a[1].style.display="none",t.reliableHiddenOffsets=p&&0===a[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=4===d.offsetWidth,t.doesNotIncludeMarginInBodyOffset=1!==u.offsetTop,e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(o.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(u.style.zoom=1)),u.removeChild(n),n=d=a=r=null)}),n=s=u=l=r=a=null,t}();var O=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,B=/([A-Z])/g;function P(e,n,r,i){if(b.acceptData(e)){var o,a,s=b.expando,u="string"==typeof n,l=e.nodeType,p=l?b.cache:e,f=l?e[s]:e[s]&&s;if(f&&p[f]&&(i||p[f].data)||!u||r!==t)return f||(l?e[s]=f=c.pop()||b.guid++:f=s),p[f]||(p[f]={},l||(p[f].toJSON=b.noop)),("object"==typeof n||"function"==typeof n)&&(i?p[f]=b.extend(p[f],n):p[f].data=b.extend(p[f].data,n)),o=p[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[b.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[b.camelCase(n)])):a=o,a}}function R(e,t,n){if(b.acceptData(e)){var r,i,o,a=e.nodeType,s=a?b.cache:e,u=a?e[b.expando]:b.expando;if(s[u]){if(t&&(o=n?s[u]:s[u].data)){b.isArray(t)?t=t.concat(b.map(t,b.camelCase)):t in o?t=[t]:(t=b.camelCase(t),t=t in o?[t]:t.split(" "));for(r=0,i=t.length;i>r;r++)delete o[t[r]];if(!(n?$:b.isEmptyObject)(o))return}(n||(delete s[u].data,$(s[u])))&&(a?b.cleanData([e],!0):b.support.deleteExpando||s!=s.window?delete s[u]:s[u]=null)}}}b.extend({cache:{},expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?b.cache[e[b.expando]]:e[b.expando],!!e&&!$(e)},data:function(e,t,n){return P(e,t,n)},removeData:function(e,t){return R(e,t)},_data:function(e,t,n){return P(e,t,n,!0)},_removeData:function(e,t){return R(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&b.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),b.fn.extend({data:function(e,n){var r,i,o=this[0],a=0,s=null;if(e===t){if(this.length&&(s=b.data(o),1===o.nodeType&&!b._data(o,"parsedAttrs"))){for(r=o.attributes;r.length>a;a++)i=r[a].name,i.indexOf("data-")||(i=b.camelCase(i.slice(5)),W(o,i,s[i]));b._data(o,"parsedAttrs",!0)}return s}return"object"==typeof e?this.each(function(){b.data(this,e)}):b.access(this,function(n){return n===t?o?W(o,e,b.data(o,e)):null:(this.each(function(){b.data(this,e,n)}),t)},null,n,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){b.removeData(this,e)})}});function W(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(B,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:O.test(r)?b.parseJSON(r):r}catch(o){}b.data(e,n,r)}else r=t}return r}function $(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}b.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=b._data(e,n),r&&(!i||b.isArray(r)?i=b._data(e,n,b.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=b.queue(e,t),r=n.length,i=n.shift(),o=b._queueHooks(e,t),a=function(){b.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return b._data(e,n)||b._data(e,n,{empty:b.Callbacks("once memory").add(function(){b._removeData(e,t+"queue"),b._removeData(e,n)})})}}),b.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?b.queue(this[0],e):n===t?this:this.each(function(){var t=b.queue(this,e,n);b._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&b.dequeue(this,e)})},dequeue:function(e){return this.each(function(){b.dequeue(this,e)})},delay:function(e,t){return e=b.fx?b.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=b.Deferred(),a=this,s=this.length,u=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=b._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(u));return u(),o.promise(n)}});var I,z,X=/[\t\r\n]/g,U=/\r/g,V=/^(?:input|select|textarea|button|object)$/i,Y=/^(?:a|area)$/i,J=/^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i,G=/^(?:checked|selected)$/i,Q=b.support.getSetAttribute,K=b.support.input;b.fn.extend({attr:function(e,t){return b.access(this,b.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){b.removeAttr(this,e)})},prop:function(e,t){return b.access(this,b.prop,e,t,arguments.length>1)},removeProp:function(e){return e=b.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,u="string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=b.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,u=0===arguments.length||"string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?b.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return b.isFunction(e)?this.each(function(n){b(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=b(this),u=t,l=e.match(w)||[];while(o=l[a++])u=r?u:!s.hasClass(o),s[u?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&b._data(this,"__className__",this.className),this.className=this.className||e===!1?"":b._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(X," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=b.isFunction(e),this.each(function(n){var o,a=b(this);1===this.nodeType&&(o=i?e.call(this,n,a.val()):e,null==o?o="":"number"==typeof o?o+="":b.isArray(o)&&(o=b.map(o,function(e){return null==e?"":e+""})),r=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=b.valHooks[o.type]||b.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(U,""):null==n?"":n)}}}),b.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,u=0>i?s:o?i:0;for(;s>u;u++)if(n=r[u],!(!n.selected&&u!==i||(b.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&b.nodeName(n.parentNode,"optgroup"))){if(t=b(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n=b.makeArray(t);return b(e).find("option").each(function(){this.selected=b.inArray(b(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attr:function(e,n,r){var o,a,s,u=e.nodeType;if(e&&3!==u&&8!==u&&2!==u)return typeof e.getAttribute===i?b.prop(e,n,r):(a=1!==u||!b.isXMLDoc(e),a&&(n=n.toLowerCase(),o=b.attrHooks[n]||(J.test(n)?z:I)),r===t?o&&a&&"get"in o&&null!==(s=o.get(e,n))?s:(typeof e.getAttribute!==i&&(s=e.getAttribute(n)),null==s?t:s):null!==r?o&&a&&"set"in o&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r):(b.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=b.propFix[n]||n,J.test(n)?!Q&&G.test(n)?e[b.camelCase("default-"+n)]=e[r]=!1:e[r]=!1:b.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!b.support.radioValue&&"radio"===t&&b.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!b.isXMLDoc(e),a&&(n=b.propFix[n]||n,o=b.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):V.test(e.nodeName)||Y.test(e.nodeName)&&e.href?0:t}}}}),z={get:function(e,n){var r=b.prop(e,n),i="boolean"==typeof r&&e.getAttribute(n),o="boolean"==typeof r?K&&Q?null!=i:G.test(n)?e[b.camelCase("default-"+n)]:!!i:e.getAttributeNode(n);return o&&o.value!==!1?n.toLowerCase():t},set:function(e,t,n){return t===!1?b.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&b.propFix[n]||n,n):e[b.camelCase("default-"+n)]=e[n]=!0,n}},K&&Q||(b.attrHooks.value={get:function(e,n){var r=e.getAttributeNode(n);return b.nodeName(e,"input")?e.defaultValue:r&&r.specified?r.value:t},set:function(e,n,r){return b.nodeName(e,"input")?(e.defaultValue=n,t):I&&I.set(e,n,r)}}),Q||(I=b.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&("id"===n||"name"===n||"coords"===n?""!==r.value:r.specified)?r.value:t},set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},b.attrHooks.contenteditable={get:I.get,set:function(e,t,n){I.set(e,""===t?!1:t,n)}},b.each(["width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}})})),b.support.hrefNormalized||(b.each(["href","src","width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return null==r?t:r}})}),b.each(["href","src"],function(e,t){b.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}})),b.support.style||(b.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),b.support.optSelected||(b.propHooks.selected=b.extend(b.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),b.support.enctype||(b.propFix.enctype="encoding"),b.support.checkOn||b.each(["radio","checkbox"],function(){b.valHooks[this]={get:function(e){return null===e.getAttribute("value")?"on":e.value}}}),b.each(["radio","checkbox"],function(){b.valHooks[this]=b.extend(b.valHooks[this],{set:function(e,n){return b.isArray(n)?e.checked=b.inArray(b(e).val(),n)>=0:t}})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}b.event={global:{},add:function(e,n,r,o,a){var s,u,l,c,p,f,d,h,g,m,y,v=b._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=b.guid++),(u=v.events)||(u=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof b===i||e&&b.event.triggered===e.type?t:b.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(w)||[""],l=n.length;while(l--)s=rt.exec(n[l])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),p=b.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=b.event.special[g]||{},d=b.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&b.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=u[g])||(h=u[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),b.event.global[g]=!0;e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,p,f,d,h,g,m=b.hasData(e)&&b._data(e);if(m&&(c=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(s=rt.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=b.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));u&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||b.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(c)&&(delete m.handle,b._removeData(e,"events"))}},trigger:function(n,r,i,a){var s,u,l,c,p,f,d,h=[i||o],g=y.call(n,"type")?n.type:n,m=y.call(n,"namespace")?n.namespace.split("."):[];if(l=f=i=i||o,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+b.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),u=0>g.indexOf(":")&&"on"+g,n=n[b.expando]?n:new b.Event(g,"object"==typeof n&&n),n.isTrigger=!0,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:b.makeArray(r,[n]),p=b.event.special[g]||{},a||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!a&&!p.noBubble&&!b.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(l=l.parentNode);l;l=l.parentNode)h.push(l),f=l;f===(i.ownerDocument||o)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((l=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(b._data(l,"events")||{})[n.type]&&b._data(l,"handle"),s&&s.apply(l,r),s=u&&l[u],s&&b.acceptData(l)&&s.apply&&s.apply(l,r)===!1&&n.preventDefault();if(n.type=g,!(a||n.isDefaultPrevented()||p._default&&p._default.apply(i.ownerDocument,r)!==!1||"click"===g&&b.nodeName(i,"a")||!b.acceptData(i)||!u||!i[g]||b.isWindow(i))){f=i[u],f&&(i[u]=null),b.event.triggered=g;try{i[g]()}catch(v){}b.event.triggered=t,f&&(i[u]=f)}return n.result}},dispatch:function(e){e=b.event.fix(e);var n,r,i,o,a,s=[],u=h.call(arguments),l=(b._data(this,"events")||{})[e.type]||[],c=b.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=b.event.handlers.call(this,e,l),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((b.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,u),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],u=n.delegateCount,l=e.target;if(u&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(o=[],a=0;u>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?b(r,this).index(l)>=0:b.find(r,this,null,[l]).length),o[r]&&o.push(i);o.length&&s.push({elem:l,handlers:o})}return n.length>u&&s.push({elem:this,handlers:n.slice(u)}),s},fix:function(e){if(e[b.expando])return e;var t,n,r,i=e.type,a=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new b.Event(a),t=r.length;while(t--)n=r[t],e[n]=a[n];return e.target||(e.target=a.srcElement||o),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,a):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,a,s=n.button,u=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||o,a=i.documentElement,r=i.body,e.pageX=n.clientX+(a&&a.scrollLeft||r&&r.scrollLeft||0)-(a&&a.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(a&&a.scrollTop||r&&r.scrollTop||0)-(a&&a.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&u&&(e.relatedTarget=u===e.target?n.toElement:u),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},click:{trigger:function(){return b.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t}},focus:{trigger:function(){if(this!==o.activeElement&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===o.activeElement&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=b.extend(new b.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?b.event.trigger(i,null,t):b.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},b.removeEvent=o.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},b.Event=function(e,n){return this instanceof b.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&b.extend(this,n),this.timeStamp=e&&e.timeStamp||b.now(),this[b.expando]=!0,t):new b.Event(e,n)},b.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},b.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){b.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj; +return(!i||i!==r&&!b.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),b.support.submitBubbles||(b.event.special.submit={setup:function(){return b.nodeName(this,"form")?!1:(b.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=b.nodeName(n,"input")||b.nodeName(n,"button")?n.form:t;r&&!b._data(r,"submitBubbles")&&(b.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),b._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&b.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return b.nodeName(this,"form")?!1:(b.event.remove(this,"._submit"),t)}}),b.support.changeBubbles||(b.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(b.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),b.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),b.event.simulate("change",this,e,!0)})),!1):(b.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!b._data(t,"changeBubbles")&&(b.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||b.event.simulate("change",this.parentNode,e,!0)}),b._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return b.event.remove(this,"._change"),!Z.test(this.nodeName)}}),b.support.focusinBubbles||b.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){b.event.simulate(t,e.target,b.event.fix(e),!0)};b.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),b.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return b().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=b.guid++)),this.each(function(){b.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,b(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){b.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){b.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?b.event.trigger(e,n,r,!0):t}}),function(e,t){var n,r,i,o,a,s,u,l,c,p,f,d,h,g,m,y,v,x="sizzle"+-new Date,w=e.document,T={},N=0,C=0,k=it(),E=it(),S=it(),A=typeof t,j=1<<31,D=[],L=D.pop,H=D.push,q=D.slice,M=D.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},_="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=F.replace("w","w#"),B="([*^$|!~]?=)",P="\\["+_+"*("+F+")"+_+"*(?:"+B+_+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+O+")|)|)"+_+"*\\]",R=":("+F+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+P.replace(3,8)+")*)|.*)\\)|)",W=RegExp("^"+_+"+|((?:^|[^\\\\])(?:\\\\.)*)"+_+"+$","g"),$=RegExp("^"+_+"*,"+_+"*"),I=RegExp("^"+_+"*([\\x20\\t\\r\\n\\f>+~])"+_+"*"),z=RegExp(R),X=RegExp("^"+O+"$"),U={ID:RegExp("^#("+F+")"),CLASS:RegExp("^\\.("+F+")"),NAME:RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:RegExp("^("+F.replace("w","w*")+")"),ATTR:RegExp("^"+P),PSEUDO:RegExp("^"+R),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+_+"*(even|odd|(([+-]|)(\\d*)n|)"+_+"*(?:([+-]|)"+_+"*(\\d+)|))"+_+"*\\)|)","i"),needsContext:RegExp("^"+_+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+_+"*((?:-\\d)?\\d*)"+_+"*\\)|)(?=[^-]|$)","i")},V=/[\x20\t\r\n\f]*[+~]/,Y=/^[^{]+\{\s*\[native code/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/^(?:input|select|textarea|button)$/i,Q=/^h\d$/i,K=/'|\\/g,Z=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,et=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,tt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{q.call(w.documentElement.childNodes,0)[0].nodeType}catch(nt){q=function(e){var t,n=[];while(t=this[e++])n.push(t);return n}}function rt(e){return Y.test(e+"")}function it(){var e,t=[];return e=function(n,r){return t.push(n+=" ")>i.cacheLength&&delete e[t.shift()],e[n]=r}}function ot(e){return e[x]=!0,e}function at(e){var t=p.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}}function st(e,t,n,r){var i,o,a,s,u,l,f,g,m,v;if((t?t.ownerDocument||t:w)!==p&&c(t),t=t||p,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(!d&&!r){if(i=J.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&y(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return H.apply(n,q.call(t.getElementsByTagName(e),0)),n;if((a=i[3])&&T.getByClassName&&t.getElementsByClassName)return H.apply(n,q.call(t.getElementsByClassName(a),0)),n}if(T.qsa&&!h.test(e)){if(f=!0,g=x,m=t,v=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){l=ft(e),(f=t.getAttribute("id"))?g=f.replace(K,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=l.length;while(u--)l[u]=g+dt(l[u]);m=V.test(e)&&t.parentNode||t,v=l.join(",")}if(v)try{return H.apply(n,q.call(m.querySelectorAll(v),0)),n}catch(b){}finally{f||t.removeAttribute("id")}}}return wt(e.replace(W,"$1"),t,n,r)}a=st.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},c=st.setDocument=function(e){var n=e?e.ownerDocument||e:w;return n!==p&&9===n.nodeType&&n.documentElement?(p=n,f=n.documentElement,d=a(n),T.tagNameNoComments=at(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),T.attributes=at(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return"boolean"!==t&&"string"!==t}),T.getByClassName=at(function(e){return e.innerHTML="",e.getElementsByClassName&&e.getElementsByClassName("e").length?(e.lastChild.className="e",2===e.getElementsByClassName("e").length):!1}),T.getByName=at(function(e){e.id=x+0,e.innerHTML="
    ",f.insertBefore(e,f.firstChild);var t=n.getElementsByName&&n.getElementsByName(x).length===2+n.getElementsByName(x+0).length;return T.getIdNotName=!n.getElementById(x),f.removeChild(e),t}),i.attrHandle=at(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==A&&"#"===e.firstChild.getAttribute("href")})?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},T.getIdNotName?(i.find.ID=function(e,t){if(typeof t.getElementById!==A&&!d){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){return e.getAttribute("id")===t}}):(i.find.ID=function(e,n){if(typeof n.getElementById!==A&&!d){var r=n.getElementById(e);return r?r.id===e||typeof r.getAttributeNode!==A&&r.getAttributeNode("id").value===e?[r]:t:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){var n=typeof e.getAttributeNode!==A&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=T.tagNameNoComments?function(e,n){return typeof n.getElementsByTagName!==A?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.NAME=T.getByName&&function(e,n){return typeof n.getElementsByName!==A?n.getElementsByName(name):t},i.find.CLASS=T.getByClassName&&function(e,n){return typeof n.getElementsByClassName===A||d?t:n.getElementsByClassName(e)},g=[],h=[":focus"],(T.qsa=rt(n.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+_+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){e.innerHTML="",e.querySelectorAll("[i^='']").length&&h.push("[*^$]="+_+"*(?:\"\"|'')"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(T.matchesSelector=rt(m=f.matchesSelector||f.mozMatchesSelector||f.webkitMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){T.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",R)}),h=RegExp(h.join("|")),g=RegExp(g.join("|")),y=rt(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},v=f.compareDocumentPosition?function(e,t){var r;return e===t?(u=!0,0):(r=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t))?1&r||e.parentNode&&11===e.parentNode.nodeType?e===n||y(w,e)?-1:t===n||y(w,t)?1:0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return u=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:0;if(o===a)return ut(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?ut(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},u=!1,[0,0].sort(v),T.detectDuplicates=u,p):p},st.matches=function(e,t){return st(e,null,null,t)},st.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Z,"='$1']"),!(!T.matchesSelector||d||g&&g.test(t)||h.test(t)))try{var n=m.call(e,t);if(n||T.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return st(t,p,null,[e]).length>0},st.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},st.attr=function(e,t){var n;return(e.ownerDocument||e)!==p&&c(e),d||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):d||T.attributes?e.getAttribute(t):((n=e.getAttributeNode(t))||e.getAttribute(t))&&e[t]===!0?t:n&&n.specified?n.value:null},st.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},st.uniqueSort=function(e){var t,n=[],r=1,i=0;if(u=!T.detectDuplicates,e.sort(v),u){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e};function ut(e,t){var n=t&&e,r=n&&(~t.sourceIndex||j)-(~e.sourceIndex||j);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function lt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ct(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pt(e){return ot(function(t){return t=+t,ot(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}o=st.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=st.selectors={cacheLength:50,createPseudo:ot,match:U,find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(et,tt),e[3]=(e[4]||e[5]||"").replace(et,tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||st.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&st.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return U.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&z.test(n)&&(t=ft(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){return"*"===e?function(){return!0}:(e=e.replace(et,tt).toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[e+" "];return t||(t=RegExp("(^|"+_+")"+e+"("+_+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==A&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=st.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!u&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[x]||(m[x]={}),l=c[e]||[],d=l[0]===N&&l[1],f=l[0]===N&&l[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[N,d,f];break}}else if(v&&(l=(t[x]||(t[x]={}))[e])&&l[0]===N)f=l[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[x]||(p[x]={}))[e]=[N,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||st.error("unsupported pseudo: "+e);return r[x]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ot(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=M.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ot(function(e){var t=[],n=[],r=s(e.replace(W,"$1"));return r[x]?ot(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ot(function(e){return function(t){return st(e,t).length>0}}),contains:ot(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ot(function(e){return X.test(e||"")||st.error("unsupported lang: "+e),e=e.replace(et,tt).toLowerCase(),function(t){var n;do if(n=d?t.getAttribute("xml:lang")||t.getAttribute("lang"):t.lang)return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return Q.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:pt(function(){return[0]}),last:pt(function(e,t){return[t-1]}),eq:pt(function(e,t,n){return[0>n?n+t:n]}),even:pt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:pt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:pt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:pt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[n]=lt(n);for(n in{submit:!0,reset:!0})i.pseudos[n]=ct(n);function ft(e,t){var n,r,o,a,s,u,l,c=E[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=i.preFilter;while(s){(!n||(r=$.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),u.push(o=[])),n=!1,(r=I.exec(s))&&(n=r.shift(),o.push({value:n,type:r[0].replace(W," ")}),s=s.slice(n.length));for(a in i.filter)!(r=U[a].exec(s))||l[a]&&!(r=l[a](r))||(n=r.shift(),o.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?st.error(e):E(e,u).slice(0)}function dt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function ht(e,t,n){var i=t.dir,o=n&&"parentNode"===i,a=C++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,s){var u,l,c,p=N+" "+a;if(s){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[x]||(t[x]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,s)||r,l[1]===!0)return!0}}function gt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function mt(e,t,n,r,i){var o,a=[],s=0,u=e.length,l=null!=t;for(;u>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),l&&t.push(s));return a}function yt(e,t,n,r,i,o){return r&&!r[x]&&(r=yt(r)),i&&!i[x]&&(i=yt(i,o)),ot(function(o,a,s,u){var l,c,p,f=[],d=[],h=a.length,g=o||xt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:mt(g,f,e,s,u),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,u),r){l=mt(y,d),r(l,[],s,u),c=l.length;while(c--)(p=l[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?M.call(o,p):f[c])>-1&&(o[l]=!(a[l]=p))}}else y=mt(y===a?y.splice(h,y.length):y),i?i(null,a,y,u):H.apply(a,y)})}function vt(e){var t,n,r,o=e.length,a=i.relative[e[0].type],s=a||i.relative[" "],u=a?1:0,c=ht(function(e){return e===t},s,!0),p=ht(function(e){return M.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>u;u++)if(n=i.relative[e[u].type])f=[ht(gt(f),n)];else{if(n=i.filter[e[u].type].apply(null,e[u].matches),n[x]){for(r=++u;o>r;r++)if(i.relative[e[r].type])break;return yt(u>1&>(f),u>1&&dt(e.slice(0,u-1)).replace(W,"$1"),n,r>u&&vt(e.slice(u,r)),o>r&&vt(e=e.slice(r)),o>r&&dt(e))}f.push(n)}return gt(f)}function bt(e,t){var n=0,o=t.length>0,a=e.length>0,s=function(s,u,c,f,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,T=l,C=s||a&&i.find.TAG("*",d&&u.parentNode||u),k=N+=null==T?1:Math.random()||.1;for(w&&(l=u!==p&&u,r=n);null!=(h=C[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,u,c)){f.push(h);break}w&&(N=k,r=++n)}o&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,o&&b!==v){g=0;while(m=t[g++])m(x,y,u,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=L.call(f));y=mt(y)}H.apply(f,y),w&&!s&&y.length>0&&v+t.length>1&&st.uniqueSort(f)}return w&&(N=k,l=T),x};return o?ot(s):s}s=st.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=ft(e)),n=t.length;while(n--)o=vt(t[n]),o[x]?r.push(o):i.push(o);o=S(e,bt(i,r))}return o};function xt(e,t,n){var r=0,i=t.length;for(;i>r;r++)st(e,t[r],n);return n}function wt(e,t,n,r){var o,a,u,l,c,p=ft(e);if(!r&&1===p.length){if(a=p[0]=p[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&!d&&i.relative[a[1].type]){if(t=i.find.ID(u.matches[0].replace(et,tt),t)[0],!t)return n;e=e.slice(a.shift().value.length)}o=U.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],i.relative[l=u.type])break;if((c=i.find[l])&&(r=c(u.matches[0].replace(et,tt),V.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=r.length&&dt(a),!e)return H.apply(n,q.call(r,0)),n;break}}}return s(e,p)(r,t,d,n,V.test(e)),n}i.pseudos.nth=i.pseudos.eq;function Tt(){}i.filters=Tt.prototype=i.pseudos,i.setFilters=new Tt,c(),st.attr=b.attr,b.find=st,b.expr=st.selectors,b.expr[":"]=b.expr.pseudos,b.unique=st.uniqueSort,b.text=st.getText,b.isXMLDoc=st.isXML,b.contains=st.contains}(e);var at=/Until$/,st=/^(?:parents|prev(?:Until|All))/,ut=/^.[^:#\[\.,]*$/,lt=b.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};b.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return r=this,this.pushStack(b(e).filter(function(){for(t=0;i>t;t++)if(b.contains(r[t],this))return!0}));for(n=[],t=0;i>t;t++)b.find(e,this[t],n);return n=this.pushStack(i>1?b.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t,n=b(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(b.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1))},filter:function(e){return this.pushStack(ft(this,e,!0))},is:function(e){return!!e&&("string"==typeof e?lt.test(e)?b(e,this.context).index(this[0])>=0:b.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],a=lt.test(e)||"string"!=typeof e?b(e,t||this.context):0;for(;i>r;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&11!==n.nodeType){if(a?a.index(n)>-1:b.find.matchesSelector(n,e)){o.push(n);break}n=n.parentNode}}return this.pushStack(o.length>1?b.unique(o):o)},index:function(e){return e?"string"==typeof e?b.inArray(this[0],b(e)):b.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?b(e,t):b.makeArray(e&&e.nodeType?[e]:e),r=b.merge(this.get(),n);return this.pushStack(b.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),b.fn.andSelf=b.fn.addBack;function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}b.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return b.dir(e,"parentNode")},parentsUntil:function(e,t,n){return b.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return b.dir(e,"nextSibling")},prevAll:function(e){return b.dir(e,"previousSibling")},nextUntil:function(e,t,n){return b.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return b.dir(e,"previousSibling",n)},siblings:function(e){return b.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return b.sibling(e.firstChild)},contents:function(e){return b.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:b.merge([],e.childNodes)}},function(e,t){b.fn[e]=function(n,r){var i=b.map(this,t,n);return at.test(e)||(r=n),r&&"string"==typeof r&&(i=b.filter(r,i)),i=this.length>1&&!ct[e]?b.unique(i):i,this.length>1&&st.test(e)&&(i=i.reverse()),this.pushStack(i)}}),b.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),1===t.length?b.find.matchesSelector(t[0],e)?[t[0]]:[]:b.find.matches(e,t)},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!b(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return b.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=b.grep(e,function(e){return 1===e.nodeType});if(ut.test(t))return b.filter(t,r,!n);t=b.filter(t,r)}return b.grep(e,function(e){return b.inArray(e,t)>=0===n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:b.support.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},jt=dt(o),Dt=jt.appendChild(o.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,b.fn.extend({text:function(e){return b.access(this,function(e){return e===t?b.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(b.isFunction(e))return this.each(function(t){b(this).wrapAll(e.call(this,t))});if(this[0]){var t=b(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return b.isFunction(e)?this.each(function(t){b(this).wrapInner(e.call(this,t))}):this.each(function(){var t=b(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=b.isFunction(e);return this.each(function(n){b(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){b.nodeName(this,"body")||b(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.insertBefore(e,this.firstChild)})},before:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=0;for(;null!=(n=this[r]);r++)(!e||b.filter(e,[n]).length>0)&&(t||1!==n.nodeType||b.cleanData(Ot(n)),n.parentNode&&(t&&b.contains(n.ownerDocument,n)&&Mt(Ot(n,"script")),n.parentNode.removeChild(n)));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&b.cleanData(Ot(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&b.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return b.clone(this,e,t)})},html:function(e){return b.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!b.support.htmlSerialize&&mt.test(e)||!b.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(b.cleanData(Ot(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){var t=b.isFunction(e);return t||"string"==typeof e||(e=b(e).not(this).detach()),this.domManip([e],!0,function(e){var t=this.nextSibling,n=this.parentNode;n&&(b(this).remove(),n.insertBefore(e,t))})},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=f.apply([],e);var i,o,a,s,u,l,c=0,p=this.length,d=this,h=p-1,g=e[0],m=b.isFunction(g);if(m||!(1>=p||"string"!=typeof g||b.support.checkClone)&&Ct.test(g))return this.each(function(i){var o=d.eq(i);m&&(e[0]=g.call(this,i,n?o.html():t)),o.domManip(e,n,r)});if(p&&(l=b.buildFragment(e,this[0].ownerDocument,!1,this),i=l.firstChild,1===l.childNodes.length&&(l=i),i)){for(n=n&&b.nodeName(i,"tr"),s=b.map(Ot(l,"script"),Ht),a=s.length;p>c;c++)o=l,c!==h&&(o=b.clone(o,!0,!0),a&&b.merge(s,Ot(o,"script"))),r.call(n&&b.nodeName(this[c],"table")?Lt(this[c],"tbody"):this[c],o,c);if(a)for(u=s[s.length-1].ownerDocument,b.map(s,qt),c=0;a>c;c++)o=s[c],kt.test(o.type||"")&&!b._data(o,"globalEval")&&b.contains(u,o)&&(o.src?b.ajax({url:o.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):b.globalEval((o.text||o.textContent||o.innerHTML||"").replace(St,"")));l=i=null}return this}});function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function Ht(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Mt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval",!t||b._data(t[r],"globalEval"))}function _t(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,o=b._data(e),a=b._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)b.event.add(t,n,s[n][r])}a.data&&(a.data=b.extend({},a.data))}}function Ft(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!b.support.noCloneEvent&&t[b.expando]){i=b._data(t);for(r in i.events)b.removeEvent(t,r,i.handle);t.removeAttribute(b.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),b.support.html5Clone&&e.innerHTML&&!b.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Nt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}b.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){b.fn[e]=function(e){var n,r=0,i=[],o=b(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),b(o[r])[t](n),d.apply(i,n.get());return this.pushStack(i)}});function Ot(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||b.nodeName(o,n)?s.push(o):b.merge(s,Ot(o,n));return n===t||n&&b.nodeName(e,n)?b.merge([e],s):s}function Bt(e){Nt.test(e.type)&&(e.defaultChecked=e.checked)}b.extend({clone:function(e,t,n){var r,i,o,a,s,u=b.contains(e.ownerDocument,e);if(b.support.html5Clone||b.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(b.support.noCloneEvent&&b.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||b.isXMLDoc(e)))for(r=Ot(o),s=Ot(e),a=0;null!=(i=s[a]);++a)r[a]&&Ft(i,r[a]);if(t)if(n)for(s=s||Ot(e),r=r||Ot(o),a=0;null!=(i=s[a]);a++)_t(i,r[a]);else _t(e,o);return r=Ot(o,"script"),r.length>0&&Mt(r,!u&&Ot(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,u,l,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===b.type(o))b.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),u=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[u]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!b.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!b.support.tbody){o="table"!==u||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)b.nodeName(l=o.childNodes[i],"tbody")&&!l.childNodes.length&&o.removeChild(l) +}b.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),b.support.appendChecked||b.grep(Ot(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===b.inArray(o,r))&&(a=b.contains(o.ownerDocument,o),s=Ot(f.appendChild(o),"script"),a&&Mt(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,u=b.expando,l=b.cache,p=b.support.deleteExpando,f=b.event.special;for(;null!=(n=e[s]);s++)if((t||b.acceptData(n))&&(o=n[u],a=o&&l[o])){if(a.events)for(r in a.events)f[r]?b.event.remove(n,r):b.removeEvent(n,r,a.handle);l[o]&&(delete l[o],p?delete n[u]:typeof n.removeAttribute!==i?n.removeAttribute(u):n[u]=null,c.push(o))}}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+x+")(.*)$","i"),Yt=RegExp("^("+x+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+x+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=b._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=b._data(r,"olddisplay",un(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&b._data(r,"olddisplay",i?n:b.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}b.fn.extend({css:function(e,n){return b.access(this,function(e,n,r){var i,o,a={},s=0;if(b.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=b.css(e,n[s],!1,o);return a}return r!==t?b.style(e,n,r):b.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?b(this).show():b(this).hide()})}}),b.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":b.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,u=b.camelCase(n),l=e.style;if(n=b.cssProps[u]||(b.cssProps[u]=tn(l,u)),s=b.cssHooks[n]||b.cssHooks[u],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:l[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(b.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||b.cssNumber[u]||(r+="px"),b.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(l[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{l[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,u=b.camelCase(n);return n=b.cssProps[u]||(b.cssProps[u]=tn(e.style,u)),s=b.cssHooks[n]||b.cssHooks[u],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||b.isNumeric(o)?o||0:a):a},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s.getPropertyValue(n)||s[n]:t,l=e.style;return s&&(""!==u||b.contains(e.ownerDocument,e)||(u=b.style(e,n)),Yt.test(u)&&Ut.test(n)&&(i=l.width,o=l.minWidth,a=l.maxWidth,l.minWidth=l.maxWidth=l.width=u,u=s.width,l.width=i,l.minWidth=o,l.maxWidth=a)),u}):o.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s[n]:t,l=e.style;return null==u&&l&&l[n]&&(u=l[n]),Yt.test(u)&&!zt.test(n)&&(i=l.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),l.left="fontSize"===n?"1em":u,u=l.pixelLeft+"px",l.left=i,a&&(o.left=a)),""===u?"auto":u});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=b.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=b.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=b.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=b.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=b.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=b.support.boxSizing&&"border-box"===b.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(b.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function un(e){var t=o,n=Gt[e];return n||(n=ln(e,t),"none"!==n&&n||(Pt=(Pt||b("